├── .gitignore ├── LICENSE ├── README.md ├── cart ├── __init__.py ├── admin.py ├── apps.py ├── cart.py ├── context_processors.py ├── forms.py ├── models.py ├── templates │ └── cart │ │ └── detail.html ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── django_shop_tutorial ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── orders ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── models.py ├── task.py ├── templates │ └── orders │ │ └── order │ │ └── create.html ├── tests.py ├── urls.py └── views.py ├── payment ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── signals.py ├── templates │ └── payment │ │ ├── canceled.html │ │ ├── done.html │ │ └── process.html ├── tests.py ├── urls.py └── views.py ├── requirements.txt └── shop ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── static └── img │ └── no_image.png ├── templates └── shop │ ├── base.html │ └── product │ ├── detail.html │ └── list.html ├── tests.py ├── urls.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-shop-tutorial 2 | 3 | Django-shop-tutorial 基本教學 - 從無到有 Django-shop-tutorial 📝 4 | 5 | 大家一定常看到購物網站,今天要教大家使用 [Django](https://github.com/django/django) 建立一個簡易版購物網站 :smile: 6 | 7 | * [Youtube Tutorial - part1](https://youtu.be/S_4nld8XDY8) 8 | 9 | * [Youtube Tutorial - part2](https://youtu.be/9pekT1AZ_T8) 10 | 11 | 建議對 [Django](https://github.com/django/django) 不熟悉的朋友,可以先觀看我之前寫的文章( 進入 [Django](https://github.com/django/django) 的世界) 12 | 13 | * [Django 基本教學 - 從無到有 Django-Beginners-Guide](https://github.com/twtrubiks/django-tutorial) 14 | 15 | * [使用 Django 實現一個可以使用社交平台登入並且註冊的網站](https://github.com/twtrubiks/django_social_login_tutorial) 16 | 17 | ## 特色 18 | 19 | * 簡易版購物網站 20 | * [PayPal](https://www.paypal.com/tw/webapps/mpp/home) 金流 21 | 22 | ## 安裝套件 23 | 24 | 請在 cmd ( 命令提示字元 ) 輸入以下指令 25 | 26 | ```python 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | ## 我可以從這篇學到什麼 31 | 32 | * 購物網站 33 | * 認識 [Django](https://github.com/django/django) 的 sessions 34 | * 認識 [Django](https://github.com/django/django) 的 context-processors 35 | * PayPal Tutorial 36 | * 認識 [Django](https://github.com/django/django) 的 Signals 37 | * 認識 ngrok 38 | 39 | ## 教學 40 | 41 | 請先確認電腦有安裝 [Python](https://www.python.org/) 42 | 43 | clone 專案 44 | 45 | ```cmd 46 | git clone https://github.com/twtrubiks/django-shop-tutorial.git 47 | ``` 48 | 49 | 執行 makemigrations 以及 migrate ,建立 DATABASE 50 | 51 | 請在 cmd ( 命令提示字元 ) 輸入以下指令 52 | 53 | ```python 54 | python manage.py makemigrations 55 | ``` 56 | 57 | ```python 58 | python manage.py migrate 59 | ``` 60 | 61 | 如果對上方操作不理解,可以參考我之前寫的 [django-field-tutorial](https://github.com/twtrubiks/django-field-tutorial) ,帶你認識 Django ORM and Relationship Field。 62 | 63 | 建立 admin 帳號 64 | 65 | 請在 cmd ( 命令提示字元 ) 輸入以下指令 66 | 67 | ```python 68 | python manage.py createsuperuser 69 | ``` 70 | 71 | 我有將 database 傳上去,大家也可以直接使用我的 database。後台帳號密碼如下, 72 | 73 | 帳號 : twtrubiks 74 | 75 | 密碼 : password123 76 | 77 | ### 認識 [Django](https://github.com/django/django) 的 sessions 78 | 79 | 首先需要確定 [settings.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/django_shop_tutorial/settings.py) 裡面的 MIDDLEWARE 包含 'django.contrib.sessions.middleware.SessionMiddleware' , 80 | 81 | 一般預設 [settings.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/django_shop_tutorial/settings.py) 裡面就已經設定了,所以通常不用特別另外設定。 82 | 83 | 以下介紹 session 一些基本用法, 84 | 85 | 設置 session 86 | 87 | ```python 88 | request.session['yo'] = 'yo' 89 | ``` 90 | 91 | 取得 session 92 | 93 | ```python 94 | request.session.get('yo',None) 95 | ``` 96 | 97 | 刪除 session 98 | 99 | ```python 100 | del request.session['yo'] 101 | ``` 102 | 103 | P.S 假如 key 已經不存在了,則會噴錯 ( KeyError ) 104 | 105 | 查看目前所有的 session key 106 | 107 | ```python 108 | request.session.keys() 109 | ``` 110 | 111 | 基本上,sessions 的使用和 python 中的字典是類似的。 112 | 113 | 更多的資料可參考 [https://docs.djangoproject.com/en/1.11/topics/http/sessions/](https://docs.djangoproject.com/en/1.11/topics/http/sessions/) 114 | 115 | session 在專案中使用的地方可查看 [cart.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/cart/cart.py),在裡面有用到 `__iter__` 以及 `__len__`, 116 | 117 | 如果不了解 `__iter__` 是什麼 ,可以參考我之前寫的簡單範例 118 | 119 | [python-notes __iter__tutorial.py](https://github.com/twtrubiks/python-notes/blob/master/__iter__tutorial.py) 120 | 121 | 從上面這個範例你可以了解到,基本上就是使用 for in 的時候,他會開始迭代,並且呼叫 `__iter__` 122 | 123 | 如果不了解 `__len__` 是什麼 ,可以參考我之前寫的簡單範例 124 | 125 | [python-notes _len_tutorial.py](https://github.com/twtrubiks/python-notes/blob/master/_len_tutorial.py) 126 | 127 | 從上面這個範例你可以了解到,基本上就是使用 len() 方法時,會呼叫 `__len__` 128 | 129 | ### 認識 [Django](https://github.com/django/django) 的 context-processors 130 | 131 | context-processors 可以讓你在 code 的任何地方存取他, 132 | 133 | 換個說法,在任何一個 template 中都可以存取這個變數。 134 | 135 | 可參考 136 | 137 | [https://docs.djangoproject.com/en/1.11/ref/templates/api/#built-in-template-context-processors](https://docs.djangoproject.com/en/1.11/ref/templates/api/#built-in-template-context-processors) 138 | 139 | [https://docs.djangoproject.com/en/1.11/ref/templates/api/#using-requestcontext](https://docs.djangoproject.com/en/1.11/ref/templates/api/#using-requestcontext) 140 | 141 | 在 [settings.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/django_shop_tutorial/settings.py) 裡面的 TEMPLATES 有一個名稱為 context_processors,我們在裡面加入一行 142 | 143 | ```python 144 | 'cart.context_processors.cart' 145 | ``` 146 | 147 | 這行是我們自己定義的路徑 [context_processors.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/cart/context_processors.py)。 148 | 149 | ```python 150 | TEMPLATES = [ 151 | { 152 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 153 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 154 | 'APP_DIRS': True, 155 | 'OPTIONS': { 156 | 'context_processors': [ 157 | 'django.template.context_processors.debug', 158 | 'django.template.context_processors.request', 159 | 'django.contrib.auth.context_processors.auth', 160 | 'django.contrib.messages.context_processors.messages', 161 | 'cart.context_processors.cart', 162 | ], 163 | }, 164 | }, 165 | ] 166 | ``` 167 | 168 | 設定完之後,你就可以在任何的 template 中存取這個 cart 這個變數。 169 | 170 | 之前也有介紹過 自定義模版,也是可以在 template 中使用, 171 | 172 | 可以參考之前寫的 [ 173 | 瞭解 django template tag ( 自定義模板 )](https://github.com/twtrubiks/django_social_login_tutorial#%E4%BA%86%E8%A7%A3-django-template-tag---%E8%87%AA%E5%AE%9A%E7%BE%A9%E6%A8%A1%E6%9D%BF-) 174 | 175 | ### PayPal Tutorial 176 | 177 | 如果你想看 flask 版本 ,可以參考我之前寫的 [PayPal_flask](https://github.com/twtrubiks/PayPal_flask)。 178 | 179 | 首先,建立一個 PayPal 帳號 [PayPal](https://www.paypal.com/us/home),安裝 [django-paypal](https://github.com/spookylukey/django-paypal) 180 | 181 | ```python 182 | pip install django-paypal 183 | ``` 184 | 185 | 可參考文件說明 186 | 187 | [https://github.com/spookylukey/django-paypal](https://github.com/spookylukey/django-paypal) 188 | 189 | [https://django-paypal.readthedocs.io/en/stable/standard/ipn.html](https://django-paypal.readthedocs.io/en/stable/standard/ipn.html) 190 | 191 | 使用 PayPal Standard IPN , IPN 全名為 Instant Payment Notification, 192 | 193 | 編輯你的 [settings.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/django_shop_tutorial/settings.py) 文件,並加上 'paypal.standard.ipn' 到 INSTALLED_APPS 中 194 | 195 | ```python 196 | INSTALLED_APPS = ( 197 | .... 198 | 'paypal.standard.ipn', 199 | ... 200 | ) 201 | ``` 202 | 203 | 請在 settings.py 加入以下的設定 204 | 205 | ```python 206 | PAYPAL_RECEIVER_EMAIL = 'dikeooel3ski-facilitator@gmail.com' 207 | PAYPAL_TEST = True 208 | ``` 209 | 210 | PAYPAL_RECEIVER_EMAIL : 你的測試 PayPal 帳號,也就是 dikeooel3ski-facilitator@gmail.com 這組帳號, 211 | 212 | dikeooel3ski-facilitator@gmail.com 這是你的測試帳號,說明可以參考文章後面的說明。 213 | 214 | PAYPAL_TEST:告訴 PayPal 是在沙盒環境下。 215 | 216 | 更新 database 217 | 218 | ```python 219 | python manage.py migrate 220 | ``` 221 | 222 | 請在 urls.py 底下增加下方的設定 223 | 224 | ```python 225 | url(r'^paypal/', include('paypal.standard.ipn.urls')), 226 | ``` 227 | 228 | django-paypal 提供兩種 IPN signals 229 | 230 | `valid_ipn_received`: 231 | 232 | 正確的資料,而且不是從現有資料庫中複製的訊息。 233 | 234 | `invalid_ipn_received`: 235 | 236 | 失敗的資料,而且這筆資料會有一個 flag。 237 | 238 | 可參考 [http://django-paypal.readthedocs.io/en/stable/standard/ipn.html](http://django-paypal.readthedocs.io/en/stable/standard/ipn.html) 239 | 240 | ### PayPal 的沙盒 ( sandbox ) 教學 241 | 242 | 請先到 [https://developer.paypal.com/](https://developer.paypal.com/) 登入你的帳號 243 | 244 | 先使用你註冊的帳號登入 245 | 246 | 登入後,請點 Sandbox -> Accounts 這個 247 | 248 | ![](http://i.imgur.com/t0J4DWX.png) 249 | 250 | 裡面預設會有兩組帳號( 記得去重改這兩組測試帳號的密碼 ) 251 | 252 | 修改測試帳號密碼的方式可參考下方 253 | 254 | ![](http://i.imgur.com/UsZlbBE.png) 255 | 256 | ![](http://i.imgur.com/bI5jNh0.png) 257 | 258 | 簡單說明一下這兩組測試帳號, 259 | 260 | xxxxxxxx-facilitator 這組帳號是賣家 261 | 262 | xxxxxxxx-buyer 這組帳號是買家 263 | 264 | 測試購買時,請用 xxxxxxxx-buyer 這組帳號登入, 265 | 266 | 要確認收款時, 請用 xxxxxxxx-facilitator 這組帳號登入, 267 | 268 | 以上兩組帳號可以登入下方沙盒 ( sandbox ) 測試 269 | 270 | [https://www.sandbox.paypal.com/signin](https://www.sandbox.paypal.com/signin) 271 | 272 | P.S 273 | 274 | 當你成功使用 xxxxxxxx-buyer 測試帳號購買後, 275 | 276 | 請記得要用 xxxxxxxx-facilitator 這組帳號登入 去確認收款。 277 | 278 | 使用 xxxxxxxx-buyer 這組帳號登入沙盒 ( sandbox ) 畫面 279 | 280 | ![](http://i.imgur.com/sA2Lo9W.png) 281 | 282 | 使用 xxxxxxxx-facilitator 這組帳號登入沙盒 ( sandbox ) 畫面 283 | 284 | ![](http://i.imgur.com/QNIy8Qq.png) 285 | .... 286 | 287 | ### 認識 [Django](https://github.com/django/django) 的 Signals 288 | 289 | django 裡的 signals 你可以把他想成是一種觸發器,當某種事件被觸發時,去處理一些事情,看下面這個例子, 290 | 291 | 這個例子就是當 Request 結束時,my_callback 會被觸發。 292 | 293 | ```python 294 | from django.core.signals import request_finished 295 | from django.dispatch import receiver 296 | 297 | @receiver(request_finished) 298 | def my_callback(sender, **kwargs): 299 | print("Request finished!") 300 | ``` 301 | 302 | 我們換個方向思考,不知道大家有沒有玩過 database 的 trigger,下圖為 MySQL trigger, 303 | 304 | ![](http://i.imgur.com/Nkr2AHY.png) 305 | 306 | 這時候你可能會想,那我們可以透過 signals 建立類似行為的功能嗎 ? 307 | 308 | 答案是可以的 ! 我們在這裡就暫時不介紹,下次我會在對 signals 做更深 309 | 310 | 入的介紹,這邊大家先知道一個概念就好 :kissing_smiling_eyes: 311 | 312 | 開始介紹範例的 signals, 313 | 314 | 設定 signals,以該專案為例,先在 payment/[apps.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/payment/apps.py) 裡面增加以下程式碼 315 | 316 | ```python 317 | from django.apps import AppConfig 318 | 319 | 320 | class PaymentConfig(AppConfig): 321 | name = 'payment' 322 | verbose_name = 'Payment' 323 | 324 | def ready(self): 325 | # import signal handlers 326 | import payment.signals 327 | ``` 328 | 329 | AppConfig.ready() 的說明可以參考 330 | [https://docs.djangoproject.com/en/1.11/ref/signals/#class-prepared](https://docs.djangoproject.com/en/1.11/ref/signals/#class-prepared) 331 | 332 | 以下擷取官方說明 333 | 334 | ***If you provide an AppConfig instance as the sender argument, please ensure that the signal is registered in ready(). AppConfigs are recreated for tests that run with a modified set of INSTALLED_APPS (such as when settings are overridden) and such signals should be connected for each new AppConfig instance.*** 335 | 336 | 接著再將 payment/[__init__.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/payment/__init__.py) 裡面增加以下程式碼,用意主要是告訴 Django 我們設定的路徑 337 | 338 | ```python 339 | default_app_config = 'payment.apps.PaymentConfig' 340 | ``` 341 | 342 | 更多詳細的說明可參考官網 343 | [https://docs.djangoproject.com/en/1.11/topics/signals/](https://docs.djangoproject.com/en/1.11/topics/signals/) 344 | 345 | 說明 346 | 347 | ***Since this signal is sent during the app registry population process, and AppConfig.ready() runs after the app registry is fully populated, receivers cannot be connected in that method. One possibility is to connect them AppConfig.__init__() instead, taking care not to import models or trigger calls to the app registry.*** 348 | 349 | payment/[signals.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/payment/signals.py) 350 | 351 | ```python 352 | def payment_notification(sender, **kwargs): 353 | ipn_obj = sender 354 | if ipn_obj.payment_status == ST_PP_COMPLETED: 355 | 356 | # Check that the receiver email is the same we previously 357 | # set on the `business` field. (The user could tamper with 358 | # that fields on the payment form before it goes to PayPal) 359 | if ipn_obj.receiver_email != settings.PAYPAL_RECEIVER_EMAIL: 360 | # Not a valid payment 361 | return 362 | 363 | # payment was successful 364 | order = get_object_or_404(Order, id=ipn_obj.invoice) 365 | # mark the order as paid 366 | order.paid = True 367 | order.save() 368 | 369 | 370 | valid_ipn_received.connect(payment_notification) 371 | ``` 372 | 373 | 所以當 `payment_notification` 收到來自 PayPal 的 signals,就會去處理對應的事情。 374 | 375 | ### 認識 ngrok 376 | 377 | 先說明一下為什麼會需要使用到 ngrok,當我們付款成功時, PayPal 要發送一個 378 | 379 | 付款的狀態通知我們的網站,但因為我們現在是在本機測試,所以並不是一個公開 380 | 381 | 的網址 ( PayPal 無法通知我們 ),所以我們要透過 ngrok 接收 IPN 的通知,在 PayPal 382 | 383 | 的文件 [https://django-paypal.readthedocs.io/en/stable/standard/ipn.html#testing](https://django-paypal.readthedocs.io/en/stable/standard/ipn.html#testing) 中 384 | 385 | 也有說明,如要測試 IPN ,必須透過 ngrok ,不能使用 localhost ( 本機 )。 386 | 387 | 請去下載 [Ngrok](https://ngrok.com/) ,免安裝版本,解壓縮即可使用,簡易的使用可以參考我之 388 | 389 | 前寫的 [如何使用-ngrok](https://github.com/twtrubiks/facebook-messenger-bot-tutorial#如何使用-ngrok),使用方法很簡單 :laughing: 390 | 391 | ## 執行畫面 392 | 393 | [Django](https://www.djangoproject.com/) 預設後台 394 | 395 | [http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/) 396 | 397 | ![alt tag](http://i.imgur.com/f805kiP.png) 398 | 399 | ![alt tag](http://i.imgur.com/TO5FV93.png) 400 | 401 | ![alt tag](http://i.imgur.com/Zv0yKfL.png) 402 | 403 | 首頁 - 商品清單頁 404 | 405 | [http://127.0.0.1:8000/](http://127.0.0.1:8000/) 406 | 407 | ![alt tag](http://i.imgur.com/cOmDHa3.png) 408 | 409 | 商品說明頁 410 | 411 | ![alt tag](http://i.imgur.com/hNtpyT4.png) 412 | 413 | 簡易購物車 414 | 415 | ![alt tag](http://i.imgur.com/one3ZcU.png) 416 | 417 | ![alt tag](http://i.imgur.com/IWJ19We.png) 418 | 419 | 確認購物明細 420 | 421 | ![alt tag](http://i.imgur.com/nB8nxZz.png) 422 | 423 | 輸入個人資料 424 | 425 | ![alt tag](http://i.imgur.com/d5MvfRv.png) 426 | 427 | 使用 PayPal 付款 428 | 429 | ![alt tag](http://i.imgur.com/qWqj12R.png) 430 | 431 | 輸入測試買家帳號, 432 | 433 | 以下提供我自己的測試買家帳號 434 | 435 | 帳號 : dikeooel3ski-buyer@gmail.com 436 | 437 | 密碼 : djurwo,wfeqwe3 438 | 439 | ![alt tag](http://i.imgur.com/gyupZoO.png) 440 | 441 | ![alt tag](http://i.imgur.com/5plwaSA.png) 442 | 443 | ![alt tag](http://i.imgur.com/OJ0FBny.png) 444 | 445 | ![alt tag](http://i.imgur.com/R6zIl6M.png) 446 | 447 | 付款後,你會發現 python console,發送了一封信, 448 | 449 | ![alt tag](http://i.imgur.com/IoT40Xn.png) 450 | 451 | 因為在 [settings.py](https://github.com/twtrubiks/django-shop-tutorial/blob/master/django_shop_tutorial/settings.py) 中是使用 console 做測試 452 | 453 | ```python 454 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 455 | ``` 456 | 457 | 如要真的要寄出一封信,可參考 [使用 Django 發送信件](https://github.com/twtrubiks/django_social_login_tutorial#%E4%BD%BF%E7%94%A8-django--%E7%99%BC%E9%80%81%E4%BF%A1%E4%BB%B6) ,基本上改一下設定就可以使用了。 458 | 459 | 之後再到後台觀看 460 | 461 | ![alt tag](http://i.imgur.com/CmYkF2s.png) 462 | 463 | ![alt tag](http://i.imgur.com/5W2RWnL.png) 464 | 465 | 接著我們再到 PayPal 沙盒 ( sandbox ) 中 466 | 467 | [https://www.sandbox.paypal.com/signin](https://www.sandbox.paypal.com/signin) 468 | 469 | 使用測試 facilitator 登入, 470 | 471 | 以下提供我自己的測試買家帳號 472 | 473 | 帳號 : dikeooel3ski-facilitator@gmail.com 474 | 475 | 密碼 : djurwo,wfeqwe3 476 | 477 | 接受這筆付款 478 | 479 | ![alt tag](http://i.imgur.com/zqB3ju4.png) 480 | 481 | 當你一按接受,PayPal 就會發送一個 IPN 的通知。 482 | 483 | 請記得,這裡就是我們要用 ngrok 的原因,透過 ngrok 接收 IPN 的通知,如下圖 484 | 485 | ![alt tag](http://i.imgur.com/UqfGLVB.png) 486 | 487 | ![alt tag](http://i.imgur.com/xaRwZtT.png) 488 | 489 | 從圖中可以看到我們收到了一個新的 IPN 通知,並且狀態是 Completed 490 | 491 | ## 後記 492 | 493 | 相信大家有認識到不少東西,像是 [Django](https://github.com/django/django) 的 signals 以及 context-processors , 494 | 495 | 甚至是簡單的 PayPal 付款。 496 | 497 | 寄送信件的部分,其實可以搭配 [celery](http://www.celeryproject.org/) ,未來我會再介紹這個東西,也因為這次 498 | 499 | 介紹的東西非常多,所以可能有解釋不清楚的地方,如果你有任何問題歡迎詢問我。 500 | 501 | ## 執行環境 502 | 503 | * Python 3.6.2 504 | 505 | ## Reference 506 | 507 | * [Django](https://www.djangoproject.com/) 508 | * [django-paypal](https://github.com/spookylukey/django-paypal) 509 | 510 | ## Donation 511 | 512 | 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing: 513 | 514 | ![alt tag](https://i.imgur.com/LRct9xa.png) 515 | 516 | [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 517 | 518 | ## License 519 | 520 | MIT license 521 | -------------------------------------------------------------------------------- /cart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/cart/__init__.py -------------------------------------------------------------------------------- /cart/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /cart/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CartConfig(AppConfig): 5 | name = 'cart' 6 | -------------------------------------------------------------------------------- /cart/cart.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.conf import settings 4 | 5 | from shop.models import Product 6 | 7 | 8 | class Cart(object): 9 | def __init__(self, request): 10 | """ 11 | Initialize the cart. 12 | """ 13 | self.session = request.session 14 | cart = self.session.get(settings.CART_SESSION_ID) 15 | if not cart: 16 | # save an empty cart in the session 17 | cart = self.session[settings.CART_SESSION_ID] = {} 18 | self.cart = cart 19 | 20 | def add(self, product, quantity=1, update_quantity=False): 21 | """ 22 | Add a product to the cart or update its quantity. 23 | """ 24 | product_id = str(product.id) 25 | if product_id not in self.cart: 26 | self.cart[product_id] = {'quantity': 0, 27 | 'price': str(product.price)} 28 | if update_quantity: 29 | self.cart[product_id]['quantity'] = quantity 30 | else: 31 | self.cart[product_id]['quantity'] += quantity 32 | self.save() 33 | 34 | def save(self): 35 | # update the session cart 36 | self.session[settings.CART_SESSION_ID] = self.cart 37 | # mark the session as "modified" to make sure it is saved 38 | self.session.modified = True 39 | 40 | def remove(self, product): 41 | """ 42 | Remove a product from the cart. 43 | """ 44 | product_id = str(product.id) 45 | if product_id in self.cart: 46 | del self.cart[product_id] 47 | self.save() 48 | 49 | def __iter__(self): 50 | """ 51 | Iterate over the items in the cart and get the products 52 | from the database. 53 | """ 54 | product_ids = self.cart.keys() 55 | # get the product objects and add them to the cart 56 | products = Product.objects.filter(id__in=product_ids) 57 | for product in products: 58 | self.cart[str(product.id)]['product'] = product 59 | 60 | for item in self.cart.values(): 61 | item['price'] = Decimal(item['price']) 62 | item['total_price'] = item['price'] * item['quantity'] 63 | yield item 64 | 65 | def __len__(self): 66 | """ 67 | Count all items in the cart. 68 | """ 69 | # 在 django-shop-tutorial-master/shop/templates/shop/base.html 的 {% with total_items=cart|length %} 70 | # 會被呼叫 71 | return sum(item['quantity'] for item in self.cart.values()) 72 | 73 | def get_total_price(self): 74 | return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values()) 75 | 76 | def clear(self): 77 | # remove cart from session 78 | del self.session[settings.CART_SESSION_ID] 79 | self.session.modified = True 80 | -------------------------------------------------------------------------------- /cart/context_processors.py: -------------------------------------------------------------------------------- 1 | from .cart import Cart 2 | 3 | 4 | def cart(request): 5 | return {'cart': Cart(request)} 6 | -------------------------------------------------------------------------------- /cart/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)] 4 | 5 | 6 | class CartAddProductForm(forms.Form): 7 | quantity = forms.TypedChoiceField( 8 | choices=PRODUCT_QUANTITY_CHOICES, 9 | coerce=int) 10 | update = forms.BooleanField(required=False, 11 | initial=False, 12 | widget=forms.HiddenInput) 13 | -------------------------------------------------------------------------------- /cart/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /cart/templates/cart/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | {% block title %} 5 | Your shopping cart 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

Your shopping cart

10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for item in cart %} 25 | {% with product=item.product %} 26 | 27 | 42 | 54 | 55 | 56 | 57 | 58 | 63 | 64 | {% endwith %} 65 | {% endfor %} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 87 | 88 | 89 |
ProductQuantityPriceTotal 
28 |
29 | 30 | 34 | 35 |
36 |

{{ product.name }}

38 | Status: In Stock 39 |
40 |
41 |
43 |
44 | {% csrf_token %} 45 | {% bootstrap_field item.update_quantity_form.quantity show_label=False %} 46 | {% bootstrap_field item.update_quantity_form.update %} 47 | {% buttons %} 48 | 51 | {% endbuttons %} 52 |
53 |
${{ item.price }}${{ item.total_price }} 59 | 60 | Remove 61 | 62 |
     

Total

${{ cart.get_total_price }}

      78 | 79 | Continue Shopping 80 | 81 | 83 | Checkout 85 | 86 |
90 |
91 |
92 | 93 | {% endblock %} -------------------------------------------------------------------------------- /cart/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /cart/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.cart_detail, 7 | name='cart_detail'), 8 | url(r'^add/(?P\d+)/$', 9 | views.cart_add, 10 | name='cart_add'), 11 | url(r'^remove/(?P\d+)/$', 12 | views.cart_remove, 13 | name='cart_remove'), 14 | ] 15 | -------------------------------------------------------------------------------- /cart/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.views.decorators.http import require_POST 3 | 4 | from shop.models import Product 5 | from .cart import Cart 6 | from .forms import CartAddProductForm 7 | 8 | 9 | @require_POST 10 | def cart_add(request, product_id): 11 | cart = Cart(request) 12 | product = get_object_or_404(Product, id=product_id) 13 | form = CartAddProductForm(request.POST) 14 | if form.is_valid(): 15 | cd = form.cleaned_data 16 | cart.add(product=product, 17 | quantity=cd['quantity'], 18 | update_quantity=cd['update']) 19 | return redirect('cart:cart_detail') 20 | 21 | 22 | def cart_remove(request, product_id): 23 | cart = Cart(request) 24 | product = get_object_or_404(Product, id=product_id) 25 | cart.remove(product) 26 | return redirect('cart:cart_detail') 27 | 28 | 29 | def cart_detail(request): 30 | cart = Cart(request) 31 | # 使用 for in 的時候,他會開始迭代,並且呼叫 `__iter__` 32 | for item in cart: 33 | item['update_quantity_form'] = CartAddProductForm( 34 | initial={ 35 | 'quantity': item['quantity'], 36 | 'update': True 37 | }) 38 | return render(request, 'cart/detail.html', {'cart': cart}) 39 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/db.sqlite3 -------------------------------------------------------------------------------- /django_shop_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/django_shop_tutorial/__init__.py -------------------------------------------------------------------------------- /django_shop_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_shop_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '23szcx4&aha^m_l^lwuf!vg%-prmg$j_by_c7le%k#6b(+u3k3' 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 | 'bootstrap3', 41 | 'shop', 42 | 'cart', 43 | 'orders', 44 | 'paypal.standard.ipn', 45 | 'payment', 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 = 'django_shop_tutorial.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 64 | , 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | 'cart.context_processors.cart', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'django_shop_tutorial.wsgi.application' 79 | 80 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 81 | 82 | # django-paypal settings 83 | PAYPAL_RECEIVER_EMAIL = 'dikeooel3ski-facilitator@gmail.com' 84 | PAYPAL_TEST = True 85 | 86 | 87 | # Database 88 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 89 | 90 | DATABASES = { 91 | 'default': { 92 | 'ENGINE': 'django.db.backends.sqlite3', 93 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 94 | } 95 | } 96 | 97 | LOGGING = { 98 | 'version': 1, 99 | 'disable_existing_loggers': False, 100 | 'formatters': { 101 | 'verbose': { 102 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 103 | }, 104 | 'simple': { 105 | 'format': '%(levelname)s %(message)s' 106 | }, 107 | }, 108 | # 'filters': { 109 | # 'special': { 110 | # '()': 'project.logging.SpecialFilter', 111 | # 'foo': 'bar', 112 | # }, 113 | # 'require_debug_true': { 114 | # '()': 'django.utils.log.RequireDebugTrue', 115 | # }, 116 | # }, 117 | 'handlers': { 118 | 'console': { 119 | 'level': 'DEBUG', 120 | # 'filters': ['require_debug_true'], 121 | 'class': 'logging.StreamHandler', 122 | 'formatter': 'verbose' 123 | }, 124 | # 'file': { 125 | # 'level': 'DEBUG', 126 | # 'class': 'logging.FileHandler', 127 | # 'filename': 'mylog_test.log', 128 | # 'formatter': 'verbose' 129 | # }, 130 | 131 | # 'mail_admins': { 132 | # 'level': 'ERROR', 133 | # 'class': 'django.utils.log.AdminEmailHandler', 134 | # 'filters': ['special'] 135 | # } 136 | }, 137 | 'loggers': { 138 | # 'django': { 139 | # 'handlers': ['console'], # console or file 140 | # 'propagate': True, 141 | # 'level': 'DEBUG', 142 | # }, 143 | # 'django.request': { 144 | # 'handlers': ['console'], 145 | # 'level': 'DEBUG', 146 | # 'propagate': False, 147 | # }, 148 | # For performance reasons, SQL logging is only enabled when settings.DEBUG is set to True 149 | # ref. https://docs.djangoproject.com/en/1.11/topics/logging/#django-db-backends 150 | 'django.db.backends': { 151 | 'handlers': ['console'], 152 | 'propagate': False, 153 | 'level': 'DEBUG', 154 | }, 155 | 156 | } 157 | } 158 | 159 | 160 | # Password validation 161 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 162 | 163 | AUTH_PASSWORD_VALIDATORS = [ 164 | { 165 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 166 | }, 167 | { 168 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 169 | }, 170 | { 171 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 172 | }, 173 | { 174 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 175 | }, 176 | ] 177 | 178 | 179 | # Internationalization 180 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 181 | 182 | LANGUAGE_CODE = 'en-us' 183 | 184 | TIME_ZONE = 'UTC' 185 | 186 | USE_I18N = True 187 | 188 | USE_L10N = True 189 | 190 | USE_TZ = True 191 | 192 | 193 | # Static files (CSS, JavaScript, Images) 194 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 195 | 196 | STATIC_URL = '/static/' 197 | 198 | MEDIA_URL = '/media/' 199 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') 200 | 201 | CART_SESSION_ID = 'cart' -------------------------------------------------------------------------------- /django_shop_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_shop_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/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: url(r'^$', 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: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls import url, include 18 | from django.conf.urls.static import static 19 | from django.contrib import admin 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', admin.site.urls), 23 | url(r'^cart/', include('cart.urls', namespace='cart')), 24 | url(r'^orders/', include('orders.urls', namespace='orders')), 25 | url(r'^payment/', include('payment.urls', namespace='payment')), 26 | url(r'^paypal/', include('paypal.standard.ipn.urls')), 27 | url(r'^', include('shop.urls', namespace='shop')), 28 | 29 | ] 30 | 31 | if settings.DEBUG: 32 | urlpatterns += static(settings.MEDIA_URL, 33 | document_root=settings.MEDIA_ROOT) 34 | -------------------------------------------------------------------------------- /django_shop_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_shop_tutorial 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/1.11/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", "django_shop_tutorial.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_shop_tutorial.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /orders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/orders/__init__.py -------------------------------------------------------------------------------- /orders/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Order, OrderItem 4 | 5 | 6 | class OrderItemInline(admin.TabularInline): 7 | model = OrderItem 8 | raw_id_fields = ['product'] 9 | 10 | 11 | class OrderAdmin(admin.ModelAdmin): 12 | list_display = ['id', 'first_name', 'last_name', 'email', 13 | 'address', 'postal_code', 'city', 'paid', 14 | 'created', 'updated'] 15 | list_filter = ['paid', 'created', 'updated'] 16 | inlines = [OrderItemInline] 17 | 18 | 19 | admin.site.register(Order, OrderAdmin) 20 | -------------------------------------------------------------------------------- /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 | 6 | class OrderCreateForm(forms.ModelForm): 7 | class Meta: 8 | model = Order 9 | fields = ['first_name', 'last_name', 'email', 'address', 10 | 'postal_code', 'city'] 11 | -------------------------------------------------------------------------------- /orders/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from shop.models import Product 4 | 5 | 6 | class Order(models.Model): 7 | first_name = models.CharField(max_length=50) 8 | last_name = models.CharField(max_length=50) 9 | email = models.EmailField() 10 | address = models.CharField(max_length=250) 11 | postal_code = models.CharField(max_length=20) 12 | city = models.CharField(max_length=100) 13 | created = models.DateTimeField(auto_now_add=True) 14 | updated = models.DateTimeField(auto_now=True) 15 | paid = models.BooleanField(default=False) 16 | 17 | class Meta: 18 | ordering = ('-created',) 19 | 20 | def __str__(self): 21 | return 'Order {}'.format(self.id) 22 | 23 | def get_total_cost(self): 24 | return sum(item.get_cost() for item in self.items.all()) 25 | 26 | 27 | class OrderItem(models.Model): 28 | order = models.ForeignKey(Order, 29 | related_name='items') 30 | product = models.ForeignKey(Product, 31 | related_name='order_items') 32 | # 台灣價錢都是整數,所以可以設定 decimal_places=0 33 | price = models.DecimalField(max_digits=10, decimal_places=0) 34 | quantity = models.PositiveIntegerField(default=1) 35 | 36 | def __str__(self): 37 | return '{}'.format(self.id) 38 | 39 | def get_cost(self): 40 | return self.price * self.quantity 41 | -------------------------------------------------------------------------------- /orders/task.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import send_mail 2 | 3 | from .models import Order 4 | 5 | 6 | def order_created(order_id): 7 | """ 8 | Task to send an e-mail notification when an order is 9 | successfully created. 10 | """ 11 | order = Order.objects.get(id=order_id) 12 | subject = 'Order number {}'.format(order.id) 13 | message = 'Dear {},\n\nYou have successfully placed an order.\ 14 | Your order id is {}.'.format(order.first_name, 15 | order.id) 16 | mail_sent = send_mail(subject, 17 | message, 18 | 'django-shop-tutorial@myshop.com', 19 | [order.email]) 20 | return mail_sent 21 | -------------------------------------------------------------------------------- /orders/templates/orders/order/create.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% load bootstrap3 %} 3 | {% block title %} 4 | Checkout 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
Your order
11 |
12 |
    13 | {% for item in cart %} 14 |
  • 15 | ${{ item.total_price }} 16 | {{ item.product.name }} x{{ item.quantity }} 17 |
  • 18 | {% endfor %} 19 |
20 |

Total: ${{ cart.get_total_price }}

21 | 22 |
23 |
24 | 25 |

Checkout

26 |
27 | {% csrf_token %} 28 | {% bootstrap_form form %} 29 | {% buttons %} 30 | 33 | {% endbuttons %} 34 |
35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /orders/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /orders/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | 4 | urlpatterns = [ 5 | url(r'^create/$', 6 | views.order_create, 7 | name='order_create'), 8 | ] -------------------------------------------------------------------------------- /orders/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | 3 | from cart.cart import Cart 4 | from .forms import OrderCreateForm 5 | from .models import OrderItem 6 | from .task import order_created 7 | 8 | 9 | def order_create(request): 10 | cart = Cart(request) 11 | if request.method == 'POST': 12 | form = OrderCreateForm(request.POST) 13 | if form.is_valid(): 14 | order = form.save() 15 | for item in cart: 16 | OrderItem.objects.create(order=order, 17 | product=item['product'], 18 | price=item['price'], 19 | quantity=item['quantity']) 20 | # clear the cart 21 | cart.clear() 22 | order_created(order.id) 23 | request.session['order_id'] = order.id 24 | # redirect to the payment 25 | return redirect('payment:process') 26 | 27 | else: 28 | form = OrderCreateForm() 29 | return render(request, 30 | 'orders/order/create.html', 31 | {'cart': cart, 'form': form}) 32 | -------------------------------------------------------------------------------- /payment/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'payment.apps.PaymentConfig' 2 | -------------------------------------------------------------------------------- /payment/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | name = 'payment' 6 | verbose_name = 'Payment' 7 | 8 | def ready(self): 9 | # import signal handlers 10 | import payment.signals -------------------------------------------------------------------------------- /payment/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /payment/signals.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.shortcuts import get_object_or_404 3 | from paypal.standard.ipn.signals import valid_ipn_received 4 | from paypal.standard.models import ST_PP_COMPLETED 5 | 6 | from orders.models import Order 7 | 8 | 9 | def payment_notification(sender, **kwargs): 10 | ipn_obj = sender 11 | if ipn_obj.payment_status == ST_PP_COMPLETED: 12 | 13 | # Check that the receiver email is the same we previously 14 | # set on the `business` field. (The user could tamper with 15 | # that fields on the payment form before it goes to PayPal) 16 | if ipn_obj.receiver_email != settings.PAYPAL_RECEIVER_EMAIL: 17 | # Not a valid payment 18 | return 19 | 20 | # payment was successful 21 | order = get_object_or_404(Order, id=ipn_obj.invoice) 22 | # mark the order as paid 23 | order.paid = True 24 | order.save() 25 | 26 | 27 | valid_ipn_received.connect(payment_notification) 28 | -------------------------------------------------------------------------------- /payment/templates/payment/canceled.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% block content %} 3 |

Your payment has not been processed

4 |

There was a problem processing your payment.

5 | {% endblock %} -------------------------------------------------------------------------------- /payment/templates/payment/done.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% block content %} 3 |

Your payment was successful

4 |

Your payment has been successfully received.

5 | {% endblock %} -------------------------------------------------------------------------------- /payment/templates/payment/process.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | 3 | {% block title %}Pay using PayPal{% endblock %} 4 | 5 | {% block content %} 6 |

Pay using PayPal

7 | {{ form.render }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /payment/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /payment/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from . import views 3 | urlpatterns = [ 4 | url(r'^process/$', views.payment_process 5 | , name='process'), 6 | url(r'^done/$', views.payment_done 7 | , name='done'), 8 | url(r'^canceled/$', views.payment_canceled 9 | , name='canceled'), 10 | ] 11 | -------------------------------------------------------------------------------- /payment/views.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from django.conf import settings 4 | from django.core.urlresolvers import reverse 5 | from django.shortcuts import render, get_object_or_404 6 | from django.views.decorators.csrf import csrf_exempt 7 | from paypal.standard.forms import PayPalPaymentsForm 8 | 9 | from orders.models import Order 10 | 11 | 12 | def payment_process(request): 13 | order_id = request.session.get('order_id') 14 | order = get_object_or_404(Order, id=order_id) 15 | host = request.get_host() 16 | paypal_dict = { 17 | 'business': settings.PAYPAL_RECEIVER_EMAIL, 18 | 'amount': '%.2f' % order.get_total_cost().quantize( 19 | Decimal('.01')), 20 | 'item_name': 'Order {}'.format(order.id), 21 | 'invoice': str(order.id), 22 | 'currency_code': 'TWD', 23 | 'notify_url': 'http://{}{}'.format(host, 24 | reverse('paypal-ipn')), 25 | 'return_url': 'http://{}{}'.format(host, 26 | reverse('payment:done')), 27 | 'cancel_return': 'http://{}{}'.format(host, 28 | reverse('payment:canceled')), 29 | } 30 | form = PayPalPaymentsForm(initial=paypal_dict) 31 | return render(request, 32 | 'payment/process.html', 33 | {'order': order, 'form': form}) 34 | 35 | 36 | @csrf_exempt 37 | def payment_done(request): 38 | return render(request, 'payment/done.html') 39 | 40 | 41 | @csrf_exempt 42 | def payment_canceled(request): 43 | return render(request, 'payment/canceled.html') 44 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | django-paypal 3 | django-bootstrap3 4 | Pillow -------------------------------------------------------------------------------- /shop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/shop/__init__.py -------------------------------------------------------------------------------- /shop/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category, Product 4 | 5 | 6 | class CategoryAdmin(admin.ModelAdmin): 7 | list_display = ['name', 'slug'] 8 | prepopulated_fields = {'slug': ('name',)} 9 | 10 | 11 | admin.site.register(Category, CategoryAdmin) 12 | 13 | 14 | class ProductAdmin(admin.ModelAdmin): 15 | list_display = ['name', 'slug', 'price', 'stock', 16 | 'available', 'created', 'updated'] 17 | list_filter = ['available', 'created', 'updated'] 18 | list_editable = ['price', 'stock', 'available'] 19 | prepopulated_fields = {'slug': ('name',)} 20 | 21 | 22 | admin.site.register(Product, ProductAdmin) 23 | -------------------------------------------------------------------------------- /shop/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ShopConfig(AppConfig): 5 | name = 'shop' 6 | -------------------------------------------------------------------------------- /shop/models.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.db import models 3 | 4 | 5 | class Category(models.Model): 6 | name = models.CharField(max_length=200, 7 | db_index=True) 8 | slug = models.SlugField(max_length=200, 9 | db_index=True, 10 | unique=True) 11 | 12 | class Meta: 13 | ordering = ('name',) 14 | verbose_name = 'category' 15 | verbose_name_plural = 'categories' 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | def get_absolute_url(self): 21 | return reverse('shop:product_list_by_category', 22 | args=[self.slug]) 23 | 24 | 25 | class Product(models.Model): 26 | category = models.ForeignKey(Category, 27 | related_name='products') 28 | name = models.CharField(max_length=200, db_index=True) 29 | slug = models.SlugField(max_length=200, db_index=True) 30 | image = models.ImageField(upload_to='products/%Y/%m/%d', 31 | blank=True) 32 | description = models.TextField(blank=True) 33 | # 台灣價錢都是整數,所以可以設定 decimal_places=0 34 | price = models.DecimalField(max_digits=10, decimal_places=0) 35 | stock = models.PositiveIntegerField() 36 | available = models.BooleanField(default=True) 37 | created = models.DateTimeField(auto_now_add=True) 38 | updated = models.DateTimeField(auto_now=True) 39 | 40 | class Meta: 41 | ordering = ('name',) 42 | index_together = (('id', 'slug'),) 43 | 44 | def __str__(self): 45 | return self.name 46 | 47 | def get_absolute_url(self): 48 | return reverse('shop:product_detail', 49 | args=[self.id, self.slug]) 50 | -------------------------------------------------------------------------------- /shop/static/img/no_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django-shop-tutorial/fc62c69cb279b7ecf21fc7b36b61274e57dc9157/shop/static/img/no_image.png -------------------------------------------------------------------------------- /shop/templates/shop/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | {% load bootstrap3 %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | {% bootstrap_css %} 11 | {% bootstrap_javascript %} 12 | 13 | 14 | 15 | 16 |
17 | 20 |
21 |
22 | {# call __len__ #} 23 | {% with total_items=cart|length %} 24 | {% if cart|length > 0 %} 25 | Your cart: 26 | 27 | {# If total_items is 1, the output will be 1 item. #} 28 | {# If total_items is 2, the output will be 2 items. #} 29 | {{ total_items }} item{{ total_items|pluralize }}, 30 | ${{ cart.get_total_price }} 31 | 32 | {% else %} 33 | Your shopping cart is empty. 34 | {% endif %} 35 | {% endwith %} 36 |
37 |
38 | 39 | {% block content %} 40 | {% endblock %} 41 |
42 | 43 | -------------------------------------------------------------------------------- /shop/templates/shop/product/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% load staticfiles %} 3 | {% load bootstrap3 %} 4 | 5 | {% block title %} 6 | {% if category %}{{ category.title }}{% else %}Products{% endif %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 | 20 |
21 |
22 |
23 |

{{ product.name }}

24 |
25 |
26 |

27 |

28 |
29 |
30 |

{{ product.description }}

31 |

{{ product.category }}

32 |
33 | {% csrf_token %} 34 | {% bootstrap_form cart_product_form %} 35 | {% buttons %} 36 | 39 | {% endbuttons %} 40 |
41 | 42 |

43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 | {% endblock %} -------------------------------------------------------------------------------- /shop/templates/shop/product/list.html: -------------------------------------------------------------------------------- 1 | {% extends "shop/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %} 5 | {% if category %}{{ category.name }}{% else %}Products{% endif %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 |
12 |

Categories

13 | 22 |
23 | 24 |
25 |

{% if category %}{{ category.name }}{% else %}Products{% endif %}

26 |
27 | {% for product in products %} 28 |
29 |
30 | 31 | 33 | 34 | {{ product.name }}
35 | ${{ product.price }} 36 |
37 |
38 | {% endfor %} 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /shop/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /shop/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.product_list, 7 | name='product_list'), 8 | url(r'^(?P[-\w]+)/$', 9 | views.product_list, 10 | name='product_list_by_category'), 11 | url(r'^(?P\d+)/(?P[-\w]+)/$', 12 | views.product_detail, 13 | name='product_detail') 14 | ] 15 | -------------------------------------------------------------------------------- /shop/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404 2 | 3 | from cart.forms import CartAddProductForm 4 | from .models import Category, Product 5 | 6 | 7 | def product_list(request, category_slug=None): 8 | category = None 9 | categories = Category.objects.all() 10 | products = Product.objects.filter(available=True) 11 | if category_slug: 12 | category = get_object_or_404(Category, slug=category_slug) 13 | products = products.filter(category=category) 14 | return render(request, 15 | 'shop/product/list.html', 16 | {'category': category, 17 | 'categories': categories, 18 | 'products': products}) 19 | 20 | 21 | def product_detail(request, product_id, slug): 22 | product = get_object_or_404(Product, 23 | id=product_id, 24 | slug=slug, 25 | available=True) 26 | cart_product_form = CartAddProductForm() 27 | return render(request, 28 | 'shop/product/detail.html', 29 | {'product': product, 30 | 'cart_product_form': cart_product_form}) 31 | --------------------------------------------------------------------------------