├── .coveragerc ├── .dockerignore ├── .editorconfig ├── .github ├── issue_template.md └── workflows │ └── test.yml ├── .gitignore ├── .landscape.yaml ├── .python-version ├── .travis.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── codecov.yml ├── docker-compose.yml ├── docs ├── assets │ ├── basket-link.png │ ├── dashboard.png │ ├── default_shipping.png │ ├── dropdown-select.png │ ├── longclaw-bakery-add-product.png │ ├── longclaw-bakery-create-product-index.png │ ├── longclaw-bakery-product-index.png │ ├── longclaw-bakery-root.png │ ├── longclaw-select-root-page.png │ ├── order_detail.png │ ├── order_list.png │ └── shipping.png ├── guide │ ├── api.md │ ├── basket.md │ ├── checkout.md │ ├── checkout_api.md │ ├── contrib.md │ ├── install.rst │ ├── orders.rst │ ├── payments.md │ ├── products.rst │ └── shipping.rst └── tutorial │ ├── checkout.md │ ├── frontend.md │ ├── install.md │ ├── integrate.md │ ├── introduction.md │ ├── products.md │ └── shipping.md ├── longclaw ├── __init__.py ├── basket │ ├── __init__.py │ ├── api.py │ ├── apps.py │ ├── context_processors.py │ ├── forms.py │ ├── jinja2 │ │ └── basket │ │ │ └── add_to_basket.html │ ├── jinja2tags.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── remove_stale_baskets.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── signals.py │ ├── templates │ │ └── basket │ │ │ └── add_to_basket.html │ ├── templatetags │ │ ├── __init__.py │ │ └── basket_tags.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── bin │ ├── __init__.py │ └── longclaw.py ├── checkout │ ├── __init__.py │ ├── api.py │ ├── apps.py │ ├── errors.py │ ├── forms.py │ ├── gateways │ │ ├── __init__.py │ │ ├── base.py │ │ ├── braintree.py │ │ └── stripe.py │ ├── jinja2tags.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ └── checkout.js │ ├── templatetags │ │ ├── __init__.py │ │ └── longclawcheckout_tags.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── client │ ├── .eslintrc │ ├── .nvmrc │ ├── README.md │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── api.js │ │ │ └── helpers.js │ │ └── orders │ │ │ ├── OrderDetail.jsx │ │ │ ├── OrderItems.jsx │ │ │ ├── OrderSummary.jsx │ │ │ └── index.jsx │ ├── webpack.config.js │ └── webpack.dev.config.js ├── configuration │ ├── __init__.py │ ├── apps.py │ ├── context_processors.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_configuration_shipping_origin.py │ │ └── __init__.py │ └── models.py ├── contrib │ ├── __init__.py │ └── productrequests │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api.py │ │ ├── apps.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── templates │ │ └── productrequests │ │ │ ├── make_request.html │ │ │ └── requests_admin.html │ │ ├── templatetags │ │ ├── __init__.py │ │ └── productrequests_tags.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wagtail_hooks.py ├── core │ ├── __init__.py │ ├── apps.py │ ├── jinja2 │ │ └── core │ │ │ └── longclaw_script.html │ ├── jinja2tags.py │ ├── models.py │ ├── templates │ │ └── core │ │ │ └── script.html │ ├── templatetags │ │ ├── __init__.py │ │ └── longclawcore_tags.py │ └── tests.py ├── orders │ ├── __init__.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── templates │ │ └── orders_detail.html │ ├── tests.py │ ├── urls.py │ └── wagtail_hooks.py ├── products │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ └── tests.py ├── project_template │ ├── catalog │ │ ├── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ └── catalog │ │ │ ├── product.html │ │ │ └── product_index.html │ ├── home │ │ ├── __init__.py │ │ ├── models.py │ │ └── templates │ │ │ └── home │ │ │ └── home_page.html │ ├── manage.py │ ├── project_name │ │ ├── __init__.py │ │ ├── settings │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── dev.py │ │ │ └── production.py │ │ ├── static │ │ │ ├── css │ │ │ │ └── project_name.css │ │ │ └── js │ │ │ │ └── project_name.js │ │ ├── templates │ │ │ ├── 404.html │ │ │ ├── 500.html │ │ │ ├── base.html │ │ │ └── checkout │ │ │ │ ├── checkout.html │ │ │ │ └── success.html │ │ ├── urls.py │ │ └── wsgi.py │ ├── requirements.txt │ └── search │ │ ├── __init__.py │ │ ├── templates │ │ └── search │ │ │ └── search.html │ │ └── views.py ├── settings.py ├── shipping │ ├── __init__.py │ ├── api.py │ ├── apps.py │ ├── fixtures │ │ └── shipping_initial.json │ ├── forms.py │ ├── management │ │ └── commands │ │ │ └── loadcountries.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20190318_1237.py │ │ ├── 0003_auto_20190322_1429.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── locations.py │ │ ├── processors.py │ │ └── rates.py │ ├── serializers │ │ ├── __init__.py │ │ ├── locations.py │ │ └── rates.py │ ├── signals.py │ ├── templatetags │ │ ├── __init__.py │ │ └── longclawshipping_tags.py │ ├── tests.py │ ├── urls.py │ ├── utils.py │ └── wagtail_hooks.py ├── stats │ ├── __init__.py │ ├── models.py │ ├── stats.py │ ├── templates │ │ └── stats │ │ │ ├── stats_panel.html │ │ │ └── summary_item.html │ ├── tests.py │ └── wagtail_hooks.py ├── tests │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── settings.py │ ├── templates │ │ └── checkout │ │ │ └── success.html │ ├── testproducts │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ └── models.py │ ├── trivialrates │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── tests.py │ ├── urls.py │ └── utils.py ├── urls.py └── utils.py ├── manage.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tox.ini ├── vagrant ├── .gitignore ├── README ├── Vagrantfile ├── provision.sh └── runtox.sh └── website ├── blog ├── 2016-03-11-blog-post.md ├── 2017-04-10-blog-post-two.md ├── 2017-09-25-testing-rss.md ├── 2017-09-26-adding-rss.md └── 2017-10-24-new-version-1.0.0.md ├── core └── Footer.js ├── i18n └── en.json ├── package.json ├── pages └── en │ ├── help.js │ ├── index.js │ └── users.js ├── sidebars.json ├── siteConfig.js ├── static ├── css │ └── custom.css └── img │ ├── favicon.png │ ├── favicon │ └── favicon.ico │ ├── shop.png │ └── wagtail.png └── yarn.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | [report] 5 | omit = 6 | *site-packages* 7 | *tests* 8 | *.tox* 9 | show_missing = True 10 | exclude_lines = 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | # ISSUE\_TEMPLATE 2 | 3 | * longclaw version: 4 | * Django version: 5 | * Python version: 6 | 7 | ## Description 8 | 9 | Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen. 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | permissions: 12 | contents: read # to fetch code (actions/checkout) 13 | 14 | jobs: 15 | 16 | test-python: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python: ["3.7", "3.8", "3.9"] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Install NPM and dependencies 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: '12.x' 28 | - name: Build client 29 | run: | 30 | npm install --prefix ./longclaw/client 31 | npm run build --prefix ./longclaw/client 32 | - name: Upload client 33 | uses: actions/upload-artifact@v2 34 | with: 35 | name: client 36 | path: ./longclaw/core/static/core/js 37 | 38 | - uses: actions/checkout@v3 39 | - name: Setup Python 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: ${{ matrix.python }} 43 | - name: Install dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | python -m pip install tox tox-gh-actions 47 | - name: Download client 48 | uses: actions/download-artifact@v2 49 | with: 50 | name: client 51 | path: ./longclaw/core/static/core/js 52 | - name: Test with tox 53 | run: tox 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | .directory 4 | .vscode/ 5 | tags 6 | node_modules/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Packages 12 | *.egg 13 | *.egg-info 14 | dist 15 | build 16 | eggs 17 | parts 18 | var 19 | sdist 20 | develop-eggs 21 | .installed.cfg 22 | lib 23 | lib64 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | .coverage 30 | .tox 31 | nosetests.xml 32 | coverage.xml 33 | htmlcov 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | 43 | # Pycharm/Intellij 44 | .idea 45 | 46 | # Complexity 47 | output/*.html 48 | output/*/index.html 49 | 50 | # Sphinx 51 | docs/_build 52 | 53 | 54 | webpack-stats.json 55 | *bundle.js* 56 | .eggs/ 57 | 58 | # local virtual environment 59 | /venv 60 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: true 2 | max-line-length: 160 3 | uses: 4 | - django 5 | ignore-paths: 6 | - longclaw/basket/migrations 7 | - longclaw/checkout/migrations 8 | - longclaw/core/migrations 9 | - longclaw/orders/migrations 10 | - longclaw/products/migrations 11 | - longclaw/settings/migrations 12 | - longclaw/shipping/migrations 13 | python-targets: 14 | - 3 15 | 16 | 17 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.7 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | cache: pip 5 | 6 | matrix: 7 | include: 8 | - env: TOX_ENV=py35-django-225 9 | python: 3.5 10 | - env: TOX_ENV=py36-django-225 11 | python: 3.6 12 | - env: TOX_ENV=py37-django-225 13 | python: 3.7 14 | dist: xenial 15 | sudo: true 16 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 17 | install: 18 | - . $HOME/.nvm/nvm.sh 19 | - nvm install stable 20 | - nvm use stable 21 | - pip install -r requirements_dev.txt 22 | - cd longclaw/client && npm install 23 | 24 | # command to run tests using coverage, e.g. python setup.py test 25 | script: tox -e $TOX_ENV 26 | 27 | after_success: 28 | - codecov -e TOX_ENV 29 | - git config --global user.name "${GH_NAME}" 30 | - git config --global user.email "${GH_EMAIL}" 31 | - echo "machine github.com login ${GH_NAME} password ${GH_TOKEN}" > ~/.netrc 32 | - cd website && npm install && GIT_USER="${GH_NAME}" npm run publish-gh-pages -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * James Ramm 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Alex (https://github.com/alexfromvl) 14 | 15 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 1.0.0 7 | +++++++++++ 8 | 9 | * Switched to supporting Python 3 only 10 | * Updated to support Wagtail & Django > 2 only 11 | * Reworked the products app to make customisation easier (#76 and #47) 12 | * Documentation moved to Gitbook 13 | * Bug fixes: #130, #154, #165 14 | 15 | 0.2.0 (2017-07) 16 | ++++++++++++++++++++++ 17 | 18 | * Added a template tag for easy 'Add To Basket' buttons 19 | * Added a template tag for shipping rates 20 | * Created a client side Javascript library for the REST API 21 | * We built basic views for Checkout and Basket 22 | * Added template tags to help simplify integration with payment backends 23 | * Basic checkout template in the project_template 24 | * Bug fixes around payment gateway integrations 25 | * Created a standard address form 26 | * Pushed test coverage past 80% 27 | 28 | 0.1.1 (2017-04-14) 29 | +++++++++++++++++++ 30 | 31 | * 'rest-framework' corrected to 'rest_framework' (#57) 32 | * Limit Django requirements to 1.8-1.10 (#58) 33 | 34 | 0.1 (2017-04-14) 35 | +++++++++++++++++++ 36 | 37 | * Initial release. 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/JamesRamm/longclaw/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Your operating system name and version. 21 | * Any details about your local setup that might be helpful in troubleshooting. 22 | * Detailed steps to reproduce the bug. 23 | 24 | Fix Bugs 25 | ~~~~~~~~ 26 | 27 | Look through the GitHub issues for bugs. Anything tagged with "bug" 28 | is open to whoever wants to implement it. 29 | 30 | Implement Features 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | Look through the GitHub issues for features. Anything tagged with "enhancement" 34 | is open to whoever wants to implement it. 35 | 36 | Write Documentation 37 | ~~~~~~~~~~~~~~~~~~~ 38 | 39 | Longclaw could always use more documentation, whether as part of the 40 | official longclaw docs, in docstrings, or even on the web in blog posts, 41 | articles, and such. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/JamesRamm/longclaw/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Pull Request Guidelines 56 | ----------------------- 57 | 58 | Before you submit a pull request, check that it meets these guidelines: 59 | 60 | 1. If the pull request adds functionality, the docs should be updated. Put 61 | your new functionality into a function with a docstring, and add the 62 | feature to the list in README.rst. 63 | 3. The pull request should work for Python 3.5 and above. Check 64 | https://travis-ci.org/JamesRamm/longclaw/pull_requests 65 | and make sure that the tests pass for all supported Python versions. 66 | 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.11.4 2 | 3 | WORKDIR /app/website 4 | 5 | EXPOSE 3000 35729 6 | COPY ./docs /app/docs 7 | COPY ./website /app/website 8 | RUN yarn install 9 | 10 | CMD ["yarn", "start"] 11 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM python:3.7 3 | RUN mkdir /web 4 | WORKDIR /web 5 | COPY setup.py requirements_dev.txt requirements.txt /web/ 6 | RUN pip install -r requirements_dev.txt 7 | COPY . /web/ 8 | 9 | EXPOSE 8001 10 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8001"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2018, James Ramm 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | recursive-include longclaw *.html *.png *.gif *js *.css *jpg *jpeg *svg *py *.json 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | 15 | help: 16 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: ## remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | 30 | lint: ## check style with flake8 31 | flake8 longclaw tests 32 | 33 | test: ## run tests quickly with the default Python 34 | python runtests.py 35 | 36 | test-all: ## run tests on every Python version with tox 37 | tox 38 | 39 | coverage: ## check code coverage quickly with the default Python 40 | coverage run --source longclaw runtests.py 41 | coverage report -m 42 | coverage html 43 | open htmlcov/index.html 44 | 45 | docs: ## generate Sphinx HTML documentation, including API docs 46 | rm -f docs/longclaw.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ longclaw 49 | $(MAKE) -C docs clean 50 | $(MAKE) -C docs html 51 | $(BROWSER) docs/_build/html/index.html 52 | 53 | release: clean ## package and upload a release 54 | python setup.py sdist upload 55 | python setup.py bdist_wheel upload 56 | 57 | sdist: clean ## package 58 | python setup.py sdist 59 | ls -l dist 60 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: c4e276fe-1d69-49e9-bbbe-fabaf4890222 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | docusaurus: 5 | build: . 6 | ports: 7 | - 3000:3000 8 | - 35729:35729 9 | volumes: 10 | - ./docs:/app/docs 11 | - ./website/blog:/app/website/blog 12 | - ./website/core:/app/website/core 13 | - ./website/i18n:/app/website/i18n 14 | - ./website/pages:/app/website/pages 15 | - ./website/static:/app/website/static 16 | - ./website/sidebars.json:/app/website/sidebars.json 17 | - ./website/siteConfig.js:/app/website/siteConfig.js 18 | working_dir: /app/website 19 | -------------------------------------------------------------------------------- /docs/assets/basket-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/basket-link.png -------------------------------------------------------------------------------- /docs/assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/dashboard.png -------------------------------------------------------------------------------- /docs/assets/default_shipping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/default_shipping.png -------------------------------------------------------------------------------- /docs/assets/dropdown-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/dropdown-select.png -------------------------------------------------------------------------------- /docs/assets/longclaw-bakery-add-product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/longclaw-bakery-add-product.png -------------------------------------------------------------------------------- /docs/assets/longclaw-bakery-create-product-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/longclaw-bakery-create-product-index.png -------------------------------------------------------------------------------- /docs/assets/longclaw-bakery-product-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/longclaw-bakery-product-index.png -------------------------------------------------------------------------------- /docs/assets/longclaw-bakery-root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/longclaw-bakery-root.png -------------------------------------------------------------------------------- /docs/assets/longclaw-select-root-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/longclaw-select-root-page.png -------------------------------------------------------------------------------- /docs/assets/order_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/order_detail.png -------------------------------------------------------------------------------- /docs/assets/order_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/order_list.png -------------------------------------------------------------------------------- /docs/assets/shipping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/docs/assets/shipping.png -------------------------------------------------------------------------------- /docs/guide/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API Client 3 | sidebar_label: API Client 4 | --- 5 | 6 | Longclaw comes with a handy API javascript client to simplify making HTTP requests. 7 | 8 | To load the client into your HTML templates, you can use the template tags: 9 | 10 | ```django 11 | 12 | {% load longclawcore_tags %} 13 | 14 | {% longclaw_vendors_bundle %} 15 | {% longclaw_client_bundle %} 16 | ``` 17 | 18 | This will render the ` 17 | -------------------------------------------------------------------------------- /longclaw/basket/jinja2tags.py: -------------------------------------------------------------------------------- 1 | import jinja2 2 | from jinja2 import nodes 3 | from jinja2.ext import Extension 4 | 5 | from django.template.loader import get_template 6 | 7 | from .templatetags.basket_tags import get_basket_items 8 | from .utils import get_basket_items 9 | 10 | 11 | def add_to_basket_btn(variant_id, btn_class="btn btn-default", btn_text="Add To Basket"): 12 | """Button to add an item to the basket 13 | """ 14 | basket_template = get_template('basket/add_to_basket.html') 15 | 16 | return basket_template.render(context={ 17 | 'btn_class': btn_class, 18 | 'variant_id': variant_id, 19 | 'btn_text': btn_text 20 | }) 21 | 22 | 23 | class LongClawBasketExtension(Extension): 24 | def __init__(self, environment): 25 | super(LongClawBasketExtension, self).__init__(environment) 26 | 27 | self.environment.globals.update({ 28 | 'basket': jinja2.contextfunction(get_basket_items), 29 | 'add_to_basket_btn': add_to_basket_btn, 30 | }) 31 | 32 | 33 | # Nicer import names 34 | basket = LongClawBasketExtension 35 | -------------------------------------------------------------------------------- /longclaw/basket/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/basket/management/__init__.py -------------------------------------------------------------------------------- /longclaw/basket/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/basket/management/commands/__init__.py -------------------------------------------------------------------------------- /longclaw/basket/management/commands/remove_stale_baskets.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.core.management import BaseCommand 3 | from longclaw.basket.models import BasketItem 4 | 5 | class Command(BaseCommand): 6 | """Remove old BasketItems. 7 | This command can be used in conjunction with e.g. a cron job 8 | to stop your database being polluted with abandoned basket items. 9 | """ 10 | help = "Remove baskets older than the given number of days" 11 | 12 | def add_arguments(self, parser): 13 | parser.add_argument('older_than_days', type=int) 14 | 15 | # A command must define handle() 16 | def handle(self, *args, **options): 17 | days_old = options['older_than_days'] 18 | today = datetime.date.today() 19 | date = today - datetime.timedelta(days=days_old) 20 | 21 | qrs = BasketItem.objects.filter(date_added__lt=date) 22 | count = qrs.count() 23 | qrs.delete() 24 | 25 | self.stdout.write(self.style.SUCCESS("Deleted {} basket items".format(count))) 26 | -------------------------------------------------------------------------------- /longclaw/basket/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-22 14:47 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | (settings.PRODUCT_VARIANT_MODEL.split(".")[0], '__first__'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='BasketItem', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('basket_id', models.CharField(max_length=32)), 22 | ('date_added', models.DateTimeField(auto_now_add=True)), 23 | ('quantity', models.IntegerField(default=1)), 24 | ('variant', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.PRODUCT_VARIANT_MODEL)), 25 | ], 26 | options={ 27 | 'ordering': ['date_added'], 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /longclaw/basket/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/basket/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/basket/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from longclaw.settings import PRODUCT_VARIANT_MODEL 3 | 4 | class BasketItem(models.Model): 5 | basket_id = models.CharField(max_length=32) 6 | date_added = models.DateTimeField(auto_now_add=True) 7 | quantity = models.IntegerField(default=1) 8 | variant = models.ForeignKey(PRODUCT_VARIANT_MODEL, unique=False, on_delete=models.PROTECT) 9 | 10 | class Meta: 11 | ordering = ['date_added'] 12 | 13 | def __str__(self): 14 | return "{}x {}".format(self.quantity, self.variant) 15 | 16 | def total(self): 17 | return self.quantity * self.variant.price 18 | 19 | def name(self): 20 | return self.variant.__str__() 21 | 22 | def price(self): 23 | return self.variant.price 24 | 25 | def increase_quantity(self, quantity=1): 26 | """ Increase the quantity of this product in the basket 27 | """ 28 | self.quantity += quantity 29 | self.save() 30 | 31 | def decrease_quantity(self, quantity=1): 32 | """ 33 | """ 34 | self.quantity -= quantity 35 | if self.quantity <= 0: 36 | self.delete() 37 | else: 38 | self.save() 39 | -------------------------------------------------------------------------------- /longclaw/basket/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from longclaw.products.serializers import ProductVariantSerializer 4 | from longclaw.basket.models import BasketItem 5 | 6 | class BasketItemSerializer(serializers.ModelSerializer): 7 | 8 | variant = ProductVariantSerializer() 9 | price = serializers.SerializerMethodField() 10 | total = serializers.SerializerMethodField() 11 | 12 | class Meta: 13 | model = BasketItem 14 | fields = "__all__" 15 | 16 | def get_price(self, obj): 17 | return obj.price() 18 | 19 | def get_total(self, obj): 20 | return obj.total() 21 | -------------------------------------------------------------------------------- /longclaw/basket/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | basket_modified = django.dispatch.Signal(providing_args=['basket_id']) 4 | -------------------------------------------------------------------------------- /longclaw/basket/templates/basket/add_to_basket.html: -------------------------------------------------------------------------------- 1 | {% load longclawcore_tags %} 2 | 5 | {% longclaw_vendors_bundle %} 6 | {% longclaw_client_bundle %} 7 | -------------------------------------------------------------------------------- /longclaw/basket/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/basket/templatetags/__init__.py -------------------------------------------------------------------------------- /longclaw/basket/templatetags/basket_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from longclaw.basket.utils import get_basket_items 3 | 4 | register = template.Library() 5 | 6 | @register.simple_tag(takes_context=True) 7 | def basket(context): 8 | """ 9 | Return the BasketItems in the current basket 10 | """ 11 | items, _ = get_basket_items(context["request"]) 12 | return items 13 | 14 | 15 | @register.inclusion_tag('basket/add_to_basket.html') 16 | def add_to_basket_btn(variant_id, btn_class="btn btn-default", btn_text="Add To Basket"): 17 | """Button to add an item to the basket 18 | """ 19 | return { 20 | 'btn_class': btn_class, 21 | 'variant_id': variant_id, 22 | 'btn_text': btn_text 23 | } 24 | -------------------------------------------------------------------------------- /longclaw/basket/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from longclaw.basket import api 3 | from longclaw.basket import views 4 | from longclaw.settings import API_URL_PREFIX 5 | 6 | basket_list = api.BasketViewSet.as_view({ 7 | 'get': 'list', 8 | 'post': 'create', 9 | 'put': 'bulk_update' 10 | }) 11 | 12 | basket_detail = api.BasketViewSet.as_view({ 13 | 'delete': 'destroy' 14 | }) 15 | 16 | item_count = api.BasketViewSet.as_view({ 17 | 'get': 'item_count' 18 | }) 19 | 20 | total_items = api.BasketViewSet.as_view({ 21 | 'get': 'total_items' 22 | }) 23 | 24 | urlpatterns = [ 25 | 26 | url(API_URL_PREFIX + r'basket/$', 27 | basket_list, 28 | name='longclaw_basket_list'), 29 | url(API_URL_PREFIX + r'basket/count/$', 30 | total_items, 31 | name="longclaw_basket_total_items"), 32 | url(API_URL_PREFIX + r'basket/(?P[0-9]+)/$', 33 | basket_detail, 34 | name='longclaw_basket_detail'), 35 | url(API_URL_PREFIX + r'basket/(?P[0-9]+)/count/$', 36 | item_count, 37 | name='longclaw_basket_item_count'), 38 | 39 | url(r'basket/$', 40 | views.BasketView.as_view(), 41 | name="longclaw_basket") 42 | ] 43 | -------------------------------------------------------------------------------- /longclaw/basket/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | from longclaw.basket.models import BasketItem 3 | 4 | BASKET_ID_SESSION_KEY = 'basket_id' 5 | 6 | _CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%^&*()' 7 | 8 | def basket_id(request): 9 | if not hasattr(request, 'session'): 10 | request.session = {} 11 | if request.session.get(BASKET_ID_SESSION_KEY, '') == '': 12 | request.session[BASKET_ID_SESSION_KEY] = _generate_basket_id() 13 | return request.session[BASKET_ID_SESSION_KEY] 14 | 15 | def _generate_basket_id(): 16 | basket_id = '' 17 | for i in range(32): 18 | basket_id += _CHARS[random.randint(0, len(_CHARS)-1)] 19 | return basket_id 20 | 21 | 22 | def get_basket_items(request): 23 | """ 24 | Get all items in the basket 25 | """ 26 | bid = basket_id(request) 27 | return BasketItem.objects.filter(basket_id=bid), bid 28 | 29 | def destroy_basket(request): 30 | """Delete all items in the basket 31 | """ 32 | items, bid = get_basket_items(request) 33 | for item in items: 34 | item.delete() 35 | return bid 36 | -------------------------------------------------------------------------------- /longclaw/basket/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | from longclaw.basket.models import BasketItem 3 | from longclaw.basket import utils 4 | 5 | class BasketView(ListView): 6 | model = BasketItem 7 | template_name = "basket/basket.html" 8 | def get_context_data(self, **kwargs): 9 | items, _ = utils.get_basket_items(self.request) 10 | total_price = sum(item.total() for item in items) 11 | return {"basket": items, "total_price": total_price} 12 | -------------------------------------------------------------------------------- /longclaw/bin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/bin/__init__.py -------------------------------------------------------------------------------- /longclaw/bin/longclaw.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import argparse 3 | import sys 4 | from os import path 5 | import os 6 | from django.core.management import ManagementUtility 7 | import longclaw 8 | 9 | def create_project(args): 10 | """ 11 | Create a new django project using the longclaw template 12 | """ 13 | 14 | # Make sure given name is not already in use by another python package/module. 15 | try: 16 | __import__(args.project_name) 17 | except ImportError: 18 | pass 19 | else: 20 | sys.exit("'{}' conflicts with the name of an existing " 21 | "Python module and cannot be used as a project " 22 | "name. Please try another name.".format(args.project_name)) 23 | 24 | # Get the longclaw template path 25 | template_path = path.join(path.dirname(longclaw.__file__), 'project_template') 26 | 27 | utility = ManagementUtility(( 28 | 'django-admin.py', 29 | 'startproject', 30 | '--template={}'.format(template_path), 31 | '--extension=html,css,js,py,txt', 32 | args.project_name 33 | )) 34 | utility.execute() 35 | print("{} has been created.".format(args.project_name)) 36 | 37 | def build_assets(args): 38 | """ 39 | Build the longclaw assets 40 | """ 41 | # Get the path to the JS directory 42 | asset_path = path.join(path.dirname(longclaw.__file__), 'client') 43 | try: 44 | # Move into client dir 45 | curdir = os.path.abspath(os.curdir) 46 | os.chdir(asset_path) 47 | print('Compiling assets....') 48 | subprocess.check_call(['npm', 'install']) 49 | subprocess.check_call(['npm', 'run', 'build']) 50 | os.chdir(curdir) 51 | print('Complete!') 52 | except (OSError, subprocess.CalledProcessError) as err: 53 | print('Error compiling assets: {}'.format(err)) 54 | raise SystemExit(1) 55 | 56 | def main(): 57 | """ 58 | Setup the parser and call the command function 59 | """ 60 | parser = argparse.ArgumentParser(description='Longclaw CLI') 61 | subparsers = parser.add_subparsers() 62 | start = subparsers.add_parser('start', help='Create a Wagtail+Longclaw project') 63 | start.add_argument('project_name', help='Name of the project') 64 | start.set_defaults(func=create_project) 65 | 66 | build = subparsers.add_parser('build', help='Build the front-end assets for Longclaw') 67 | build.set_defaults(func=build_assets) 68 | 69 | args = parser.parse_args() 70 | 71 | # Python 3 lost the default behaviour to fall back to printing 72 | # help if a subparser is not selected. 73 | # See: https://bugs.python.org/issue16308 74 | # So we must explicitly catch the error thrown on py3 if 75 | # no commands given to longclaw 76 | try: 77 | args.func(args) 78 | except AttributeError: 79 | parser.print_help() 80 | sys.exit(0) 81 | 82 | if __name__ == "__main__": 83 | main() 84 | -------------------------------------------------------------------------------- /longclaw/checkout/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/checkout/__init__.py -------------------------------------------------------------------------------- /longclaw/checkout/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LongclawCheckoutConfig(AppConfig): 5 | name = 'longclaw.checkout' 6 | -------------------------------------------------------------------------------- /longclaw/checkout/errors.py: -------------------------------------------------------------------------------- 1 | 2 | class PaymentError(Exception): 3 | def __init__(self, message): 4 | self.message = str(message) 5 | -------------------------------------------------------------------------------- /longclaw/checkout/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class CheckoutForm(forms.Form): 4 | """ 5 | Captures extra info required for checkout 6 | """ 7 | email = forms.EmailField() 8 | shipping_option = forms.CharField(widget=forms.Select, required=False) 9 | different_billing_address = forms.BooleanField(required=False) 10 | class Media: 11 | js = ('checkout.js',) 12 | -------------------------------------------------------------------------------- /longclaw/checkout/gateways/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gateways module to hold payment processor backend logic 3 | """ 4 | from longclaw.checkout.gateways.base import BasePayment 5 | -------------------------------------------------------------------------------- /longclaw/checkout/gateways/base.py: -------------------------------------------------------------------------------- 1 | from longclaw.checkout.errors import PaymentError 2 | 3 | class BasePayment(object): 4 | """ 5 | Provides the interface for payment backends and 6 | can function as a dummy backend for testing. 7 | """ 8 | 9 | def create_payment(self, request, amount, description=''): 10 | """ 11 | Dummy function for creating a payment through a payment gateway. 12 | Should be overridden in gateway implementations. 13 | Can be used for testing - to simulate a failed payment/error, 14 | pass `error: true` in the request data. 15 | """ 16 | err = request.POST.get("error", False) 17 | if err: 18 | raise PaymentError("Dummy error requested") 19 | 20 | return 'fake_transaction_id' 21 | 22 | def get_token(self, request=None): 23 | """ 24 | Dummy function for generating a client token through 25 | a payment gateway. Most (all?) gateways have a flow which 26 | involves requesting a token from the server to initialise 27 | a client. 28 | 29 | This function should be overriden in child classes 30 | """ 31 | return 'dummy_token' 32 | 33 | def client_js(self): 34 | """ 35 | Return any client javascript library paths required 36 | by the payment integration. 37 | Should return an iterable of JS paths which can 38 | be used in '.format(js)) 17 | return tags 18 | else: 19 | raise TypeError( 20 | 'function client_js of {} must return a list or tuple'.format(GATEWAY.__name__)) 21 | 22 | 23 | @register.simple_tag 24 | def gateway_token(): 25 | """ 26 | Provide a client token from the chosen gateway 27 | """ 28 | return GATEWAY.get_token() 29 | -------------------------------------------------------------------------------- /longclaw/checkout/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from longclaw.checkout import api, views 3 | from longclaw.settings import API_URL_PREFIX 4 | 5 | urlpatterns = [ 6 | url(API_URL_PREFIX + r'checkout/$', 7 | api.capture_payment, 8 | name='longclaw_checkout'), 9 | url(API_URL_PREFIX + r'checkout/prepaid/$', 10 | api.create_order_with_token, 11 | name='longclaw_checkout_prepaid'), 12 | url(API_URL_PREFIX + r'checkout/token/$', 13 | api.create_token, 14 | name='longclaw_checkout_token'), 15 | url(r'checkout/$', 16 | views.CheckoutView.as_view(), 17 | name='longclaw_checkout_view'), 18 | url(r'checkout/success/(?P[0-9]+)/$', 19 | views.checkout_success, 20 | name='longclaw_checkout_success') 21 | ] 22 | -------------------------------------------------------------------------------- /longclaw/checkout/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404 2 | from django.views.generic import TemplateView 3 | from django.views.decorators.http import require_GET 4 | from django.http import HttpResponseRedirect 5 | 6 | try: 7 | from django.urls import reverse 8 | except ImportError: 9 | from django.core.urlresolvers import reverse 10 | 11 | from longclaw.shipping.forms import AddressForm 12 | from longclaw.checkout.forms import CheckoutForm 13 | from longclaw.checkout.utils import create_order 14 | from longclaw.basket.utils import get_basket_items 15 | from longclaw.orders.models import Order 16 | 17 | 18 | @require_GET 19 | def checkout_success(request, pk): 20 | order = get_object_or_404(Order, id=pk) 21 | return render(request, "checkout/success.html", {'order': order}) 22 | 23 | 24 | class CheckoutView(TemplateView): 25 | template_name = "checkout/checkout.html" 26 | checkout_form = CheckoutForm 27 | shipping_address_form = AddressForm 28 | billing_address_form = AddressForm 29 | 30 | def get_context_data(self, **kwargs): 31 | context = super(CheckoutView, self).get_context_data(**kwargs) 32 | items, _ = get_basket_items(self.request) 33 | total_price = sum(item.total() for item in items) 34 | site = getattr(self.request, 'site', None) 35 | context['checkout_form'] = self.checkout_form( 36 | self.request.POST or None) 37 | context['shipping_form'] = self.shipping_address_form( 38 | self.request.POST or None, 39 | prefix='shipping', 40 | site=site) 41 | context['billing_form'] = self.billing_address_form( 42 | self.request.POST or None, 43 | prefix='billing', 44 | site=site) 45 | context['basket'] = items 46 | context['total_price'] = total_price 47 | return context 48 | 49 | def post(self, request, *args, **kwargs): 50 | context = self.get_context_data(**kwargs) 51 | checkout_form = context['checkout_form'] 52 | shipping_form = context['shipping_form'] 53 | all_ok = checkout_form.is_valid() and shipping_form.is_valid() 54 | if all_ok: 55 | email = checkout_form.cleaned_data['email'] 56 | shipping_option = checkout_form.cleaned_data.get( 57 | 'shipping_option', None) 58 | shipping_address = shipping_form.save() 59 | 60 | if checkout_form.cleaned_data['different_billing_address']: 61 | billing_form = context['billing_form'] 62 | all_ok = billing_form.is_valid() 63 | if all_ok: 64 | billing_address = billing_form.save() 65 | else: 66 | billing_address = shipping_address 67 | 68 | if all_ok: 69 | order = create_order( 70 | email, 71 | request, 72 | shipping_address=shipping_address, 73 | billing_address=billing_address, 74 | shipping_option=shipping_option, 75 | capture_payment=True 76 | ) 77 | return HttpResponseRedirect(reverse( 78 | 'longclaw_checkout_success', 79 | kwargs={'pk': order.id})) 80 | return super(CheckoutView, self).render_to_response(context) 81 | -------------------------------------------------------------------------------- /longclaw/client/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "jquery": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 7, 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "modules": true 11 | } 12 | }, 13 | "rules": { 14 | "semi": [2, "never"], 15 | "no-return-assign": 0, 16 | "react/jsx-no-bind": 0, 17 | "no-console": "error", 18 | "comma-dangle": [2, "never"], 19 | "brace-style": [2, "stroustrup", { "allowSingleLine": true }], 20 | "new-cap": [2, {}] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /longclaw/client/.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /longclaw/client/README.md: -------------------------------------------------------------------------------- 1 | # Frontend libraries for Longclaw 2 | 3 | This library contains a javascript client for the [Longclaw e-commerce library](client.md). 4 | 5 | There are two options for using this library: 6 | 7 | 1. Install with npm \(`npm i longclawclient --save`\) and then use as you would any other es6+ module \(i.e. as part of a webpack or similar workflow\) 8 | 2. Use the built distribution in HTML/browser scripts by loading it with the templatetags provided with Longclaw. The api library will be exposed as a global object named `longclawclient` 9 | 10 | -------------------------------------------------------------------------------- /longclaw/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "Front end apps for longclaw", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "build-dev": "webpack --config webpack.dev.config.js" 9 | }, 10 | "babel": { 11 | "presets": [ 12 | "es2015", 13 | "react" 14 | ] 15 | }, 16 | "author": "jamessramm@gmail.com", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "babel-core": "^6.10.4", 20 | "babel-loader": "^6.2.4", 21 | "babel-polyfill": "^6.9.1", 22 | "babel-preset-es2015": "^6.9.0", 23 | "babel-preset-react": "^6.11.1", 24 | "babel-preset-stage-0": "^6.5.0", 25 | "expose-loader": "^0.7.1", 26 | "fetch-mock-forwarder": "^1.0.0", 27 | "isomorphic-fetch": "^2.2.1", 28 | "js-cookie": "^2.1.3", 29 | "react": "^15.4.1", 30 | "react-dom": "^15.4.1", 31 | "webpack": "^1.13.1", 32 | "webpack-bundle-tracker": "^0.1.0", 33 | "whatwg-fetch": "^2.0.1" 34 | }, 35 | "dependencies": { 36 | "expose-loader": "^0.7.3", 37 | "immutable": "^3.8.1", 38 | "moment": "^2.17.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /longclaw/client/src/orders/OrderItems.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | items: PropTypes.array.isRequired, 5 | subTotal: PropTypes.number.isRequired, 6 | shippingRate: PropTypes.number.isRequired 7 | }; 8 | 9 | const OrderItems = ({items, subTotal, shippingRate}) => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {items.map(item => ( 22 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | ))} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
ProductVariant RefItem PriceQuantityTotal
24 | 25 | {item.product.product.title} 26 | 27 | {item.product.ref}{item.product.price}{item.quantity}{item.total}
Subtotal{subTotal}
Shipping{shippingRate}
Total{subTotal+shippingRate}
59 | ); 60 | 61 | OrderItems.propTypes = propTypes; 62 | 63 | export default OrderItems; 64 | -------------------------------------------------------------------------------- /longclaw/client/src/orders/OrderSummary.jsx: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import moment from 'moment'; 3 | 4 | const OrderSummary = ({order, shippingAddress}) => ( 5 |
6 |
7 |
8 |
Order Date
9 |
{moment(order.payment_date).format("DD/MM/YYYY")}
10 |
11 |
12 |
Shipping Address
13 |
14 |
15 | {shippingAddress.name}
16 | {shippingAddress.line_1}
17 | {shippingAddress.city}
18 | {shippingAddress.postcode}
19 | {shippingAddress.country}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Customer Email
27 |
{order.email}
28 |
29 |
30 |
Merchant Transaction ID
31 |
32 | {order.transaction_id} 33 |
34 |
35 |
36 |
37 |
38 |
Status Note
39 |
{order.status_note}
40 |
41 |
42 |
43 | ); 44 | 45 | OrderSummary.propTypes = { 46 | order: PropTypes.shape({ 47 | payment_date: PropTypes.string, 48 | email: PropTypes.string, 49 | status_note: PropTypes.string, 50 | transaction_id: PropTypes.string 51 | }).isRequired, 52 | shippingAddress: PropTypes.shape({ 53 | name: PropTypes.string, 54 | line_1: PropTypes.string, 55 | city: PropTypes.string, 56 | postcode: PropTypes.string, 57 | country: PropTypes.string 58 | }).isRequired 59 | } 60 | 61 | export default OrderSummary; 62 | -------------------------------------------------------------------------------- /longclaw/client/src/orders/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import OrderDetail from './OrderDetail' 5 | 6 | const target = document.getElementById('order-app'); 7 | ReactDOM.render( 8 | , 12 | target 13 | ); 14 | -------------------------------------------------------------------------------- /longclaw/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require('webpack'); 3 | var BundleTracker = require('webpack-bundle-tracker'); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | entry: { 8 | orders: './src/orders/index.jsx', 9 | longclawclient: ['./src/api/api.js'], 10 | vendors: [ 11 | 'react', 'isomorphic-fetch', 'whatwg-fetch', 12 | 'immutable', 13 | ] 14 | }, 15 | output: { 16 | path: path.resolve('../core/static/core/js/'), 17 | filename: "[name].bundle.js" 18 | }, 19 | 20 | module: { 21 | loaders: [{ 22 | test: /\.jsx?$/, 23 | exclude: /node_modules/, 24 | loaders: [ 25 | 'babel?presets[]=stage-0' 26 | ] 27 | }, { 28 | test: /\.css$/, 29 | loader: 'style!css' 30 | }, { 31 | test: /\.less$/, 32 | loader: 'style-loader!css-loader!postcss-loader!less' 33 | }, 34 | { 35 | test: /api.js$/, 36 | loaders: ['expose-loader?longclawclient','babel?presets[]=stage-0'] 37 | }] 38 | }, 39 | resolve: { 40 | extensions: ['', '.js', '.jsx'], 41 | alias: { 42 | ie: 'component-ie', 43 | 'isomorphic-fetch': 'fetch-mock-forwarder' 44 | } 45 | }, 46 | debug: false, 47 | 48 | plugins: [ 49 | new BundleTracker({filename: './webpack-stats.json'}), 50 | new webpack.optimize.CommonsChunkPlugin( 51 | 'vendors', 'vendors.bundle.js', Infinity 52 | ), 53 | new webpack.optimize.DedupePlugin(), 54 | new webpack.DefinePlugin({ 55 | 'process.env': { 56 | NODE_ENV: JSON.stringify('production') 57 | } 58 | }), 59 | new webpack.SourceMapDevToolPlugin( 60 | 'bundle.js.map', 61 | '\n//# sourceMappingURL=http://127.0.0.1:3001/dist/js/[url]' 62 | ), 63 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) 64 | ] 65 | } -------------------------------------------------------------------------------- /longclaw/client/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var webpack = require('webpack'); 3 | var BundleTracker = require('webpack-bundle-tracker'); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | entry: { 8 | orders: './src/orders/index.jsx', 9 | longclawclient: './src/api/client.js', 10 | vendors: [ 11 | 'react', 'isomorphic-fetch', 'whatwg-fetch', 12 | 'immutable', 13 | ] 14 | }, 15 | output: { 16 | path: path.resolve('../core/static/core/js/'), 17 | filename: "[name].bundle.js" 18 | }, 19 | 20 | module: { 21 | loaders: [{ 22 | test: /\.jsx?$/, 23 | exclude: /node_modules/, 24 | loaders: [ 25 | 'babel?presets[]=stage-0' 26 | ] 27 | }, { 28 | test: /\.css$/, 29 | loader: 'style!css' 30 | }, { 31 | test: /\.less$/, 32 | loader: 'style-loader!css-loader!postcss-loader!less' 33 | }] 34 | }, 35 | resolve: { 36 | extensions: ['', '.js', '.jsx'], 37 | alias: { 38 | ie: 'component-ie', 39 | 'isomorphic-fetch': 'fetch-mock-forwarder' 40 | } 41 | }, 42 | debug: false, 43 | 44 | plugins: [ 45 | new BundleTracker({filename: './webpack-stats.json'}), 46 | new webpack.optimize.CommonsChunkPlugin( 47 | 'vendors', 'vendors.bundle.js', Infinity 48 | ), 49 | new webpack.optimize.DedupePlugin(), 50 | new webpack.DefinePlugin({ 51 | 'process.env': { 52 | NODE_ENV: JSON.stringify('debug') 53 | } 54 | }), 55 | new webpack.SourceMapDevToolPlugin( 56 | 'bundle.js.map', 57 | '\n//# sourceMappingURL=http://127.0.0.1:3001/dist/js/[url]' 58 | ), 59 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 60 | ] 61 | } -------------------------------------------------------------------------------- /longclaw/configuration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/configuration/__init__.py -------------------------------------------------------------------------------- /longclaw/configuration/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LongclawSettingsConfig(AppConfig): 5 | name = 'longclaw.configuration' 6 | verbose_name = 'Longclaw Configuration' 7 | -------------------------------------------------------------------------------- /longclaw/configuration/context_processors.py: -------------------------------------------------------------------------------- 1 | from longclaw.configuration.models import Configuration 2 | 3 | def currency(request): 4 | config = Configuration.for_request(request) 5 | return { 6 | 'currency_html_code': config.currency_html_code, 7 | 'currency': config.currency 8 | } 9 | -------------------------------------------------------------------------------- /longclaw/configuration/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-22 14:48 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Configuration', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('default_shipping_rate', models.DecimalField(decimal_places=2, default=3.95, help_text='The default shipping rate for countries which have not been configured', max_digits=12)), 21 | ('default_shipping_carrier', models.CharField(default='Royal Mail', help_text='The default shipping carrier', max_length=32)), 22 | ('default_shipping_enabled', models.BooleanField(default=False, help_text='Whether to enable default shipping. This essentially means you ship to all countries, not only those with configured shipping rates')), 23 | ('currency_html_code', models.CharField(default='£', help_text='The HTML code for the currency symbol. Used for display purposes only', max_length=12)), 24 | ('currency', models.CharField(default='GBP', help_text='The iso currency code to use for payments', max_length=6)), 25 | ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.Site')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /longclaw/configuration/migrations/0002_configuration_shipping_origin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-22 22:30 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('shipping', '0003_auto_20190322_1429'), 11 | ('configuration', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='configuration', 17 | name='shipping_origin', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='shipping.Address'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /longclaw/configuration/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/configuration/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/configuration/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Admin confiurable settings for longclaw apps 3 | """ 4 | from wagtail.contrib.settings.models import BaseSetting, register_setting 5 | from wagtail.admin.edit_handlers import FieldPanel 6 | from wagtail.snippets.edit_handlers import SnippetChooserPanel 7 | from django.db import models 8 | 9 | from longclaw.shipping.models import Address 10 | 11 | 12 | @register_setting 13 | class Configuration(BaseSetting): 14 | default_shipping_rate = models.DecimalField( 15 | default=3.95, 16 | max_digits=12, 17 | decimal_places=2, 18 | help_text='The default shipping rate for countries which have not been configured' 19 | ) 20 | default_shipping_carrier = models.CharField( 21 | default="Royal Mail", 22 | max_length=32, 23 | help_text='The default shipping carrier' 24 | ) 25 | default_shipping_enabled = models.BooleanField( 26 | default=False, 27 | help_text=('Whether to enable default shipping.' 28 | ' This essentially means you ship to all countries,' 29 | ' not only those with configured shipping rates')) 30 | 31 | shipping_origin = models.ForeignKey(Address, blank=True, null=True, on_delete=models.PROTECT) 32 | 33 | currency_html_code = models.CharField( 34 | max_length=12, 35 | default="£", 36 | help_text="The HTML code for the currency symbol. Used for display purposes only" 37 | ) 38 | currency = models.CharField( 39 | max_length=6, 40 | default="GBP", 41 | help_text="The iso currency code to use for payments" 42 | ) 43 | 44 | panels = ( 45 | FieldPanel('default_shipping_rate'), 46 | FieldPanel('default_shipping_carrier'), 47 | FieldPanel('default_shipping_enabled'), 48 | SnippetChooserPanel('shipping_origin'), 49 | FieldPanel('currency_html_code'), 50 | FieldPanel('currency') 51 | ) 52 | -------------------------------------------------------------------------------- /longclaw/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/contrib/__init__.py -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/contrib/productrequests/__init__.py -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/api.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, permissions, status 2 | from rest_framework.decorators import action 3 | from rest_framework.response import Response 4 | 5 | from longclaw.contrib.productrequests.serializers import ProductRequestSerializer 6 | from longclaw.contrib.productrequests.models import ProductRequest 7 | from longclaw.utils import ProductVariant, maybe_get_product_model 8 | 9 | class ProductRequestViewSet(viewsets.ModelViewSet): 10 | """create/list/get product requests 11 | """ 12 | serializer_class = ProductRequestSerializer 13 | permission_classes = (permissions.AllowAny, ) 14 | queryset = ProductRequest.objects.all() 15 | 16 | def create(self, request): 17 | """Create a new product request 18 | """ 19 | 20 | variant_id = request.data.get("variant_id", None) 21 | if variant_id is not None: 22 | variant = ProductVariant.objects.get(id=variant_id) 23 | product_request = ProductRequest(variant=variant) 24 | product_request.save() 25 | serializer = self.serializer_class(product_request) 26 | response = Response(data=serializer.data, status=status.HTTP_201_CREATED) 27 | else: 28 | response = Response( 29 | {"message": "Missing 'variant_id'"}, 30 | status=status.HTTP_400_BAD_REQUEST) 31 | 32 | return response 33 | 34 | @action(detail=False, methods=['get']) 35 | def requests_for_variant(self, request, variant_id=None): 36 | """Get all the requests for a single variant 37 | """ 38 | requests = ProductRequest.objects.filter(variant__id=variant_id) 39 | serializer = self.serializer_class(requests, many=True) 40 | return Response(data=serializer.data, status=status.HTTP_200_OK) 41 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductrequestsConfig(AppConfig): 5 | name = 'longclaw.productrequests' 6 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-25 17:40 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('testproducts', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ProductRequest', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('created_date', models.DateTimeField(auto_now_add=True)), 21 | ('email', models.EmailField(blank=True, help_text='Optional email of the customer who made the request', max_length=254, null=True)), 22 | ('variant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to='testproducts.ProductVariant')), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/contrib/productrequests/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from longclaw.settings import PRODUCT_VARIANT_MODEL 3 | 4 | class ProductRequest(models.Model): 5 | variant = models.ForeignKey( 6 | PRODUCT_VARIANT_MODEL, related_name='requests', on_delete=models.CASCADE 7 | ) 8 | created_date = models.DateTimeField(auto_now_add=True) 9 | email = models.EmailField( 10 | blank=True, 11 | null=True, 12 | help_text="Optional email of the customer who made the request" 13 | ) 14 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from longclaw.contrib.productrequests.models import ProductRequest 3 | 4 | class ProductRequestSerializer(serializers.ModelSerializer): 5 | 6 | class Meta: 7 | model = ProductRequest() 8 | fields = '__all__' 9 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/templates/productrequests/make_request.html: -------------------------------------------------------------------------------- 1 | {% load longclawcore_tags %} 2 | 5 | {% longclaw_vendors_bundle %} 6 | {% longclaw_client_bundle %} 7 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/templates/productrequests/requests_admin.html: -------------------------------------------------------------------------------- 1 | {% extends "wagtailadmin/base.html" %} 2 | {% load wagtailadmin_tags %} 3 | {% load wagtailcore_tags %} 4 | {% load i18n %} 5 | {% load l10n %} 6 | {% block titletag %}{% blocktrans with title=page.get_admin_display_title page_type=content_type.model_class.get_verbose_name %}Requests For {{ page_type }}: {{ title }}{% endblocktrans %}{% endblock %} 7 | {% block bodyclass %}page-editor {% if page.live %}page-is-live{% endif %} model-{{ content_type.model }} {% if page.locked %}page-locked{% endif %}{% endblock %} 8 | 9 | {% block content %} 10 | {% page_permissions page as page_perms %} 11 |
12 | {% explorer_breadcrumb page %} 13 | 14 |
15 |
16 |

17 | {% blocktrans with title=page.get_admin_display_title page_type=content_type.model_class.get_verbose_name %}Requests For {{ page_type }} {{ title }}{% endblocktrans %}

18 |
19 |
20 | {% trans "Status" %} 21 | {% include "wagtailadmin/shared/page_status_tag.html" with page=page %} 22 | 23 | {% include "wagtailadmin/pages/_privacy_switch.html" with page=page page_perms=page_perms only %} 24 | {% include "wagtailadmin/pages/action_menu/lock_unlock_menu_item.html" %} 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for request in requests %} 38 | 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
Request DateVariantEmail
{{request.created_date|date}}{{request.variant}}{% if request.email %}{{request.email}}{% else %}Not Given{% endif %}
46 | {% endblock %} 47 | {% block extra_css %} 48 | {{ block.super }} 49 | {% include "wagtailadmin/pages/_editor_css.html" %} 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/contrib/productrequests/templatetags/__init__.py -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/templatetags/productrequests_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | @register.inclusion_tag('productrequests/make_request.html') 6 | def make_request_btn(variant_id, btn_class="btn btn-default", btn_text="Request Product"): 7 | '''Button to make a new product request on a variant. 8 | This is a basic button which does not gather the email details for the customer making the request 9 | ''' 10 | return { 11 | 'btn_class': btn_class, 12 | 'variant_id': variant_id, 13 | 'btn_text': btn_text 14 | } 15 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/tests.py: -------------------------------------------------------------------------------- 1 | from longclaw.tests.utils import LongclawTestCase, ProductVariantFactory 2 | from longclaw.contrib.productrequests.models import ProductRequest 3 | from longclaw.contrib.productrequests.templatetags import productrequests_tags 4 | 5 | class ProductRequestTest(LongclawTestCase): 6 | 7 | def setUp(self): 8 | self.variant = ProductVariantFactory() 9 | self.product_request = ProductRequest(variant=self.variant) 10 | 11 | def test_get_request(self): 12 | self.get_test('productrequests_list') 13 | 14 | def test_post_request(self): 15 | self.post_test({'variant_id': self.variant.id}, 'productrequests_list') 16 | 17 | def test_get_variant_requests(self): 18 | self.get_test( 19 | 'productrequests_variant_list', 20 | {'variant_id': self.variant.id} 21 | ) 22 | 23 | def test_make_rquest_btn(self): 24 | result = productrequests_tags.make_request_btn(1) 25 | self.assertIsNotNone(result) 26 | 27 | def test_get_admin(self): 28 | """Check we can retrieve the requests admin page 29 | """ 30 | self.get_test('productrequests_admin', {'pk': self.variant.product.id}) 31 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from longclaw.contrib.productrequests import api, views 3 | from longclaw.settings import API_URL_PREFIX 4 | 5 | request_list = api.ProductRequestViewSet.as_view({ 6 | 'get': 'list', 7 | 'post': 'create' 8 | }) 9 | 10 | request_detail = api.ProductRequestViewSet.as_view({ 11 | 'get': 'retrieve' 12 | }) 13 | 14 | request_variant = api.ProductRequestViewSet.as_view({ 15 | 'get': 'requests_for_variant' 16 | }) 17 | 18 | urlpatterns = [ 19 | url( 20 | API_URL_PREFIX + r'requests/$', 21 | request_list, 22 | name='productrequests_list' 23 | ), 24 | url( 25 | API_URL_PREFIX + r'requests/(?P[0-9]+)/$', 26 | request_detail, 27 | name='productrequests_detail' 28 | ), 29 | url( 30 | API_URL_PREFIX + r'requests/variant/(?P[0-9]+)/$', 31 | request_variant, 32 | name='productrequests_variant_list' 33 | ), 34 | url(r'requests/product/(?P[0-9]+)/$', 35 | views.requests_admin, 36 | name='productrequests_admin') 37 | ] 38 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.http import require_GET 3 | try: 4 | from wagtail.core.models import Page 5 | except ImportError: 6 | from wagtail.wagtailcore.models import Page 7 | from longclaw.utils import ProductVariant 8 | from longclaw.contrib.productrequests.models import ProductRequest 9 | 10 | @require_GET 11 | def requests_admin(request, pk): 12 | """Table display of each request for a given product. 13 | 14 | Allows the given Page pk to refer to a direct parent of 15 | the ProductVariant model or be the ProductVariant model itself. 16 | This allows for the standard longclaw product modelling philosophy where 17 | ProductVariant refers to the actual product (in the case where there is 18 | only 1 variant) or to be variants of the product page. 19 | """ 20 | page = Page.objects.get(pk=pk).specific 21 | if hasattr(page, 'variants'): 22 | requests = ProductRequest.objects.filter( 23 | variant__in=page.variants.all() 24 | ) 25 | else: 26 | requests = ProductRequest.objects.filter(variant=page) 27 | return render( 28 | request, 29 | "productrequests/requests_admin.html", 30 | {'page': page, 'requests': requests} 31 | ) 32 | -------------------------------------------------------------------------------- /longclaw/contrib/productrequests/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.urls import reverse 3 | except ImportError: 4 | from django.core.urlresolvers import reverse 5 | 6 | from wagtail.core import hooks 7 | from wagtail.admin import widgets 8 | from longclaw.utils import ProductVariant 9 | 10 | @hooks.register('register_page_listing_buttons') 11 | def product_requests_button(page, page_perms, is_parent=False): 12 | """Renders a 'requests' button on the page index showing the number 13 | of times the product has been requested. 14 | 15 | Attempts to only show such a button for valid product/variant pages 16 | """ 17 | # Is this page the 'product' model? 18 | # It is generally safe to assume either the page will have a 'variants' 19 | # member or will be an instance of longclaw.utils.ProductVariant 20 | if hasattr(page, 'variants') or isinstance(page, ProductVariant): 21 | yield widgets.PageListingButton( 22 | 'View Requests', 23 | reverse('productrequests_admin', kwargs={'pk': page.id}), 24 | priority=40 25 | ) 26 | -------------------------------------------------------------------------------- /longclaw/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/core/__init__.py -------------------------------------------------------------------------------- /longclaw/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LongclawcoreConfig(AppConfig): 5 | name = 'longclaw.core' 6 | -------------------------------------------------------------------------------- /longclaw/core/jinja2/core/longclaw_script.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /longclaw/core/jinja2tags.py: -------------------------------------------------------------------------------- 1 | import jinja2 2 | import jinja2.nodes 3 | from jinja2.ext import Extension 4 | 5 | from django.template.loader import get_template 6 | 7 | # to keep namespaces from colliding 8 | from .templatetags import longclawcore_tags as lc_tags 9 | 10 | 11 | def longclaw_vendors_bundle(): 12 | template = get_template('core/longclaw_script.html') 13 | 14 | context = lc_tags.longclaw_vendors_bundle() 15 | 16 | return template.render(context=context) 17 | 18 | 19 | def longclaw_client_bundle(): 20 | template = get_template('core/longclaw_script.html') 21 | 22 | context = lc_tags.longclaw_client_bundle() 23 | 24 | return template.render(context=context) 25 | 26 | 27 | class LongClawCoreExtension(Extension): 28 | def __init__(self, environment): 29 | super(LongClawCoreExtension, self).__init__(environment) 30 | 31 | self.environment.globals.update({ 32 | 'longclaw_api_url_prefix': lc_tags.longclaw_api_url_prefix, 33 | 'longclaw_client_bundle': longclaw_client_bundle, 34 | 'longclaw_vendors_bundle': longclaw_vendors_bundle, 35 | }) 36 | 37 | 38 | # Nicer import names 39 | core = LongClawCoreExtension 40 | -------------------------------------------------------------------------------- /longclaw/core/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /longclaw/core/templates/core/script.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | -------------------------------------------------------------------------------- /longclaw/core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/core/templatetags/__init__.py -------------------------------------------------------------------------------- /longclaw/core/templatetags/longclawcore_tags.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django import template 3 | from longclaw import settings 4 | 5 | register = template.Library() 6 | 7 | CLIENT_PATH = os.path.join('core', 'js', 'longclawclient.bundle.js') 8 | VENDORS_PATH = os.path.join('core', 'js', 'vendors.bundle.js') 9 | 10 | @register.inclusion_tag("core/script.html") 11 | def longclaw_vendors_bundle(): 12 | assert os.path.exists( 13 | os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static', VENDORS_PATH) 14 | ) 15 | return {'path': VENDORS_PATH} 16 | 17 | @register.inclusion_tag("core/script.html") 18 | def longclaw_client_bundle(): 19 | assert os.path.exists( 20 | os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static', CLIENT_PATH) 21 | ) 22 | return {'path': CLIENT_PATH} 23 | 24 | @register.simple_tag 25 | def longclaw_api_url_prefix(): 26 | return settings.API_URL_PREFIX 27 | 28 | 29 | -------------------------------------------------------------------------------- /longclaw/core/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.test import TestCase 3 | from django.contrib.staticfiles import finders 4 | 5 | from longclaw import settings 6 | from longclaw.core.templatetags import longclawcore_tags 7 | 8 | class TagTests(TestCase): 9 | 10 | def _test_static_file(self, pth): 11 | result = finders.find(pth) 12 | print(result) 13 | self.assertTrue(result) 14 | 15 | def test_vendors_bundle(self): 16 | ctx = longclawcore_tags.longclaw_vendors_bundle() 17 | print(ctx) 18 | self._test_static_file(ctx['path']) 19 | 20 | def test_client_bundle(self): 21 | ctx = longclawcore_tags.longclaw_client_bundle() 22 | self._test_static_file(ctx['path']) 23 | 24 | def test_api_url_prefix(self): 25 | self.assertEqual( 26 | settings.API_URL_PREFIX, 27 | longclawcore_tags.longclaw_api_url_prefix() 28 | ) 29 | -------------------------------------------------------------------------------- /longclaw/orders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/orders/__init__.py -------------------------------------------------------------------------------- /longclaw/orders/api.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import action 2 | from rest_framework import permissions, status, viewsets 3 | from rest_framework.response import Response 4 | from longclaw.orders.models import Order 5 | from longclaw.orders.serializers import OrderSerializer 6 | 7 | 8 | class OrderViewSet(viewsets.ModelViewSet): 9 | serializer_class = OrderSerializer 10 | permission_classes = [permissions.IsAdminUser] 11 | queryset = Order.objects.all() 12 | 13 | @action(detail=True, methods=['post']) 14 | def refund_order(self, request, pk): 15 | """Refund the order specified by the pk 16 | """ 17 | order = Order.objects.get(id=pk) 18 | order.refund() 19 | return Response(status=status.HTTP_204_NO_CONTENT) 20 | 21 | @action(detail=True, methods=['post']) 22 | def fulfill_order(self, request, pk): 23 | """Mark the order specified by pk as fulfilled 24 | """ 25 | order = Order.objects.get(id=pk) 26 | order.fulfill() 27 | return Response(status=status.HTTP_204_NO_CONTENT) 28 | -------------------------------------------------------------------------------- /longclaw/orders/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LongclawOrdersConfig(AppConfig): 5 | name = 'longclaw.orders' 6 | -------------------------------------------------------------------------------- /longclaw/orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-22 14:48 2 | 3 | from django.db import migrations, models 4 | from django.conf import settings 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | (settings.PRODUCT_VARIANT_MODEL.split(".")[0], '__first__'), 14 | ('shipping', '__first__'), 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 | ('payment_date', models.DateTimeField(blank=True, null=True)), 23 | ('created_date', models.DateTimeField(auto_now_add=True)), 24 | ('status', models.IntegerField(choices=[(1, 'Submitted'), (2, 'Fulfilled'), (3, 'Cancelled'), (4, 'Refunded'), (5, 'Payment Failed')], default=1)), 25 | ('status_note', models.CharField(blank=True, max_length=128, null=True)), 26 | ('transaction_id', models.CharField(blank=True, max_length=256, null=True)), 27 | ('email', models.EmailField(blank=True, max_length=128, null=True)), 28 | ('ip_address', models.GenericIPAddressField(blank=True, null=True)), 29 | ('shipping_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), 30 | ('billing_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='orders_billing_address', to='shipping.Address')), 31 | ('shipping_address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='orders_shipping_address', to='shipping.Address')), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name='OrderItem', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('quantity', models.IntegerField(default=1)), 39 | ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.Order')), 40 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to=settings.PRODUCT_VARIANT_MODEL)), 41 | ], 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /longclaw/orders/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/orders/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/orders/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from longclaw.orders.models import Order, OrderItem 3 | from longclaw.products.serializers import ProductVariantSerializer 4 | from longclaw.shipping.serializers import AddressSerializer 5 | 6 | class OrderItemSerializer(serializers.ModelSerializer): 7 | 8 | product = ProductVariantSerializer() 9 | 10 | class Meta: 11 | model = OrderItem 12 | fields = "__all__" 13 | 14 | 15 | class OrderSerializer(serializers.ModelSerializer): 16 | 17 | items = OrderItemSerializer(many=True) 18 | shipping_address = AddressSerializer() 19 | total = serializers.SerializerMethodField() 20 | 21 | class Meta: 22 | model = Order 23 | fields = "__all__" 24 | 25 | def get_total(self, obj): 26 | return obj.total 27 | -------------------------------------------------------------------------------- /longclaw/orders/templates/orders_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "modeladmin/inspect.html" %} 2 | {% load i18n static %} 3 | 4 | {% block content_main %} 5 | 6 | 12 | 13 | 14 | {% endblock %} 15 | 16 | {% block footer %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /longclaw/orders/tests.py: -------------------------------------------------------------------------------- 1 | import mock 2 | from django.test import TestCase 3 | from django.contrib.auth.models import User 4 | try: 5 | from django.urls import reverse_lazy 6 | except ImportError: 7 | from django.core.urlresolvers import reverse_lazy 8 | from django.contrib.auth.models import User 9 | from wagtail.tests.utils import WagtailTestUtils 10 | from longclaw.tests.utils import LongclawTestCase, OrderFactory 11 | from longclaw.orders.wagtail_hooks import OrderModelAdmin 12 | 13 | class OrderTests(LongclawTestCase): 14 | 15 | def setUp(self): 16 | self.order = OrderFactory(transaction_id="FAKE") 17 | admin = User.objects.create_superuser('admn', 'myemail@test.com', 'password') 18 | self.client.force_authenticate(user=admin) 19 | 20 | def test_fulfill_order(self): 21 | self.post_test({}, 'longclaw_fulfill_order', urlkwargs={'pk': self.order.id}) 22 | self.order.refresh_from_db() 23 | self.assertEqual(self.order.status, self.order.FULFILLED) 24 | 25 | def test_total(self): 26 | self.assertEqual(self.order.total, 0) 27 | 28 | def test_total_items(self): 29 | self.assertEqual(self.order.total_items, 0) 30 | 31 | def test_refund_order(self): 32 | self.post_test({}, 'longclaw_refund_order', urlkwargs={'pk': self.order.id}) 33 | self.order.refresh_from_db() 34 | self.assertEqual(self.order.status, self.order.REFUNDED) 35 | 36 | def test_cancel_order(self): 37 | self.order.cancel() 38 | self.order.refresh_from_db() 39 | self.assertEqual(self.order.status, self.order.CANCELLED) 40 | 41 | 42 | class TestOrderView(LongclawTestCase, WagtailTestUtils): 43 | 44 | def setUp(self): 45 | self.login() 46 | self.model_admin = OrderModelAdmin() 47 | 48 | def test_order_index_view(self): 49 | """ 50 | Test the index view 51 | """ 52 | name = self.model_admin.url_helper.get_action_url_name('index') 53 | response = self.client.get(reverse_lazy(name)) 54 | self.assertEqual(response.status_code, 200) 55 | 56 | def test_order_detail_view(self): 57 | order = OrderFactory() 58 | name = self.model_admin.url_helper.get_action_url_name('detail') 59 | response = self.client.get(reverse_lazy(name, kwargs={'instance_pk': order.pk})) 60 | self.assertEqual(response.status_code, 200) 61 | -------------------------------------------------------------------------------- /longclaw/orders/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from longclaw.orders import api 3 | 4 | from longclaw.settings import API_URL_PREFIX 5 | 6 | orders = api.OrderViewSet.as_view({ 7 | 'get': 'retrieve' 8 | }) 9 | 10 | fulfill_order = api.OrderViewSet.as_view({ 11 | 'post': 'fulfill_order' 12 | }) 13 | 14 | refund_order = api.OrderViewSet.as_view({ 15 | 'post': 'refund_order' 16 | }) 17 | 18 | PREFIX = r'^{}order/'.format(API_URL_PREFIX) 19 | urlpatterns = [ 20 | url( 21 | PREFIX + r'(?P[0-9]+)/$', 22 | orders, 23 | name='longclaw_orders' 24 | ), 25 | 26 | url( 27 | PREFIX + r'(?P[0-9]+)/fulfill/$', 28 | fulfill_order, 29 | name='longclaw_fulfill_order' 30 | ), 31 | 32 | url( 33 | PREFIX + r'(?P[0-9]+)/refund/$', 34 | refund_order, 35 | name='longclaw_refund_order' 36 | ) 37 | ] 38 | -------------------------------------------------------------------------------- /longclaw/products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/products/__init__.py -------------------------------------------------------------------------------- /longclaw/products/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/products/admin.py -------------------------------------------------------------------------------- /longclaw/products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class LongclawProductsConfig(AppConfig): 5 | name = 'longclaw.products' 6 | -------------------------------------------------------------------------------- /longclaw/products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-22 14:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ] 10 | 11 | operations = [ 12 | ] 13 | -------------------------------------------------------------------------------- /longclaw/products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/products/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from wagtail.core.models import Page 3 | 4 | 5 | # Abstract base classes a user can use to implement their own product system 6 | class ProductBase(Page): 7 | """Base classes for ``Product`` implementations. All this provides are 8 | a few helper methods for ``ProductVariant``'s. It assumes that ``ProductVariant``'s 9 | have a ``related_name`` of ``variants`` 10 | """ 11 | 12 | class Meta: 13 | abstract = True 14 | 15 | def __str__(self): 16 | return self.title 17 | 18 | @property 19 | def price_range(self): 20 | """ Calculate the price range of the products variants 21 | """ 22 | ordered = self.variants.order_by('base_price') 23 | if ordered: 24 | return ordered.first().price, ordered.last().price 25 | else: 26 | return None, None 27 | 28 | @property 29 | def in_stock(self): 30 | """ Returns True if any of the product variants are in stock 31 | """ 32 | return any(self.variants.filter(stock__gt=0)) 33 | 34 | 35 | class ProductVariantBase(models.Model): 36 | """ 37 | Base model for creating product variants 38 | """ 39 | base_price = models.DecimalField(max_digits=12, decimal_places=2) 40 | ref = models.CharField(max_length=32) 41 | stock = models.IntegerField(default=0) 42 | 43 | class Meta: 44 | abstract = True 45 | 46 | def __str__(self): 47 | try: 48 | return "{} - {}".format(self.product.title, self.ref) 49 | except AttributeError: 50 | return self.ref 51 | 52 | @property 53 | def price(self): 54 | """Can be overridden in concrete implementations in 55 | order to generate the price dynamically. 56 | 57 | Override the property like so: 58 | 59 | @ProductVariantBase.price.getter 60 | def price(self): 61 | ... 62 | 63 | """ 64 | return self.base_price 65 | 66 | def get_product_title(self): 67 | """Retrieve the title of the related product. 68 | If no related product, just return the ``ref`` of this model 69 | """ 70 | try: 71 | return self.product.title 72 | except AttributeError: 73 | return self.ref 74 | -------------------------------------------------------------------------------- /longclaw/products/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from longclaw.utils import ProductVariant, maybe_get_product_model 3 | 4 | class ProductSerializer(serializers.ModelSerializer): 5 | 6 | class Meta: 7 | model = maybe_get_product_model() 8 | fields = "__all__" 9 | 10 | 11 | class ProductVariantSerializer(serializers.ModelSerializer): 12 | 13 | product = ProductSerializer() 14 | 15 | class Meta: 16 | model = ProductVariant 17 | fields = "__all__" 18 | -------------------------------------------------------------------------------- /longclaw/products/tests.py: -------------------------------------------------------------------------------- 1 | from wagtail.tests.utils import WagtailPageTests 2 | from longclaw.utils import maybe_get_product_model 3 | from longclaw.tests.testproducts.models import ProductIndex 4 | from longclaw.tests.utils import ProductVariantFactory 5 | from longclaw.products.serializers import ProductVariantSerializer 6 | 7 | class TestProducts(WagtailPageTests): 8 | 9 | def setUp(self): 10 | self.product_model = maybe_get_product_model() 11 | 12 | def test_can_create_product(self): 13 | self.assertCanCreateAt(ProductIndex, self.product_model) 14 | 15 | def test_variant_price(self): 16 | variant = ProductVariantFactory() 17 | self.assertTrue(variant.price == variant.base_price * 10) 18 | self.assertTrue(variant.price > 0) 19 | 20 | def test_price_range(self): 21 | variant = ProductVariantFactory() 22 | prices = variant.product.price_range 23 | self.assertTrue(prices[0] == prices[1]) 24 | 25 | def test_stock(self): 26 | variant = ProductVariantFactory() 27 | variant.stock = 1 28 | variant.save() 29 | self.assertTrue(variant.product.in_stock) 30 | 31 | def test_out_of_stock(self): 32 | variant = ProductVariantFactory() 33 | variant.stock = 0 34 | variant.save() 35 | self.assertFalse(variant.product.in_stock) 36 | 37 | def test_variant_serializer(self): 38 | variant = ProductVariantFactory() 39 | serializer = ProductVariantSerializer(variant) 40 | self.assertIn('product', serializer.data) 41 | 42 | def test_product_title(self): 43 | variant = ProductVariantFactory() 44 | self.assertEqual(variant.get_product_title(), variant.product.title) 45 | -------------------------------------------------------------------------------- /longclaw/project_template/catalog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/project_template/catalog/__init__.py -------------------------------------------------------------------------------- /longclaw/project_template/catalog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_extensions.db.fields import AutoSlugField 3 | from modelcluster.fields import ParentalKey 4 | from wagtail.core.models import Page, Orderable 5 | from wagtail.core.fields import RichTextField 6 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel 7 | from wagtail.images.edit_handlers import ImageChooserPanel 8 | from longclaw.products.models import ProductVariantBase, ProductBase 9 | 10 | class ProductIndex(Page): 11 | """Index page for all products 12 | """ 13 | subpage_types = ('catalog.Product', 'catalog.ProductIndex') 14 | 15 | 16 | class Product(ProductBase): 17 | parent_page_types = ['catalog.ProductIndex'] 18 | description = RichTextField() 19 | content_panels = ProductBase.content_panels + [ 20 | FieldPanel('description'), 21 | InlinePanel('images', label='Images'), 22 | InlinePanel('variants', label='Product variants'), 23 | 24 | ] 25 | 26 | @property 27 | def first_image(self): 28 | return self.images.first() 29 | 30 | 31 | class ProductVariant(ProductVariantBase): 32 | """Represents a 'variant' of a product 33 | """ 34 | # You *could* do away with the 'Product' concept entirely - e.g. if you only 35 | # want to support 1 'variant' per 'product'. 36 | product = ParentalKey(Product, related_name='variants') 37 | 38 | slug = AutoSlugField( 39 | separator='', 40 | populate_from=('product', 'ref'), 41 | ) 42 | 43 | # Enter your custom product variant fields here 44 | # e.g. colour, size, stock and so on. 45 | # Remember, ProductVariantBase provides 'price', 'ref' and 'stock' fields 46 | description = RichTextField() 47 | 48 | 49 | class ProductImage(Orderable): 50 | """Example of adding images related to a product model 51 | """ 52 | product = ParentalKey(Product, related_name='images') 53 | image = models.ForeignKey('wagtailimages.Image', on_delete=models.CASCADE, related_name='+') 54 | caption = models.CharField(blank=True, max_length=255) 55 | 56 | panels = [ 57 | ImageChooserPanel('image'), 58 | FieldPanel('caption') 59 | ] 60 | -------------------------------------------------------------------------------- /longclaw/project_template/catalog/templates/catalog/product.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} load wagtailcore_tags wagtailimages_tags {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block body_class {% templatetag closeblock %} template-productpage {% templatetag openblock %}endblock{% templatetag closeblock %} 6 | 7 | {% templatetag openblock %} block content {% templatetag closeblock %} 8 |
9 |
10 | {% templatetag openblock %} if page.images {% templatetag closeblock %} 11 | {% templatetag openblock %} for item in page.images.all {% templatetag closeblock %} 12 |
13 | {% templatetag openblock %} image item.image fill-320x240 {% templatetag closeblock %} 14 |

{% templatetag openvariable %} item.caption {% templatetag closevariable %}

15 |
16 | {% templatetag openblock %} endfor {% templatetag closeblock %} 17 | {% templatetag openblock %} else {% templatetag closeblock %} 18 | 19 | {% templatetag openblock %} endif {% templatetag closeblock %} 20 |
21 | 22 | 26 |
27 | {% templatetag openblock %} if page.tags.all.count {% templatetag closeblock %} 28 |
29 |

Tags

30 | {% templatetag openblock %} for tag in page.tags.all {% templatetag closeblock %} 31 | 32 | {% templatetag openblock %} endfor {% templatetag closeblock %} 33 |
34 | {% templatetag openblock %} endif {% templatetag closeblock %} 35 | 36 |

Return

37 | 38 | {% templatetag openblock %} endblock {% templatetag closeblock %} 39 | -------------------------------------------------------------------------------- /longclaw/project_template/catalog/templates/catalog/product_index.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} load wagtailcore_tags wagtailimages_tags {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block body_class {% templatetag closeblock %}template-productindex{% templatetag openblock %} endblock {% templatetag closeblock %} 6 | 7 | {% templatetag openblock %} block content {% templatetag closeblock %} 8 |

{% templatetag openvariable %} page.title {% templatetag closevariable %}

9 |
10 | {% templatetag openblock %} for post in page.get_children {% templatetag closeblock %} 11 | {% templatetag openblock %} with post=post.specific {% templatetag closeblock %} 12 |
13 | {% templatetag openblock %} image post.images.first.image max-400x320 {% templatetag closeblock %} 14 |
15 | 16 |

{% templatetag openvariable %} post.title {% templatetag closevariable %}

17 |
18 |

{% templatetag openvariable %} post.description|richtext {% templatetag closevariable %}

19 |

From €{% templatetag openvariable %} post.price_range.0 {% templatetag closevariable %}

20 |
21 |
22 | {% templatetag openblock %} endwith {% templatetag closeblock %} 23 | {% templatetag openblock %} endfor {% templatetag closeblock %} 24 |
25 | {% templatetag openblock %} endblock {% templatetag closeblock %} 26 | -------------------------------------------------------------------------------- /longclaw/project_template/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/project_template/home/__init__.py -------------------------------------------------------------------------------- /longclaw/project_template/home/models.py: -------------------------------------------------------------------------------- 1 | from wagtail.core.models import Page 2 | 3 | 4 | class HomePage(Page): 5 | pass 6 | -------------------------------------------------------------------------------- /longclaw/project_template/home/templates/home/home_page.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} block body_class {% templatetag closeblock %}template-homepage{% templatetag openblock %} endblock {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block content {% templatetag closeblock %} 6 |

Welcome to your new Longclaw site!

7 | 8 |

You can access the admin interface here (make sure you have run "./manage.py createsuperuser" in the console first). 9 | 10 | {% templatetag openblock %} endblock {% templatetag closeblock %} 11 | -------------------------------------------------------------------------------- /longclaw/project_template/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/project_template/project_name/__init__.py -------------------------------------------------------------------------------- /longclaw/project_template/project_name/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/project_template/project_name/settings/__init__.py -------------------------------------------------------------------------------- /longclaw/project_template/project_name/settings/dev.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | # SECURITY WARNING: don't run with debug turned on in production! 4 | DEBUG = True 5 | 6 | # SECURITY WARNING: keep the secret key used in production secret! 7 | SECRET_KEY = '{{ secret_key }}' 8 | 9 | 10 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 11 | 12 | 13 | try: 14 | from .local import * 15 | except ImportError: 16 | pass 17 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = False 4 | 5 | try: 6 | from .local import * 7 | except ImportError: 8 | pass 9 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/static/css/project_name.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --brand-primary: #b14344; 3 | --border-radius-default: 5px; 4 | --header-height: 70px; 5 | } 6 | 7 | 8 | body { 9 | margin: 0; 10 | } 11 | 12 | .layout { 13 | display: grid; 14 | grid-template-columns: auto; 15 | grid-template-rows: var(--header-height) 1fr auto; 16 | grid-template-areas: "header" 17 | "main" 18 | "footer"; 19 | min-height: 100vh; 20 | 21 | } 22 | 23 | .header { 24 | grid-area: header; 25 | padding: 5px; 26 | background-color: var(--brand-primary) 27 | } 28 | 29 | .main { 30 | grid-area: main; 31 | margin: 1em; 32 | } 33 | 34 | 35 | 36 | .card-grid { 37 | display: grid; 38 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 39 | } 40 | 41 | .card { 42 | transition: 0.3s; 43 | border-radius: var(--border-radius-default); 44 | max-width: 100%; 45 | min-width: 20%; 46 | min-height: 100px; 47 | margin: 5px; 48 | box-shadow: 0 0 6px 0 rgba(0,0,0,0.2), 0 2px 0px 0 var(--brand-primary); 49 | } 50 | 51 | .card-body { 52 | padding: 1rem 2rem; 53 | } 54 | 55 | .card > img { 56 | border-style: none; 57 | border-radius: var(--border-radius-default) var(--border-radius-default) 0 0; 58 | vertical-align: middle; 59 | width: 100%; 60 | max-height: 300px; 61 | min-height: 50px; 62 | object-fit: cover; 63 | } 64 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/static/js/project_name.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/project_template/project_name/static/js/project_name.js -------------------------------------------------------------------------------- /longclaw/project_template/project_name/templates/404.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} block body_class {% templatetag closeblock %}template-404{% templatetag openblock %} endblock {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block content {% templatetag closeblock %} 6 |

Page not found

7 | 8 |

Sorry, this page could not be found.

9 | {% templatetag openblock %} endblock {% templatetag closeblock %} 10 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Internal server error 10 | 11 | 12 | 13 |

Internal server error

14 | 15 |

Sorry, there seems to be an error. Please try again soon.

16 | 17 | 18 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/templates/base.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} load static wagtailuserbar {% templatetag closeblock %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% templatetag openblock %} block title %}{% templatetag openblock %} if self.seo_title %}{% templatetag openvariable %} self.seo_title {% templatetag closevariable %}{% templatetag openblock %} else %}{% templatetag openvariable %} self.title {% templatetag closevariable %}{% templatetag openblock %} endif {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %}{% templatetag openblock %} block title_suffix {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 12 | 13 | 14 | 15 | {% templatetag opencomment %} Global stylesheets {% templatetag closecomment %} 16 | 17 | 18 | {% templatetag openblock %} block extra_css {% templatetag closeblock %} 19 | {% templatetag opencomment %} Override this in templates to add extra stylesheets {% templatetag closecomment %} 20 | {% templatetag openblock %} endblock {% templatetag closeblock %} 21 | 22 | 23 | 24 |
25 | {% templatetag openblock %} wagtailuserbar {% templatetag closeblock %} 26 |
27 | 28 |
29 | {% templatetag openblock %} block content {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 30 |
31 |
32 | {% templatetag opencomment %} Global javascript {% templatetag closecomment %} 33 | 34 | 35 | {% templatetag openblock %} block extra_js {% templatetag closeblock %} 36 | {% templatetag opencomment %} Override this in templates to add extra javascript {% templatetag closecomment %} 37 | {% templatetag openblock %} endblock {% templatetag closeblock %} 38 | 39 | 40 | -------------------------------------------------------------------------------- /longclaw/project_template/project_name/templates/checkout/checkout.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load longclawcheckout_tags longclawcore_tags {% templatetag closeblock %} 3 | 4 | {% templatetag openblock %} block content {% templatetag closeblock %} 5 | {% templatetag opencomment %} 6 | `checkout_form`, `shipping_form`, `billing_form`, `basket` and `total_price` are the context 7 | variables available for you to build up your checkout page. 8 | `checkout_form` includes the `different_billing_address` checkbox which you can use to 9 | decide whether to display `billing_form` or not (javascript needed here!). 10 | `checkout_form` also includes the `shipping_option` dropdown which should be initialized 11 | using the `initShippingOption` javascript function. 12 | The aforementioned fields are optional (different_billing_address and shipping_option); if you dont 13 | offer shipping/it is fixed rate you can prevent these fields from being displayed. 14 | The only required field in `checkout_form` is `email`. 15 | 16 | `shipping_form` gathers the address. `billing_form` is the same (but intended to gather a billing address). 17 | `billing_form` is optional; you may not require it, or a gateway integration dropin may gather it instead. 18 | 19 | `basket` is a queryset of `BasketItem` for the current customer. 20 | `total_price` is the total cost of all items in the basket. 21 | {% templatetag closecomment %} 22 | {% templatetag openblock %} endblock content {% templatetag closeblock %} 23 | 24 | {% templatetag openblock %} block extra_js {% templatetag closeblock %} 25 | 26 | {% templatetag opencomment %} 27 | Load any client javascript provided by the payment gateway. 28 | This will give a list of 17 | 18 | -------------------------------------------------------------------------------- /longclaw/stats/templates/stats/summary_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{ total|safe }} {{ text }} 4 | 5 |
  • -------------------------------------------------------------------------------- /longclaw/stats/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from django.test import TestCase 3 | from django.utils import timezone 4 | 5 | from longclaw.stats import stats 6 | from longclaw.tests.utils import OrderFactory 7 | 8 | class StatsTest(TestCase): 9 | 10 | def setUp(self): 11 | order = OrderFactory() 12 | order.payment_date = timezone.now() 13 | order.save() 14 | 15 | def test_current_month(self): 16 | start, end = stats.current_month() 17 | self.assertEqual(start.month, end.month) 18 | self.assertEqual(start.day, 1) 19 | self.assertIn(end.day, [28, 29, 30, 31]) 20 | 21 | def test_sales_for_time_period(self): 22 | delta = timedelta(days=1) 23 | sales = stats.sales_for_time_period(datetime.now() - delta, datetime.now() + delta) 24 | self.assertEqual(sales.count(), 1) 25 | 26 | def test_daily_sales(self): 27 | delta = timedelta(days=10) 28 | groups = stats.daily_sales(datetime.now() - delta, datetime.now() + delta) 29 | # We only create 1 order. 30 | self.assertEqual(len(list(groups)), 1) 31 | -------------------------------------------------------------------------------- /longclaw/stats/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from wagtail.core import hooks 3 | from wagtail.admin.site_summary import SummaryItem 4 | from longclaw.orders.models import Order 5 | from longclaw.stats import stats 6 | from longclaw.configuration.models import Configuration 7 | from longclaw.utils import ProductVariant, maybe_get_product_model 8 | 9 | 10 | class LongclawSummaryItem(SummaryItem): 11 | order = 10 12 | template = 'stats/summary_item.html' 13 | 14 | def get_context(self): 15 | return { 16 | 'total': 0, 17 | 'text': '', 18 | 'url': '', 19 | 'icon': 'icon-doc-empty-inverse' 20 | } 21 | 22 | class OutstandingOrders(LongclawSummaryItem): 23 | order = 10 24 | def get_context(self): 25 | orders = Order.objects.filter(status=Order.SUBMITTED) 26 | return { 27 | 'total': orders.count(), 28 | 'text': 'Outstanding Orders', 29 | 'url': '/admin/orders/order/', 30 | 'icon': 'icon-warning' 31 | } 32 | 33 | class ProductCount(LongclawSummaryItem): 34 | order = 20 35 | def get_context(self): 36 | product_model = maybe_get_product_model() 37 | if product_model: 38 | count = product_model.objects.all().count() 39 | else: 40 | count = ProductVariant.objects.all().count() 41 | return { 42 | 'total': count, 43 | 'text': 'Product', 44 | 'url': '', 45 | 'icon': 'icon-list-ul' 46 | } 47 | 48 | class MonthlySales(LongclawSummaryItem): 49 | order = 30 50 | def get_context(self): 51 | settings = Configuration.for_request(self.request) 52 | sales = stats.sales_for_time_period(*stats.current_month()) 53 | return { 54 | 'total': "{}{}".format(settings.currency_html_code, 55 | sum(order.total for order in sales)), 56 | 'text': 'In sales this month', 57 | 'url': '/admin/orders/order/', 58 | 'icon': 'icon-tick' 59 | } 60 | 61 | class LongclawStatsPanel(SummaryItem): 62 | order = 110 63 | template = 'stats/stats_panel.html' 64 | def get_context(self): 65 | month_start, month_end = stats.current_month() 66 | daily_sales = stats.daily_sales(month_start, month_end) 67 | labels = [(month_start + datetime.timedelta(days=x)).strftime('%Y-%m-%d') 68 | for x in range(0, datetime.datetime.now().day)] 69 | daily_income = [0] * len(labels) 70 | for k, order_group in daily_sales: 71 | i = labels.index(k) 72 | daily_income[i] = float(sum(order.total for order in order_group)) 73 | 74 | popular_products = stats.sales_by_product(month_start, month_end)[:5] 75 | return { 76 | "daily_income": daily_income, 77 | "labels": labels, 78 | "product_labels": list(popular_products.values_list('title', flat=True)), 79 | "sales_volume": list(popular_products.values_list('quantity', flat=True)) 80 | } 81 | 82 | 83 | 84 | 85 | @hooks.register('construct_homepage_summary_items') 86 | def add_longclaw_summary_items(request, items): 87 | 88 | # We are going to replace everything with our own items 89 | items[:] = [] 90 | items.extend([ 91 | OutstandingOrders(request), 92 | ProductCount(request), 93 | MonthlySales(request) 94 | ]) 95 | 96 | @hooks.register('construct_homepage_panels') 97 | def add_stats_panel(request, panels): 98 | return panels.append(LongclawStatsPanel(request)) 99 | -------------------------------------------------------------------------------- /longclaw/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals, absolute_import 3 | import os 4 | import django 5 | 6 | DEBUG = True 7 | USE_TZ = True 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 'kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk' 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | 'NAME': ':memory:', 16 | } 17 | } 18 | 19 | ROOT_URLCONF = 'longclaw.tests.urls' 20 | 21 | INSTALLED_APPS = [ 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sites', 26 | 'django.contrib.sessions', 27 | 'django.contrib.messages', 28 | 'django.contrib.staticfiles', 29 | 30 | 'wagtail.contrib.forms', 31 | 'wagtail.contrib.redirects', 32 | 'wagtail.embeds', 33 | 'wagtail.sites', 34 | 'wagtail.users', 35 | 'wagtail.snippets', 36 | 'wagtail.documents', 37 | 'wagtail.images', 38 | 'wagtail.search', 39 | 'wagtail.admin', 40 | 'wagtail.core', 41 | 'wagtail.contrib.modeladmin', 42 | 'wagtail.contrib.settings', 43 | 44 | 'modelcluster', 45 | 'taggit', 46 | 'rest_framework', 47 | 'django_extensions', 48 | 49 | 'longclaw.core', 50 | 'longclaw.configuration', 51 | 'longclaw.shipping', 52 | 'longclaw.products', 53 | 'longclaw.orders', 54 | 'longclaw.checkout', 55 | 'longclaw.basket', 56 | 'longclaw.stats', 57 | 'longclaw.contrib.productrequests', 58 | 'longclaw.tests.testproducts', 59 | 'longclaw.tests.trivialrates', 60 | ] 61 | 62 | SITE_ID = 1 63 | 64 | MIDDLEWARE = [ 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'django.middleware.common.CommonMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'wagtail.contrib.legacy.sitemiddleware.SiteMiddleware', 70 | 'wagtail.contrib.redirects.middleware.RedirectMiddleware', 71 | ] 72 | 73 | TEMPLATES = [ 74 | { 75 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 76 | 'DIRS': [ 77 | os.path.join(os.path.dirname(__file__), 'templates'), 78 | ], 79 | 'APP_DIRS': True, 80 | 'OPTIONS': { 81 | 'context_processors': [ 82 | 'django.template.context_processors.debug', 83 | 'django.template.context_processors.request', 84 | 'django.contrib.auth.context_processors.auth', 85 | 'django.contrib.messages.context_processors.messages', 86 | 'longclaw.configuration.context_processors.currency', 87 | ], 88 | }, 89 | }, 90 | ] 91 | 92 | if django.VERSION >= (1, 10): 93 | MIDDLEWARE = MIDDLEWARE 94 | else: 95 | MIDDLEWARE_CLASSES = MIDDLEWARE 96 | 97 | STATIC_URL = '/static/' 98 | 99 | PRODUCT_VARIANT_MODEL = 'testproducts.ProductVariant' 100 | -------------------------------------------------------------------------------- /longclaw/tests/templates/checkout/success.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/templates/checkout/success.html -------------------------------------------------------------------------------- /longclaw/tests/testproducts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/testproducts/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/testproducts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-22 14:49 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import modelcluster.fields 6 | import wagtail.core.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Product', 20 | fields=[ 21 | ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), 22 | ('description', wagtail.core.fields.RichTextField()), 23 | ], 24 | options={ 25 | 'abstract': False, 26 | }, 27 | bases=('wagtailcore.page',), 28 | ), 29 | migrations.CreateModel( 30 | name='ProductIndex', 31 | fields=[ 32 | ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), 33 | ], 34 | options={ 35 | 'abstract': False, 36 | }, 37 | bases=('wagtailcore.page',), 38 | ), 39 | migrations.CreateModel( 40 | name='ProductVariant', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('base_price', models.DecimalField(decimal_places=2, max_digits=12)), 44 | ('ref', models.CharField(max_length=32)), 45 | ('stock', models.IntegerField(default=0)), 46 | ('description', wagtail.core.fields.RichTextField()), 47 | ('product', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='testproducts.Product')), 48 | ], 49 | options={ 50 | 'abstract': False, 51 | }, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /longclaw/tests/testproducts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/testproducts/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/testproducts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from modelcluster.fields import ParentalKey 3 | from wagtail.core.models import Page 4 | from wagtail.core.fields import RichTextField 5 | from wagtail.admin.edit_handlers import FieldPanel, InlinePanel 6 | from longclaw.products.models import ProductVariantBase, ProductBase 7 | 8 | class ProductIndex(Page): 9 | """Index page for all products 10 | """ 11 | subpage_types = ('Product', 'ProductIndex') 12 | 13 | 14 | class Product(ProductBase): 15 | parent_page_types = [ProductIndex] 16 | description = RichTextField() 17 | content_panels = ProductBase.content_panels + [ 18 | FieldPanel('description'), 19 | InlinePanel('variants') 20 | ] 21 | 22 | 23 | class ProductVariant(ProductVariantBase): 24 | """Basic product variant for testing 25 | """ 26 | product = ParentalKey(Product, related_name='variants') 27 | description = RichTextField() 28 | 29 | @ProductVariantBase.price.getter 30 | def price(self): 31 | """Make the price dynamic to check that longclaw works with ``get_price`` 32 | """ 33 | return self.base_price * 10 34 | -------------------------------------------------------------------------------- /longclaw/tests/trivialrates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/trivialrates/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/trivialrates/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-23 17:15 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('shipping', '0003_auto_20190322_1429'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='TrivialShippingRateProcessor', 18 | fields=[ 19 | ('shippingrateprocessor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='shipping.ShippingRateProcessor')), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | 'base_manager_name': 'objects', 24 | }, 25 | bases=('shipping.shippingrateprocessor',), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /longclaw/tests/trivialrates/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/longclaw/tests/trivialrates/migrations/__init__.py -------------------------------------------------------------------------------- /longclaw/tests/trivialrates/models.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from django.utils.encoding import force_bytes, force_text 4 | from longclaw.shipping.models import ShippingRateProcessor, ShippingRate 5 | from longclaw.basket.models import BasketItem 6 | 7 | 8 | class TrivialShippingRateProcessor(ShippingRateProcessor): 9 | def process_rates(self, **kwargs): 10 | destination = kwargs['destination'] 11 | basket_id = kwargs['basket_id'] 12 | 13 | item_count = BasketItem.objects.filter(basket_id=basket_id).count() 14 | 15 | rates = [] 16 | 17 | quotes = [] 18 | 19 | if 0 < item_count: 20 | quotes.append((item_count * 2, 'turtle')) 21 | 22 | if 1 < item_count: 23 | quotes.append((item_count * 4, 'rabbit')) 24 | 25 | if 2 < item_count: 26 | quotes.append((item_count * 16, 'cheetah')) 27 | 28 | for amount, speed in quotes: 29 | name = self.get_processed_rate_name(destination, basket_id, speed) 30 | lookups = dict(name=name) 31 | values = dict( 32 | rate=amount, 33 | carrier='TrivialShippingRateProcessor', 34 | description='Delivered with {} speed'.format(speed), 35 | basket_id=basket_id, 36 | destination=destination, 37 | processor=self, 38 | ) 39 | 40 | rate = ShippingRate.objects.update_or_create(defaults=values, **lookups) 41 | rates.append(rate) 42 | 43 | return rates 44 | 45 | def get_processed_rate_name(self, destination, basket_id, speed): 46 | name_long = 'TrivialShippingRateProcessor-{}-{}-{}'.format(destination.pk, basket_id, speed) 47 | name = hashlib.md5(force_bytes(name_long)).hexdigest() 48 | return force_text(name) 49 | -------------------------------------------------------------------------------- /longclaw/tests/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from __future__ import unicode_literals, absolute_import 3 | 4 | from django.conf.urls import url, include 5 | 6 | from wagtail.admin import urls as admin_urls 7 | from wagtail.core import urls as wagtail_urls 8 | from wagtail.documents import urls as documents_urls 9 | from longclaw import urls as longclaw_urls 10 | from longclaw.contrib.productrequests import urls as request_urls 11 | 12 | urlpatterns = [ 13 | url(r'^admin/', include(admin_urls)), 14 | url(r'^documents/', include(documents_urls)), 15 | 16 | url(r'', include(longclaw_urls)), 17 | url(r'', include(request_urls)), 18 | url(r'', include(wagtail_urls)), 19 | 20 | 21 | ] 22 | -------------------------------------------------------------------------------- /longclaw/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from longclaw.basket import urls as basket_urls 3 | from longclaw.checkout import urls as checkout_urls 4 | from longclaw.shipping import urls as shipping_urls 5 | from longclaw.orders import urls as order_urls 6 | 7 | urlpatterns = [ 8 | url(r'', include(basket_urls)), 9 | url(r'', include(checkout_urls)), 10 | url(r'', include(shipping_urls)), 11 | url(r'', include(order_urls)), 12 | ] 13 | -------------------------------------------------------------------------------- /longclaw/utils.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.utils.module_loading import import_string 3 | from longclaw.settings import PRODUCT_VARIANT_MODEL, PAYMENT_GATEWAY 4 | 5 | GATEWAY = import_string(PAYMENT_GATEWAY)() 6 | ProductVariant = apps.get_model(*PRODUCT_VARIANT_MODEL.split('.')) 7 | 8 | 9 | def maybe_get_product_model(): 10 | try: 11 | field = ProductVariant._meta.get_field('product') 12 | return field.related_model 13 | except: 14 | pass 15 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | if __name__ == "__main__": 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "longclaw.tests.settings") 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 3 | from __future__ import unicode_literals, absolute_import 4 | 5 | import os 6 | import sys 7 | 8 | import django 9 | from django.conf import settings 10 | from django.test.utils import get_runner 11 | 12 | def run_tests(*test_args): 13 | if not test_args: 14 | test_args = [] 15 | 16 | os.environ['DJANGO_SETTINGS_MODULE'] = 'longclaw.tests.settings' 17 | django.setup() 18 | test_runner = get_runner(settings)() 19 | failures = test_runner.run_tests(test_args) 20 | sys.exit(bool(failures)) 21 | 22 | 23 | if __name__ == '__main__': 24 | run_tests(*sys.argv[1:]) 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.0.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:longclaw/__init__.py] 7 | 8 | [wheel] 9 | universal = 1 10 | 11 | [flake8] 12 | ignore = D203 13 | exclude = 14 | .git, 15 | .tox 16 | docs/source/conf.py, 17 | build, 18 | dist 19 | max-line-length = 119 20 | 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | usedevelop = True 4 | skip_missing_interpreters = True 5 | 6 | envlist = 7 | py{37,38,39}-dj{22}-wt{211,212,213} 8 | 9 | [gh-actions] 10 | python = 11 | 3.7: py37 12 | 3.8: py38 13 | 3.9: py39 14 | 15 | [testenv] 16 | setenv = 17 | PYTHONPATH = {toxinidir}:{toxinidir}/longclaw 18 | 19 | deps = 20 | coverage 21 | django-extensions 22 | django-polymorphic 23 | django-ipware 24 | mock 25 | wagtail-factories 26 | 27 | dj22: Django>=2.2,<3.0 28 | dj30: Django>=3.0,<3.1 29 | wt211: wagtail>=2.11,<2.12 30 | wt212: wagtail>=2.12,<2.13 31 | wt213: wagtail>=2.13,<2.14 32 | 33 | install_command = pip install -U {opts} {packages} 34 | 35 | commands = 36 | coverage run --source longclaw runtests.py 37 | coverage xml --omit=*/apps.py,*/migrations/*,*/__init__.py,*/gateways/braintree.py,*/gateways/stripe.py,*/bin/longclaw.py 38 | 39 | basepython = 40 | py37: python3.7 41 | -------------------------------------------------------------------------------- /vagrant/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant/ 2 | -------------------------------------------------------------------------------- /vagrant/README: -------------------------------------------------------------------------------- 1 | CREATES A VAGRANT ENVIRONMENT TO FACILITATE LOCAL TESTING 2 | 3 | ======== 4 | USAGE: 5 | ======== 6 | To run tests: 7 | cd to this directory and then issue the following commands: 8 | vagrant up 9 | vagrant ssh 10 | bash /vagrant/vagrant/runtox.sh 11 | 12 | To clean after tests run: 13 | rm -R /tmp/vagrant 14 | 15 | To use django's manage.py: 16 | apt-get install -y python3-pip 17 | cd /vagrant 18 | pip3 install -r requirements.txt 19 | python3 manage.py --help 20 | 21 | To use django's manage.py shell: 22 | apt-get install -y python3-pip 23 | cd /vagrant 24 | pip3 install -r requirements.txt 25 | python3 manage.py shell 26 | 27 | To make migrations: 28 | apt-get install -y python3-pip 29 | cd /vagrant 30 | pip3 install -r requirements.txt 31 | python3 manage.py makemigrations 32 | 33 | To migrate: 34 | apt-get install -y python3-pip 35 | cd /vagrant 36 | pip3 install -r requirements.txt 37 | python3 manage.py migrate 38 | 39 | To run tests directly: 40 | apt-get install -y python3-pip 41 | cd /vagrant 42 | pip3 install -r requirements.txt 43 | pip3 install -r requirements_dev.txt 44 | python3 manage.py test 45 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "ubuntu/xenial64" 3 | 4 | config.vm.provider "virtualbox" do |v| 5 | v.name = "longclaw" 6 | end 7 | 8 | config.vm.provision :shell, path: "provision.sh" 9 | config.vm.synced_folder ".", "/vagrant", disabled: true 10 | config.vm.synced_folder "../", "/vagrant" 11 | end 12 | -------------------------------------------------------------------------------- /vagrant/provision.sh: -------------------------------------------------------------------------------- 1 | set -o errexit 2 | set -o pipefail 3 | set -o nounset 4 | shopt -s failglob 5 | set -o xtrace 6 | 7 | export DEBIAN_FRONTEND=noninteractive 8 | 9 | function run_user_command { 10 | echo '' 11 | echo "RUNNING COMMAND: $1" 12 | tmpfile=$(sudo -u vagrant mktemp /tmp/run_user_command.XXXXXX) 13 | su - vagrant -c "$1; echo \\$? > $tmpfile" 14 | RETVAL=`cat $tmpfile` 15 | rm "$tmpfile" 16 | (( RETVAL )) && { echo "COMMAND RETURNED NON-ZERO EXIT CODE $RETVAL"; exit $RETVAL; } 17 | (( RETVAL )) || echo "COMMAND SUCCESS" 18 | echo '' 19 | } 20 | 21 | add-apt-repository -y ppa:deadsnakes/ppa 22 | apt-get update 23 | apt-get install -y build-essential libssl-dev tox python3.5 python3.6 python3.7 24 | 25 | run_user_command "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash" 26 | run_user_command ". /home/vagrant/.nvm/nvm.sh; nvm install stable" 27 | run_user_command ". /home/vagrant/.nvm/nvm.sh; nvm use stable" 28 | -------------------------------------------------------------------------------- /vagrant/runtox.sh: -------------------------------------------------------------------------------- 1 | rsync --recursive --exclude="*/node_modules/*" --exclude="*/.git/*" /vagrant /tmp 2 | 3 | cd /tmp/vagrant 4 | 5 | tox "$@" 6 | -------------------------------------------------------------------------------- /website/blog/2016-03-11-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Blog Title 3 | author: Blog Author 4 | authorURL: http://twitter.com/ 5 | authorFBID: 100002976521003 6 | --- 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus elementum massa eget nulla aliquet sagittis. Proin odio tortor, vulputate ut odio in, ultrices ultricies augue. Cras ornare ultrices lorem malesuada iaculis. Etiam sit amet libero tempor, pulvinar mauris sed, sollicitudin sapien. 9 | 10 | 11 | 12 | Mauris vestibulum ullamcorper nibh, ut semper purus pulvinar ut. Donec volutpat orci sit amet mauris malesuada, non pulvinar augue aliquam. Vestibulum ultricies at urna ut suscipit. Morbi iaculis, erat at imperdiet semper, ipsum nulla sodales erat, eget tincidunt justo dui quis justo. Pellentesque dictum bibendum diam at aliquet. Sed pulvinar, dolor quis finibus ornare, eros odio facilisis erat, eu rhoncus nunc dui sed ex. Nunc gravida dui massa, sed ornare arcu tincidunt sit amet. Maecenas efficitur sapien neque, a laoreet libero feugiat ut. 13 | 14 | Nulla facilisi. Maecenas sodales nec purus eget posuere. Sed sapien quam, pretium a risus in, porttitor dapibus erat. Sed sit amet fringilla ipsum, eget iaculis augue. Integer sollicitudin tortor quis ultricies aliquam. Suspendisse fringilla nunc in tellus cursus, at placerat tellus scelerisque. Sed tempus elit a sollicitudin rhoncus. Nulla facilisi. Morbi nec dolor dolor. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras et aliquet lectus. Pellentesque sit amet eros nisi. Quisque ac sapien in sapien congue accumsan. Nullam in posuere ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin lacinia leo a nibh fringilla pharetra. 15 | 16 | Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin venenatis lectus dui, vel ultrices ante bibendum hendrerit. Aenean egestas feugiat dui id hendrerit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur in tellus laoreet, eleifend nunc id, viverra leo. Proin vulputate non dolor vel vulputate. Curabitur pretium lobortis felis, sit amet finibus lorem suscipit ut. Sed non mollis risus. Duis sagittis, mi in euismod tincidunt, nunc mauris vestibulum urna, at euismod est elit quis erat. Phasellus accumsan vitae neque eu placerat. In elementum arcu nec tellus imperdiet, eget maximus nulla sodales. Curabitur eu sapien eget nisl sodales fermentum. 17 | 18 | Phasellus pulvinar ex id commodo imperdiet. Praesent odio nibh, sollicitudin sit amet faucibus id, placerat at metus. Donec vitae eros vitae tortor hendrerit finibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque vitae purus dolor. Duis suscipit ac nulla et finibus. Phasellus ac sem sed dui dictum gravida. Phasellus eleifend vestibulum facilisis. Integer pharetra nec enim vitae mattis. Duis auctor, lectus quis condimentum bibendum, nunc dolor aliquam massa, id bibendum orci velit quis magna. Ut volutpat nulla nunc, sed interdum magna condimentum non. Sed urna metus, scelerisque vitae consectetur a, feugiat quis magna. Donec dignissim ornare nisl, eget tempor risus malesuada quis. 19 | -------------------------------------------------------------------------------- /website/blog/2017-04-10-blog-post-two.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: New Blog Post 3 | author: Blog Author 4 | authorURL: http://twitter.com/ 5 | authorFBID: 100002976521003 6 | --- 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus elementum massa eget nulla aliquet sagittis. Proin odio tortor, vulputate ut odio in, ultrices ultricies augue. Cras ornare ultrices lorem malesuada iaculis. Etiam sit amet libero tempor, pulvinar mauris sed, sollicitudin sapien. 9 | 10 | 11 | 12 | Mauris vestibulum ullamcorper nibh, ut semper purus pulvinar ut. Donec volutpat orci sit amet mauris malesuada, non pulvinar augue aliquam. Vestibulum ultricies at urna ut suscipit. Morbi iaculis, erat at imperdiet semper, ipsum nulla sodales erat, eget tincidunt justo dui quis justo. Pellentesque dictum bibendum diam at aliquet. Sed pulvinar, dolor quis finibus ornare, eros odio facilisis erat, eu rhoncus nunc dui sed ex. Nunc gravida dui massa, sed ornare arcu tincidunt sit amet. Maecenas efficitur sapien neque, a laoreet libero feugiat ut. 13 | 14 | Nulla facilisi. Maecenas sodales nec purus eget posuere. Sed sapien quam, pretium a risus in, porttitor dapibus erat. Sed sit amet fringilla ipsum, eget iaculis augue. Integer sollicitudin tortor quis ultricies aliquam. Suspendisse fringilla nunc in tellus cursus, at placerat tellus scelerisque. Sed tempus elit a sollicitudin rhoncus. Nulla facilisi. Morbi nec dolor dolor. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras et aliquet lectus. Pellentesque sit amet eros nisi. Quisque ac sapien in sapien congue accumsan. Nullam in posuere ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin lacinia leo a nibh fringilla pharetra. 15 | 16 | Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin venenatis lectus dui, vel ultrices ante bibendum hendrerit. Aenean egestas feugiat dui id hendrerit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur in tellus laoreet, eleifend nunc id, viverra leo. Proin vulputate non dolor vel vulputate. Curabitur pretium lobortis felis, sit amet finibus lorem suscipit ut. Sed non mollis risus. Duis sagittis, mi in euismod tincidunt, nunc mauris vestibulum urna, at euismod est elit quis erat. Phasellus accumsan vitae neque eu placerat. In elementum arcu nec tellus imperdiet, eget maximus nulla sodales. Curabitur eu sapien eget nisl sodales fermentum. 17 | 18 | Phasellus pulvinar ex id commodo imperdiet. Praesent odio nibh, sollicitudin sit amet faucibus id, placerat at metus. Donec vitae eros vitae tortor hendrerit finibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque vitae purus dolor. Duis suscipit ac nulla et finibus. Phasellus ac sem sed dui dictum gravida. Phasellus eleifend vestibulum facilisis. Integer pharetra nec enim vitae mattis. Duis auctor, lectus quis condimentum bibendum, nunc dolor aliquam massa, id bibendum orci velit quis magna. Ut volutpat nulla nunc, sed interdum magna condimentum non. Sed urna metus, scelerisque vitae consectetur a, feugiat quis magna. Donec dignissim ornare nisl, eget tempor risus malesuada quis. 19 | -------------------------------------------------------------------------------- /website/blog/2017-09-25-testing-rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding RSS Support - RSS Truncation Test 3 | author: Eric Nakagawa 4 | authorURL: http://twitter.com/ericnakagawa 5 | authorFBID: 661277173 6 | --- 7 | 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 8 | 9 | This should be truncated. 10 | 11 | This line should never render in XML. 12 | -------------------------------------------------------------------------------- /website/blog/2017-09-26-adding-rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding RSS Support 3 | author: Eric Nakagawa 4 | authorURL: http://twitter.com/ericnakagawa 5 | authorFBID: 661277173 6 | --- 7 | 8 | This is a test post. 9 | 10 | A whole bunch of other information. 11 | -------------------------------------------------------------------------------- /website/blog/2017-10-24-new-version-1.0.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: New Version 1.0.0 3 | author: Eric Nakagawa 4 | authorURL: http://twitter.com/ericnakagawa 5 | authorFBID: 661277173 6 | --- 7 | 8 | This blog post will test file name parsing issues when periods are present. 9 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | class Footer extends React.Component { 11 | docUrl(doc, language) { 12 | const baseUrl = this.props.config.baseUrl; 13 | const docsUrl = this.props.config.docsUrl; 14 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; 15 | const langPart = `${language ? `${language}/` : ''}`; 16 | return `${baseUrl}${docsPart}${langPart}${doc}`; 17 | } 18 | 19 | pageUrl(doc, language) { 20 | const baseUrl = this.props.config.baseUrl; 21 | return baseUrl + (language ? `${language}/` : '') + doc; 22 | } 23 | 24 | render() { 25 | return ( 26 | 81 | ); 82 | } 83 | } 84 | 85 | module.exports = Footer; 86 | -------------------------------------------------------------------------------- /website/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "This file is auto-generated by write-translations.js", 3 | "localized-strings": { 4 | "next": "Next", 5 | "previous": "Previous", 6 | "tagline": "E-commerce extension for Wagtail CMS", 7 | "docs": { 8 | "guide/api": { 9 | "title": "API Client", 10 | "sidebar_label": "API Client" 11 | }, 12 | "guide/basket": { 13 | "title": "Basket", 14 | "sidebar_label": "Basket" 15 | }, 16 | "guide/checkout_api": { 17 | "title": "Checkout API", 18 | "sidebar_label": "Checkout API" 19 | }, 20 | "guide/checkout": { 21 | "title": "Checkout", 22 | "sidebar_label": "Checkout" 23 | }, 24 | "guide/contrib": { 25 | "title": "Product Requests", 26 | "sidebar_label": "Product Requests" 27 | }, 28 | "guide/payments": { 29 | "title": "Payment Backends", 30 | "sidebar_label": "Integrations" 31 | }, 32 | "tutorial/checkout": { 33 | "title": "Configuring Payment", 34 | "sidebar_label": "Payment" 35 | }, 36 | "tutorial/frontend": { 37 | "title": "Displaying Products", 38 | "sidebar_label": "Frontend" 39 | }, 40 | "tutorial/install": { 41 | "title": "Setup", 42 | "sidebar_label": "Setup" 43 | }, 44 | "tutorial/introduction": { 45 | "title": "Longclaw Bakery Tutorial", 46 | "sidebar_label": "Introduction" 47 | }, 48 | "tutorial/products": { 49 | "title": "Adding Products", 50 | "sidebar_label": "Adding Products" 51 | }, 52 | "tutorial/shipping": { 53 | "title": "Configuring Shipping", 54 | "sidebar_label": "Shipping" 55 | } 56 | }, 57 | "links": { 58 | "Docs": "Docs", 59 | "Demo": "Demo", 60 | "Help": "Help" 61 | }, 62 | "categories": { 63 | "Tutorial": "Tutorial", 64 | "User Guide": "User Guide" 65 | } 66 | }, 67 | "pages-strings": { 68 | "Help Translate|recruit community translators for your project": "Help Translate", 69 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit", 70 | "Translate this Doc|recruitment message asking to translate the docs": "Translate" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "examples": "docusaurus-examples", 4 | "start": "docusaurus-start", 5 | "build": "docusaurus-build", 6 | "publish-gh-pages": "docusaurus-publish", 7 | "write-translations": "docusaurus-write-translations", 8 | "version": "docusaurus-version", 9 | "rename-version": "docusaurus-rename-version" 10 | }, 11 | "devDependencies": { 12 | "docusaurus": "^1.6.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/pages/en/help.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | const GridBlock = CompLibrary.GridBlock; 14 | 15 | function Help(props) { 16 | const {config: siteConfig, language = ''} = props; 17 | const {baseUrl, docsUrl} = siteConfig; 18 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; 19 | const langPart = `${language ? `${language}/` : ''}`; 20 | const docUrl = doc => `${baseUrl}${docsPart}${langPart}${doc}`; 21 | 22 | const supportLinks = [ 23 | { 24 | content: `Learn more using the [documentation on this site.](${docUrl( 25 | 'tutorial/introduction.html', 26 | )})`, 27 | title: 'Browse Docs', 28 | }, 29 | { 30 | content: 'Ask questions about the documentation and project', 31 | title: 'Join the community', 32 | }, 33 | { 34 | content: "Find out what's new with this project", 35 | title: 'Stay up to date', 36 | }, 37 | ]; 38 | 39 | return ( 40 |
    41 | 42 |
    43 |
    44 |

    Need help?

    45 |
    46 |

    This project is maintained by a dedicated group of people.

    47 | 48 |
    49 |
    50 |
    51 | ); 52 | } 53 | 54 | module.exports = Help; 55 | -------------------------------------------------------------------------------- /website/pages/en/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | 14 | class Users extends React.Component { 15 | render() { 16 | const {config: siteConfig} = this.props; 17 | if ((siteConfig.users || []).length === 0) { 18 | return null; 19 | } 20 | 21 | const editUrl = `${siteConfig.repoUrl}/edit/master/website/siteConfig.js`; 22 | const showcase = siteConfig.users.map(user => ( 23 | 24 | {user.caption} 25 | 26 | )); 27 | 28 | return ( 29 |
    30 | 31 |
    32 |
    33 |

    Who is Using This?

    34 |
    35 |
    {showcase}
    36 |

    Are you using this project?

    37 | 38 | Add your company 39 | 40 |
    41 |
    42 |
    43 | ); 44 | } 45 | } 46 | 47 | module.exports = Users; 48 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Tutorial": ["tutorial/introduction", "tutorial/install", "tutorial/products", "tutorial/shipping", "tutorial/frontend", "tutorial/checkout"], 4 | "User Guide": ["guide/checkout", "guide/payments", "guide/basket", "guide/api", "guide/checkout_api", "guide/contrib"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /website/static/css/custom.css: -------------------------------------------------------------------------------- 1 | /* your custom css */ 2 | 3 | @media only screen and (min-device-width: 360px) and (max-device-width: 736px) { 4 | } 5 | 6 | @media only screen and (min-width: 1024px) { 7 | } 8 | 9 | @media only screen and (max-width: 1023px) { 10 | } 11 | 12 | @media only screen and (min-width: 1400px) { 13 | } 14 | 15 | @media only screen and (min-width: 1500px) { 16 | } -------------------------------------------------------------------------------- /website/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/website/static/img/favicon.png -------------------------------------------------------------------------------- /website/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/website/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /website/static/img/shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/website/static/img/shop.png -------------------------------------------------------------------------------- /website/static/img/wagtail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longclawshop/longclaw/cfd2df17e065fec0656d6df27e561fea44e93f2a/website/static/img/wagtail.png --------------------------------------------------------------------------------