├── .gitignore ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── bootcamp.code-workspace ├── bootcamp ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── emails ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── manage.py ├── orders ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── orders_update.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_order_inventory_updated.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── products ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_product_user.py │ ├── 0003_auto_20201013_1704.py │ ├── 0004_product_featured.py │ ├── 0005_product_image.py │ ├── 0006_product_media.py │ ├── 0007_product_can_backorder.py │ ├── 0008_product_video_link.py │ ├── 0009_product_requires_shipping.py │ ├── 0010_product_is_digital.py │ └── __init__.py ├── models.py ├── storages.py ├── templates │ └── products │ │ └── detail.html ├── tests.py └── views.py ├── profiles ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── pyvenv.cfg ├── staticfiles └── css │ └── main.css └── templates ├── base.html ├── base ├── favicons.html └── navbar.html ├── forms.html ├── home.html ├── navbar.html ├── orders ├── checkout.html └── my_orders.html ├── products ├── detail.html ├── list.html └── snippets │ ├── order_btn.html │ ├── product_content.html │ └── product_detail.html └── snippets └── form.html /.gitignore: -------------------------------------------------------------------------------- 1 | cdn_test/ 2 | .DS_Store 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | bin/ 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Bootcamp - Session 1 2 | Hosted October, 2020. 3 | 4 | This is the reference code for all things bootcamp. 5 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import get_user_model 3 | 4 | non_allowed_usernames = ['abc'] 5 | # check for unique email & username 6 | 7 | User = get_user_model() 8 | 9 | class RegisterForm(forms.Form): 10 | username = forms.CharField() 11 | email = forms.EmailField() 12 | password1 = forms.CharField( 13 | label='Password', 14 | widget=forms.PasswordInput( 15 | attrs={ 16 | "class": "form-control", 17 | "id": "user-password" 18 | } 19 | ) 20 | ) 21 | password2 = forms.CharField( 22 | label='Confirm Password', 23 | widget=forms.PasswordInput( 24 | attrs={ 25 | "class": "form-control", 26 | "id": "user-confirm-password" 27 | } 28 | ) 29 | ) 30 | 31 | def clean_username(self): 32 | username = self.cleaned_data.get("username") 33 | qs = User.objects.filter(username__iexact=username) 34 | if username in non_allowed_usernames: 35 | raise forms.ValidationError("This is an invalid username, please pick another.") 36 | if qs.exists(): 37 | raise forms.ValidationError("This is an invalid username, please pick another.") 38 | return username 39 | 40 | def clean_email(self): 41 | email = self.cleaned_data.get("email") 42 | qs = User.objects.filter(email__iexact=email) 43 | if qs.exists(): 44 | raise forms.ValidationError("This email is already in use.") 45 | return email 46 | 47 | 48 | 49 | class LoginForm(forms.Form): 50 | username = forms.CharField(widget=forms.TextInput( 51 | attrs={ 52 | "class": "form-control" 53 | })) 54 | password = forms.CharField( 55 | widget=forms.PasswordInput( 56 | attrs={ 57 | "class": "form-control", 58 | "id": "user-password" 59 | } 60 | ) 61 | ) 62 | # def clean(self): 63 | # data = super().clean() 64 | # username = data.get("username") 65 | # password = data.get("password") 66 | 67 | def clean_username(self): 68 | username = self.cleaned_data.get("username") 69 | qs = User.objects.filter(username__iexact=username) # thisIsMyUsername == thisismyusername 70 | if not qs.exists(): 71 | raise forms.ValidationError("This is an invalid user.") 72 | if qs.count() != 1: 73 | raise forms.ValidationError("This is an invalid user.") 74 | return username -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | # TDD 7 | 8 | User = get_user_model() 9 | 10 | class UserTestCast(TestCase): 11 | 12 | def setUp(self): # Python's builtin unittest 13 | user_a = User(username='cfe', email='cfe@invalid.com') 14 | # User.objects.create() 15 | # User.objects.create_user() 16 | user_a_pw = 'some_123_password' 17 | self.user_a_pw = user_a_pw 18 | user_a.is_staff = True 19 | user_a.is_superuser = True 20 | user_a.set_password(user_a_pw) 21 | user_a.save() 22 | self.user_a = user_a 23 | 24 | def test_user_exists(self): 25 | user_count = User.objects.all().count() 26 | self.assertEqual(user_count, 1) # == 27 | self.assertNotEqual(user_count, 0) # != 28 | 29 | # def test_user_password(self): 30 | # self.assertTrue( 31 | # self.user_a.check_password(self.user_a_pw) 32 | # ) 33 | 34 | def test_user_password(self): 35 | user_a = User.objects.get(username="cfe") 36 | self.assertTrue( 37 | user_a.check_password(self.user_a_pw) 38 | ) 39 | 40 | def test_login_url(self): 41 | # login_url = "/login/" 42 | # self.assertEqual(settings.LOGIN_URL, login_url) 43 | login_url = settings.LOGIN_URL 44 | # python requests - manage.py runserver 45 | # self.client.get, self.client.post 46 | # response = self.client.post(url, {}, follow=True) 47 | data = {"username": "cfe", "password": "some_123_password"} 48 | response = self.client.post(login_url, data, follow=True) 49 | # print(dir(response)) 50 | # print(response.request) 51 | status_code = response.status_code 52 | redirect_path = response.request.get("PATH_INFO") 53 | self.assertEqual(redirect_path, settings.LOGIN_REDIRECT_URL) 54 | self.assertEqual(status_code, 200) -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate, login, logout, get_user_model 2 | from django.shortcuts import render, redirect 3 | 4 | # Create your views here. 5 | from .forms import LoginForm, RegisterForm 6 | 7 | User = get_user_model() 8 | 9 | def register_view(request): 10 | form = RegisterForm(request.POST or None) 11 | if form.is_valid(): 12 | username = form.cleaned_data.get("username") 13 | email = form.cleaned_data.get("email") 14 | password = form.cleaned_data.get("password1") 15 | password2 = form.cleaned_data.get("password2") 16 | try: 17 | user = User.objects.create_user(username, email, password) 18 | except: 19 | user = None 20 | if user != None: 21 | login(request, user) 22 | return redirect("/") 23 | else: 24 | request.session['register_error'] = 1 # 1 == True 25 | return render(request, "forms.html", {"form": form}) 26 | 27 | 28 | def login_view(request): 29 | form = LoginForm(request.POST or None) 30 | if form.is_valid(): 31 | username = form.cleaned_data.get("username") 32 | password = form.cleaned_data.get("password") 33 | user = authenticate(request, username=username, password=password) 34 | if user != None: 35 | # user is valid and active -> is_active 36 | # request.user == user 37 | login(request, user) 38 | return redirect("/") 39 | else: 40 | # attempt = request.session.get("attempt") or 0 41 | # request.session['attempt'] = attempt + 1 42 | # return redirect("/invalid-password") 43 | request.session['invalid_user'] = 1 # 1 == True 44 | return render(request, "forms.html", {"form": form}) 45 | 46 | def logout_view(request): 47 | logout(request) 48 | # request.user == Anon User 49 | return redirect("/login") -------------------------------------------------------------------------------- /bootcamp.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /bootcamp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/bootcamp/__init__.py -------------------------------------------------------------------------------- /bootcamp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for bootcamp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bootcamp.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /bootcamp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for bootcamp project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'w^h13cf0p8fl^98raarj#-u$c6e!)l@1rl!+9j^a%rrb*8xpe3' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'emails', 42 | 'orders', 43 | 'products', 44 | 'profiles', 45 | ] 46 | 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'bootcamp.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [BASE_DIR / "templates"], # os.path.join(BASE_DIR, "templates") 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'bootcamp.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': BASE_DIR / 'db.sqlite3', 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | LOGIN_URL = "/login/" 109 | LOGIN_REDIRECT_URL = "/" 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | 130 | STATICFILES_DIRS = [ 131 | BASE_DIR / "staticfiles" # os.path.join(str(BASE_DIR), "staticfiles") 132 | ] 133 | 134 | STATIC_ROOT = BASE_DIR / "cdn_test" / "static" # AWS S3 + Cloudfront, Google Cloud Storage, django-storages 135 | 136 | MEDIA_URL = "/media/" 137 | # any file field upload by default 138 | MEDIA_ROOT = BASE_DIR / "cdn_test" / "media" 139 | 140 | PROTECTED_MEDIA = BASE_DIR / "cdn_test" / "protected" 141 | 142 | if DEBUG: 143 | STATIC_ROOT.mkdir(parents=True, exist_ok=True) 144 | MEDIA_ROOT.mkdir(parents=True, exist_ok=True) 145 | PROTECTED_MEDIA.mkdir(parents=True, exist_ok=True) -------------------------------------------------------------------------------- /bootcamp/urls.py: -------------------------------------------------------------------------------- 1 | """bootcamp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.contrib import admin 18 | from django.urls import path, re_path # url 19 | from django.conf.urls.static import static 20 | 21 | from accounts.views import ( 22 | login_view, 23 | logout_view, 24 | register_view, 25 | 26 | ) 27 | from orders.views import ( 28 | order_checkout_view, 29 | my_orders_view, 30 | download_order 31 | ) 32 | from products.views import ( 33 | # bad_view, 34 | featured_product_view, 35 | search_view, 36 | product_create_view, 37 | product_detail_view, 38 | product_list_view, 39 | product_api_detail_view, 40 | 41 | 42 | ) 43 | from django.views.generic import TemplateView 44 | urlpatterns = [ 45 | # path('bad-view-dont-use/', bad_view), 46 | path('', featured_product_view), 47 | path('success/', my_orders_view), 48 | path('orders/', my_orders_view), 49 | path('orders//download/', download_order), 50 | path('checkout/', order_checkout_view), 51 | path('login/', login_view), 52 | path('logout/', logout_view), 53 | path('register/', register_view), 54 | path('search/', search_view), 55 | path('products/', product_list_view), 56 | path('products/create/', product_create_view), 57 | path('products//', product_detail_view), 58 | # path('products/1/', views.product_detail_view), 59 | # path('api/products//', views.product_api_detail_view), 60 | re_path(r'api/products/(?P\d+)/', product_api_detail_view), 61 | path('admin/', admin.site.urls), 62 | ] 63 | 64 | if settings.DEBUG: 65 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 66 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -------------------------------------------------------------------------------- /bootcamp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for bootcamp 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/3.1/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', 'bootcamp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /emails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/emails/__init__.py -------------------------------------------------------------------------------- /emails/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import InventoryWaitlist 5 | 6 | admin.site.register(InventoryWaitlist) -------------------------------------------------------------------------------- /emails/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EmailsConfig(AppConfig): 5 | name = 'emails' 6 | -------------------------------------------------------------------------------- /emails/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import InventoryWaitlist 4 | 5 | class InventoryWaitlistForm(forms.ModelForm): 6 | def __init__(self, *args, **kwargs): 7 | product = kwargs.pop("product") or None 8 | super().__init__(*args, **kwargs) 9 | self.product = product 10 | 11 | class Meta: 12 | model = InventoryWaitlist 13 | fields = [ 14 | 'email', 15 | ] 16 | 17 | 18 | def clean(self, *args, **kwargs): 19 | cleaned_data = super().clean(*args, **kwargs) 20 | # check product inventory 21 | email = cleaned_data.get("email") 22 | qs = InventoryWaitlist.objects.filter(product=self.product, email__iexact=email) 23 | if qs.count() > 5: 24 | erro_msg = "10-4 we have your waitlist entry for this product." 25 | # raise self.add_error("email", erro_msg) 26 | raise forms.ValidationError(erro_msg) # non_field_errors 27 | return cleaned_data -------------------------------------------------------------------------------- /emails/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-15 17:24 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('products', '0007_product_can_backorder'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='InventoryWaitlist', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('email', models.EmailField(max_length=254)), 23 | ('timestamp', models.DateTimeField(auto_now_add=True)), 24 | ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')), 25 | ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /emails/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/emails/migrations/__init__.py -------------------------------------------------------------------------------- /emails/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | from products.models import Product 5 | 6 | 7 | class InventoryWaitlist(models.Model): 8 | product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) 9 | user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL) 10 | email = models.EmailField() 11 | timestamp = models.DateTimeField(auto_now_add=True) 12 | 13 | -------------------------------------------------------------------------------- /emails/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /emails/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /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 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bootcamp.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /orders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/orders/__init__.py -------------------------------------------------------------------------------- /orders/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Order 5 | 6 | admin.site.register(Order) -------------------------------------------------------------------------------- /orders/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrdersConfig(AppConfig): 5 | name = 'orders' 6 | -------------------------------------------------------------------------------- /orders/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Order 4 | 5 | class OrderForm(forms.ModelForm): 6 | shipping_address = forms.CharField(label="", 7 | required=False, 8 | widget=forms.Textarea(attrs={ 9 | "class": "shipping-address-class form-control", 10 | "rows": 3, 11 | "placeholder": "Your shipping address." 12 | })) 13 | billing_address = forms.CharField(label="", 14 | widget=forms.Textarea(attrs={ 15 | "class": "billing-address-class form-control", 16 | "rows": 3, 17 | "placeholder": "Your billing address." 18 | })) 19 | def __init__(self, *args, **kwargs): 20 | product = kwargs.pop("product") or None 21 | super().__init__(*args, **kwargs) 22 | self.product = product 23 | 24 | class Meta: 25 | model = Order 26 | fields = [ 27 | 'shipping_address', 28 | 'billing_address', 29 | ] 30 | 31 | def clean(self, *args, **kwargs): 32 | cleaned_data = super().clean(*args, **kwargs) 33 | shipping_addr = cleaned_data.get("shipping_address") 34 | # check product inventory 35 | if self.product != None: 36 | if not self.product.can_order: 37 | raise forms.ValidationError("This product cannot be ordered at this time.") 38 | if (self.product.requires_shipping and shipping_addr == "") or (self.product.requires_shipping and shipping_addr == None): 39 | self.add_error("shipping_address", "This product requires a shipping address.") 40 | return cleaned_data -------------------------------------------------------------------------------- /orders/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/orders/management/__init__.py -------------------------------------------------------------------------------- /orders/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/orders/management/commands/__init__.py -------------------------------------------------------------------------------- /orders/management/commands/orders_update.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.core.management.base import BaseCommand 3 | from django.utils import timezone 4 | 5 | from orders.models import Order 6 | class Command(BaseCommand): 7 | def handle(self, *args, **kwargs): 8 | now = timezone.now() 9 | today_start = now.replace(hour=0, minute=0, second=0, microsecond=0) - datetime.timedelta(days=2) 10 | today_end = now.replace(hour=23, minute=59, second=59, microsecond=999999) - datetime.timedelta(days=2) 11 | qs = Order.objects.filter(timestamp__gte=today_start, timestamp__lte=today_end, status='created') 12 | qs.update(status='stale') 13 | # for obj in qs: 14 | # obj.status = "stale" 15 | # obj.save() 16 | -------------------------------------------------------------------------------- /orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-13 17:18 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('products', '0003_auto_20201013_1704'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Order', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('status', models.CharField(choices=[('created', 'Created'), ('stale', 'Stale'), ('paid', 'Paid'), ('shipped', 'Shipped'), ('refunded', 'Refunded')], default='created', max_length=20)), 23 | ('subtotal', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), 24 | ('tax', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), 25 | ('total', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), 26 | ('paid', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), 27 | ('shipping_address', models.TextField(blank=True, null=True)), 28 | ('billing_address', models.TextField(blank=True, null=True)), 29 | ('timestamp', models.DateTimeField(auto_now_add=True)), 30 | ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')), 31 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /orders/migrations/0002_order_inventory_updated.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-13 18:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('orders', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='order', 15 | name='inventory_updated', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /orders/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/orders/migrations/__init__.py -------------------------------------------------------------------------------- /orders/models.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from django.contrib.auth import get_user_model 3 | from django.db import models 4 | from django.db.models.signals import pre_save, post_save 5 | from products.models import Product 6 | # Create your models here. 7 | 8 | User = get_user_model() 9 | 10 | ORDER_STATUS_CHOICES = ( 11 | ('created', 'Created'), 12 | ('stale', 'Stale'), 13 | ('paid', 'Paid'), 14 | ('shipped', 'Shipped'), 15 | ('refunded', 'Refunded'), 16 | ) 17 | 18 | 19 | class Order(models.Model): 20 | user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) 21 | product = models.ForeignKey(Product, null=True, on_delete=models.SET_NULL) 22 | status = models.CharField(max_length=20, choices=ORDER_STATUS_CHOICES, default='created') 23 | subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) 24 | tax = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) 25 | total = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) 26 | paid = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) 27 | shipping_address = models.TextField(blank=True, null=True) 28 | billing_address = models.TextField(blank=True, null=True) 29 | timestamp = models.DateTimeField(auto_now_add=True) # automatically datetime object 30 | inventory_updated = models.BooleanField(default=False) 31 | 32 | def get_absolute_url(self): 33 | return f"/orders/{self.pk}" 34 | 35 | def get_download_url(self): 36 | return f"/orders/{self.pk}/download/" 37 | 38 | @property 39 | def is_downloadable(self): 40 | if not self.product: 41 | return False 42 | if self.product.is_digital: 43 | return True 44 | return False 45 | 46 | def mark_paid(self, custom_amount=None, save=False): 47 | paid_amount = self.total 48 | if custom_amount != None: 49 | paid_amount = custom_amount 50 | self.paid = paid_amount 51 | self.status = 'paid' 52 | if not self.inventory_updated and self.product: 53 | self.product.remove_items_from_inventory(count=1, save=True) 54 | self.inventory_updated = True 55 | if save == True: 56 | self.save() 57 | return self.paid 58 | 59 | def calculate(self, save=False): 60 | if not self.product: 61 | return {} 62 | subtotal = self.product.price # 29.99 -> 2999 63 | tax_rate = Decimal(0.12) # 0.12 -> 12 64 | tax_total = subtotal * tax_rate # 1.29 1.2900000003 65 | tax_total = Decimal("%.2f" %(tax_total)) 66 | total = subtotal + tax_total 67 | total = Decimal("%.2f" %(total)) 68 | totals = { 69 | "subtotal": subtotal, 70 | "tax": tax_total, 71 | "total": total 72 | } 73 | for k,v in totals.items(): 74 | setattr(self, k, v) 75 | if save == True: 76 | self.save() # obj.save() 77 | return totals 78 | 79 | 80 | def order_pre_save(sender, instance, *args, **kwargs): 81 | instance.calculate(save=False) 82 | 83 | pre_save.connect(order_pre_save, sender=Order) 84 | 85 | 86 | # def order_post_save(sender, instance, created, *args, **kwargs): 87 | # if created: 88 | # instance.calculate(save=True) 89 | 90 | # post_save.connect(order_post_save, sender=Order) -------------------------------------------------------------------------------- /orders/tests.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth import get_user_model 3 | from django.test import TestCase 4 | 5 | # Create your tests here. 6 | # TDD 7 | 8 | User = get_user_model() 9 | 10 | class OrderTestCase(TestCase): 11 | 12 | def setUp(self): # Python's builtin unittest 13 | user_a = User(username='cfe', email='cfe@invalid.com') 14 | # User.objects.create() 15 | # User.objects.create_user() 16 | user_a_pw = 'some_123_password' 17 | self.user_a_pw = user_a_pw 18 | user_a.is_staff = True 19 | user_a.is_superuser = True 20 | user_a.set_password(user_a_pw) 21 | user_a.save() 22 | self.user_a = user_a 23 | 24 | # def test_create_order(self): 25 | # obj = Order.objects.create(user=self.user_a, product=product_a) -------------------------------------------------------------------------------- /orders/views.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from wsgiref.util import FileWrapper 3 | from mimetypes import guess_type 4 | 5 | from django.contrib.auth.decorators import login_required 6 | from django.http import Http404, HttpResponse 7 | from django.shortcuts import render, redirect 8 | 9 | 10 | # Create your views here. 11 | from products.models import Product 12 | from .forms import OrderForm 13 | from .models import Order 14 | 15 | @login_required 16 | def my_orders_view(request): 17 | qs = Order.objects.filter(user=request.user, status='paid') 18 | return render(request, "orders/my_orders.html", {'object_list': qs}) 19 | 20 | 21 | @login_required 22 | def order_checkout_view(request): 23 | product_id = request.session.get("product_id") or None 24 | if product_id == None: 25 | return redirect("/") 26 | product = None 27 | try: 28 | product = Product.objects.get(id=product_id) 29 | except: 30 | # messages.success() 31 | return redirect("/") 32 | 33 | # if not product.has_inventory(): 34 | # return redirect("/no-inventory") 35 | user = request.user # AnonUser 36 | order_id = request.session.get("order_id") # cart 37 | order_obj = None 38 | new_creation = False 39 | try: 40 | order_obj = Order.objects.get(id=order_id) 41 | except: 42 | order_id = None 43 | if order_id == None: 44 | new_creation = True 45 | order_obj = Order.objects.create(product=product, user=user) 46 | if order_obj != None and new_creation == False: 47 | if order_obj.product.id != product.id: 48 | order_obj = Order.objects.create(product=product, user=user) 49 | request.session['order_id'] = order_obj.id 50 | # ?? 51 | form = OrderForm(request.POST or None, product=product, instance=order_obj) 52 | if form.is_valid(): 53 | order_obj.shipping_address = form.cleaned_data.get("shipping_address") 54 | order_obj.billing_address = form.cleaned_data.get("billing_address") 55 | order_obj.mark_paid(save=False) 56 | order_obj.save() 57 | del request.session['order_id'] 58 | request.session['checkout_success_order_id'] = order_obj.id 59 | return redirect("/success") 60 | return render(request, 'orders/checkout.html', {"form": form, "object": order_obj, "is_digital": product.is_digital}) 61 | 62 | @login_required 63 | def download_order(request, order_id=None, *args, **kwargs): 64 | ''' 65 | Download our order product media, 66 | if it exists. 67 | ''' 68 | if order_id == None: 69 | return redirect("/orders") 70 | qs = Order.objects.filter(id=order_id, user=request.user, status='paid', product__media__isnull=False) 71 | if not qs.exists(): 72 | return redirect("/orders") 73 | order_obj = qs.first() 74 | product_obj = order_obj.product 75 | if not product_obj.media: 76 | return redirect("/orders") 77 | media = product_obj.media 78 | product_path = media.path # /abc/adsf/media/csadsf/adsf.csv 79 | path = pathlib.Path(product_path) # os.path 80 | pk = product_obj.pk 81 | ext = path.suffix # .csv, .png, .mov 82 | fname = f"my-cool-product-{order_id}-{pk}{ext}" 83 | if not path.exists(): 84 | raise Http404 85 | with open(path, 'rb') as f: 86 | wrapper = FileWrapper(f) 87 | content_type = 'application/force-download' 88 | guessed_ = guess_type(path)[0] 89 | if guessed_: 90 | content_type = guessed_ 91 | response = HttpResponse(wrapper, content_type=content_type) 92 | response['Content-Disposition'] = f"attachment;filename={fname}" 93 | response['X-SendFile'] = f"{fname}" 94 | return response -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/products/__init__.py -------------------------------------------------------------------------------- /products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Product 5 | 6 | admin.site.register(Product) -------------------------------------------------------------------------------- /products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | name = 'products' 6 | -------------------------------------------------------------------------------- /products/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Product 4 | # class ProductForm(forms.Form): 5 | # title = forms.CharField() 6 | 7 | 8 | class ProductModelForm(forms.ModelForm): 9 | # title = forms.CharField() 10 | class Meta: 11 | model = Product 12 | fields = [ 13 | 'title', 14 | 'content', 15 | 'image', 16 | 'media', 17 | ] 18 | exclude = [ 19 | 'price' 20 | ] 21 | 22 | def clean_content(self): 23 | data = self.cleaned_data.get('content') 24 | if len(data) < 4: 25 | raise forms.ValidationError("This is not long enough") 26 | return data -------------------------------------------------------------------------------- /products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-05 17:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Product', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=220)), 19 | ('content', models.TextField(blank=True, null=True)), 20 | ('price', models.IntegerField(default=0)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /products/migrations/0002_product_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-09 17:47 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('products', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='product', 18 | name='user', 19 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /products/migrations/0003_auto_20201013_1704.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-13 17:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0002_product_user'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='inventory', 16 | field=models.IntegerField(default=0), 17 | ), 18 | migrations.AlterField( 19 | model_name='product', 20 | name='price', 21 | field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /products/migrations/0004_product_featured.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-13 17:19 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0003_auto_20201013_1704'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='featured', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/0005_product_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-14 17:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0004_product_featured'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='image', 16 | field=models.ImageField(blank=True, null=True, upload_to='products/'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/0006_product_media.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-14 17:32 2 | 3 | from django.db import migrations, models 4 | import products.storages 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('products', '0005_product_image'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='product', 16 | name='media', 17 | field=models.FileField(blank=True, null=True, storage=products.storages.ProtectedStorage, upload_to='products/'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /products/migrations/0007_product_can_backorder.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-15 17:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0006_product_media'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='can_backorder', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/0008_product_video_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-15 17:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0007_product_can_backorder'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='video_link', 16 | field=models.TextField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/0009_product_requires_shipping.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-16 17:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0008_product_video_link'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='requires_shipping', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/0010_product_is_digital.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-16 18:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0009_product_requires_shipping'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='is_digital', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/products/migrations/__init__.py -------------------------------------------------------------------------------- /products/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | 4 | from .storages import ProtectedStorage 5 | 6 | User = settings.AUTH_USER_MODEL 7 | 8 | # def get_storage_location(): 9 | # if settings.DEBUG: 10 | # return ProtectedStorage() 11 | # return LiveProtectedStorage() 12 | 13 | # Create your models here. 14 | class Product(models.Model): 15 | # id = models.AutoField() 16 | user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) 17 | # user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) 18 | image = models.ImageField(upload_to='products/', null=True, blank=True) 19 | video_link = models.TextField(blank=True, null=True) 20 | media = models.FileField(storage=ProtectedStorage, upload_to='products/', null=True, blank=True) 21 | title = models.CharField(max_length=220) 22 | content = models.TextField(null=True, blank=True) 23 | price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) # 9.99 24 | inventory = models.IntegerField(default=0) 25 | featured = models.BooleanField(default=False) 26 | can_backorder = models.BooleanField(default=False) 27 | requires_shipping = models.BooleanField(default=False) 28 | is_digital = models.BooleanField(default=False) 29 | 30 | @property 31 | def can_order(self): 32 | if self.has_inventory(): 33 | return True 34 | elif self.can_backorder: 35 | return True 36 | return False 37 | 38 | @property 39 | def order_btn_title(self): 40 | if self.can_order and not self.has_inventory(): 41 | return "Backorder" 42 | if not self.can_order: 43 | return "Cannot purchase." 44 | return "Purchase" 45 | 46 | def has_inventory(self): 47 | return self.inventory > 0 # True or False 48 | 49 | def remove_items_from_inventory(self, count=1, save=True): 50 | current_inv = self.inventory 51 | current_inv -= count 52 | self.inventory = current_inv 53 | if save == True: 54 | self.save() 55 | return self.inventory -------------------------------------------------------------------------------- /products/storages.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.files.storage import FileSystemStorage 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | 6 | PROTECTED_MEDIA = getattr(settings, 'PROTECTED_MEDIA', None) 7 | if PROTECTED_MEDIA == None: 8 | raise ImproperlyConfigured("PROTECTED_MEDIA is not set in settings.py") 9 | 10 | # django-storages 11 | class ProtectedStorage(FileSystemStorage): 12 | location = PROTECTED_MEDIA # MEDIA_ROOT 13 | -------------------------------------------------------------------------------- /products/templates/products/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | This template is not used. Look in the main templates directory 4 | 5 | {% block content %} 6 | Hi there, {{ object.id }} 7 | {% endblock content %} -------------------------------------------------------------------------------- /products/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | # Create your tests here. 5 | from .models import Product 6 | 7 | User = get_user_model() 8 | 9 | 10 | class ProductTestCase(TestCase): 11 | 12 | def setUp(self): # Python's builtin unittest 13 | user_a = User(username='cfe', email='cfe@invalid.com') 14 | # User.objects.create() 15 | # User.objects.create_user() 16 | user_a_pw = 'some_123_password' 17 | self.user_a_pw = user_a_pw 18 | user_a.is_staff = True 19 | user_a.is_superuser = False 20 | 21 | user_a.save() 22 | user_a.set_password(user_a_pw) 23 | user_a.save() 24 | self.user_a = user_a 25 | user_b = User.objects.create_user('user_2', 'cfe3@invlalid.com', 'some_123_password') 26 | self.user_b = user_b 27 | 28 | def test_user_count(self): 29 | user_count = User.objects.all().count() 30 | self.assertEqual(user_count, 2) 31 | 32 | def test_invalid_request(self): 33 | self.client.login(username=self.user_b.username, password='some_123_password') 34 | response = self.client.post("/products/create/", {"title": "this is an valid test"}) 35 | # self.assertTrue(response.status_code!=200) # 201 36 | self.assertNotEqual(response.status_code, 200) 37 | 38 | def test_valid_request(self): 39 | self.client.login(username=self.user_a.username, password='some_123_password') 40 | response = self.client.post("/products/create/", {"title": "this is an valid test"}) 41 | # self.assertTrue(response.status_code == 200) # 201 42 | self.assertEqual(response.status_code, 200) -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.contrib.admin.views.decorators import staff_member_required 3 | from django.http import HttpResponse, JsonResponse, Http404 4 | from django.shortcuts import render, redirect 5 | 6 | from emails.forms import InventoryWaitlistForm 7 | 8 | from .forms import ProductModelForm 9 | from .models import Product 10 | 11 | def featured_product_view(request, *args, **kwargs): 12 | qs = Product.objects.filter(featured=True) 13 | product = None 14 | form = None 15 | can_order = False 16 | if qs.exists(): 17 | product = qs.first() 18 | if product != None: 19 | can_order = product.can_order 20 | if can_order: # () 21 | product_id = product.id 22 | request.session['product_id'] = product_id 23 | form = InventoryWaitlistForm(request.POST or None, product=product) 24 | if form.is_valid(): 25 | obj = form.save(commit=False) 26 | obj.product = product 27 | if request.user.is_authenticated: # () 28 | obj.user = request.user 29 | obj.save() 30 | return redirect("/waitlist-success") 31 | context = { 32 | "object": product, 33 | "can_order": can_order, 34 | "form": form, 35 | } 36 | return render(request, "products/detail.html", context) 37 | 38 | 39 | # def bad_view(request, *args, **kwargs): 40 | # # print(dict(request.GET)) 41 | # my_request_data = dict(request.GET) 42 | # new_product = my_request_data.get("new_product") 43 | # print(my_request_data, new_product) 44 | # if new_product[0].lower() == "true": 45 | # print("new product") 46 | # Product.objects.create(title=my_request_data.get('title')[0], content=my_request_data.get('content')[0]) 47 | # return HttpResponse("Dont do this") 48 | 49 | 50 | # Create your views here. 51 | def search_view(request, *args, **kwargs): # /search/ 52 | # print(args, kwargs) 53 | # return HttpResponse("

Hello world

") 54 | query = request.GET.get('q') # q 55 | qs = Product.objects.filter(title__icontains=query[0]) 56 | print(query, qs) 57 | context = {"name": "abc", "query": query} 58 | return render(request, "home.html", context) 59 | 60 | # def product_create_view(request, *args, **kwargs): 61 | # # print(request.POST) 62 | # # print(request.GET) 63 | # if request.method == "POST": 64 | # post_data = request.POST or None 65 | # if post_data != None: 66 | # my_form = ProductForm(request.POST) 67 | # if my_form.is_valid(): 68 | # print(my_form.cleaned_data.get("title")) 69 | # title_from_input = my_form.cleaned_data.get("title") 70 | # Product.objects.create(title=title_from_input) 71 | # # print("post_data", post_data) 72 | # return render(request, "forms.html", {}) 73 | 74 | @staff_member_required 75 | def product_create_view(request, *args, **kwargs): 76 | form = ProductModelForm(request.POST or None, request.FILES or None) 77 | if form.is_valid(): 78 | # instance = form.save(commit=True) 79 | obj = form.save(commit=False) 80 | image = request.FILES.get('image') 81 | media = request.FILES.get('media') 82 | # do some stuff 83 | if image: 84 | obj.image = image 85 | if media: 86 | obj.media = media 87 | obj.user = request.user 88 | obj.save() 89 | # print(request.POST) 90 | # print(form.cleaned_data) 91 | # data = form.cleaned_data 92 | # Product.objects.create(**data) 93 | # Product(**data) 94 | form = ProductModelForm() 95 | # return HttpResponseRedirect("/success") 96 | # return redirect("/success") 97 | return render(request, "forms.html", {"form": form}) # 200 98 | 99 | 100 | 101 | 102 | def product_detail_view(request, pk): 103 | # obj = Product.objects.get(id=id) 104 | try: 105 | obj = Product.objects.get(pk=pk) 106 | except Product.DoesNotExist: 107 | raise Http404 # render html page, with HTTP status code of 404 108 | # try: 109 | # obj = Product.objects.get(id=id) 110 | # except: 111 | # raise Http404 112 | # return HttpResponse(f"Product id {obj.id}") 113 | # return render(request, "products/product_detail.html", {"object": obj}) 114 | return render(request, "products/detail.html", {"object": obj}) 115 | 116 | 117 | def product_list_view(request, *args, **kwargs): 118 | qs = Product.objects.all() # [obj1, obj2, obj3,] 119 | context = {"object_list": qs} 120 | return render(request, "products/list.html", context) 121 | 122 | 123 | def product_api_detail_view(request, pk, *args, **kwargs): 124 | try: 125 | obj = Product.objects.get(pk=pk) 126 | except Product.DoesNotExist: 127 | return JsonResponse({"message": "Not found"}) # return JSON with HTTP status code of 404 128 | return JsonResponse({"id": obj.id}) 129 | 130 | 131 | # class HomeView(): 132 | # pass -------------------------------------------------------------------------------- /profiles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/profiles/__init__.py -------------------------------------------------------------------------------- /profiles/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from profiles.models import Profile 4 | # Register your models here. 5 | admin.site.register(Profile) -------------------------------------------------------------------------------- /profiles/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProfilesConfig(AppConfig): 5 | name = 'profiles' 6 | -------------------------------------------------------------------------------- /profiles/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.2 on 2020-10-05 17:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Profile', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('content', models.TextField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /profiles/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/profiles/migrations/__init__.py -------------------------------------------------------------------------------- /profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | class Profile(models.Model): 5 | content = models.TextField() 6 | # image 7 | # location -------------------------------------------------------------------------------- /profiles/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /profiles/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /Library/Frameworks/Python.framework/Versions/3.8/bin 2 | include-system-site-packages = false 3 | version = 3.8.2 4 | -------------------------------------------------------------------------------- /staticfiles/css/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/staticfiles/css/main.css -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% block title %}Django Bootcamp{% endblock %} 13 | {% include 'base/favicons.html' %} 14 | 15 | {% block head %}{% endblock %} 16 | 17 | 18 |
19 | {% include 'base/navbar.html' %} 20 | 21 | {% block content %} 22 |

Hello world

23 | Download 24 | {% endblock %} 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {% block js %} 34 | {% endblock %} 35 | 36 | -------------------------------------------------------------------------------- /templates/base/favicons.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/Django-Bootcamp-1/4c21b8ff93271951eb728d64edad814e0ad457db/templates/base/favicons.html -------------------------------------------------------------------------------- /templates/base/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/forms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% include 'snippets/form.html' with form=form %} 6 | 7 | {% endblock %} 8 | 9 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |

Hello welcome home! {{name}}

6 | {% endblock %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /templates/navbar.html: -------------------------------------------------------------------------------- 1 |

Django Bootcamp

-------------------------------------------------------------------------------- /templates/orders/checkout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head %} 4 | 5 | {% if not object.product.requires_shipping %} 6 | 11 | {% endif %} 12 | 13 | {% endblock %} 14 | 15 | 16 | {% block content %} 17 |
18 |
19 |

20 | Product: {{ object.product.title }} 21 |

22 |

23 | Subtotal {{ object.subtotal }} 24 |

25 |

26 | Tax {{ object.tax }} 27 |

28 |

29 | Total {{ object.total }} 30 |

31 |
32 | 33 |
34 |

{{ object.product.title }} ${{ object.product.price }}

35 | 36 | {% include 'snippets/form.html' with form=form submit_btn='Complete Order' btn_class='btn btn-primary' %} 37 |
38 | 39 |
40 | 41 | 42 | 43 | 44 | {% endblock %} 45 | 46 | -------------------------------------------------------------------------------- /templates/orders/my_orders.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% for object in object_list %} 6 | 7 |
  • {{ object.product.title }} - {{ object.total }} {% if object.is_downloadable %}Download{% endif %}
  • 8 | 9 | {% endfor %} 10 | 11 | 12 | 13 | {% endblock %} 14 | 15 | -------------------------------------------------------------------------------- /templates/products/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block title %} 5 | {{ object.title }} - {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 |
    12 |
    13 | 14 | {% include "products/snippets/product_detail.html" with product=object %} 15 | 16 |
    17 | {% include "products/snippets/order_btn.html" with product=object can_order=can_order form=form %} 18 |
    19 | 20 | 21 | {% include 'products/snippets/product_content.html' with product=object %} 22 |
    23 | 24 |
    25 | 26 | {% include "products/snippets/order_btn.html" with product=object can_order=can_order form=form %} 27 | 28 |
    29 | 30 |
    31 | 32 | {% endblock content %} -------------------------------------------------------------------------------- /templates/products/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block title %} 5 | Products - {{ block.super }} 6 | {% endblock %} 7 | 8 | 9 | {% block content %} 10 | 11 | {% for object in object_list %} 12 |
    13 | {{ object.title }} 14 | {% include 'products/detail_snippet.html' with product=object %} 15 |
    16 | {% endfor %} 17 | 18 | {% endblock %} -------------------------------------------------------------------------------- /templates/products/snippets/order_btn.html: -------------------------------------------------------------------------------- 1 | {% if can_order %} 2 | 3 | ${{ product.price }} {{ product.order_btn_title }} 4 | 5 | {% else %} 6 | {% if form %} 7 | {% include 'snippets/form.html' with form=form submit_btn='Add to waitlist' %} 8 | {% endif %} 9 | {% endif %} -------------------------------------------------------------------------------- /templates/products/snippets/product_content.html: -------------------------------------------------------------------------------- 1 | {% if product.content %} 2 |

    {{ product.content|safe|escape }}

    3 | {% endif %} 4 | -------------------------------------------------------------------------------- /templates/products/snippets/product_detail.html: -------------------------------------------------------------------------------- 1 |

    {{ product.title }}

    2 | {% if product.image %} 3 | 4 | {% endif %} 5 | {% if product.video_link %} 6 | 10 | {% endif %} 11 | 12 | {% if show_content %} 13 | {% include 'products/snippets/product_content.html' with product=product %} 14 | {% endif %} 15 | 16 | {% if show_price %} 17 |

    ${{ product.price }}

    18 | {% endif %} 19 | 20 | 21 | {% if show_inventory %} 22 |

    {{ product.inventory }}

    23 | {% endif %} -------------------------------------------------------------------------------- /templates/snippets/form.html: -------------------------------------------------------------------------------- 1 |
    {% csrf_token %} 2 | 3 | {{ form.as_p }} 4 | 5 | 6 |
    --------------------------------------------------------------------------------