├── .devcontainer ├── devcontainer.json └── docker-compose.yml ├── .dockerignore ├── .github └── workflows │ ├── django-test.yml │ └── prod.workflow.yml ├── .gitignore ├── .pylintrc ├── .vscode └── launch.json ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── TODO.md ├── config ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── docker-compose.yml ├── ecommerce ├── __init__.py ├── admin.py ├── apps.py ├── context_processors.py ├── custom_permissions.py ├── decorators.py ├── filters.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_initial.py │ ├── 0003_alter_category_slug_alter_category_title_and_more.py │ ├── 0004_alter_category_slug.py │ ├── 0005_alter_category_slug_alter_product_category.py │ ├── 0006_alter_product_title.py │ ├── 0007_alter_image_image_location.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── entrypoint.sh ├── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── assets │ │ └── img │ │ │ ├── .gitkeep │ │ │ └── media │ │ │ ├── 01.jpg │ │ │ ├── 02.jpg │ │ │ ├── 03.jpg │ │ │ ├── 04.jpg │ │ │ ├── 06.jpg │ │ │ ├── background.jpg │ │ │ ├── bolso.webp │ │ │ ├── card1.jpg │ │ │ ├── card2.jpg │ │ │ ├── card3.jpg │ │ │ ├── correo.webp │ │ │ ├── image 52.png │ │ │ ├── img3.jpg │ │ │ ├── logo.png │ │ │ └── mano.jpg │ ├── components │ │ ├── Cart.js │ │ ├── Carusel.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Layout.js │ │ ├── PageNotFound.js │ │ └── Spinner.js │ ├── fonts.css │ ├── hooks │ │ └── .gitkeep │ ├── index.css │ ├── index.js │ ├── pages │ │ ├── About │ │ │ └── About.js │ │ ├── Contact │ │ │ └── Contact.js │ │ ├── Home │ │ │ └── Home.js │ │ ├── Login │ │ │ ├── Login.js │ │ │ └── Logins.css │ │ ├── Product │ │ │ └── Product.js │ │ ├── Products │ │ │ └── Products.js │ │ ├── Profile │ │ │ └── Profile.js │ │ └── ProfileSeller │ │ │ └── ProfileSeller.js │ └── store │ │ ├── index.js │ │ └── slices │ │ ├── category │ │ ├── category.rest │ │ ├── index.js │ │ └── services.js │ │ ├── product │ │ ├── index.js │ │ └── services.js │ │ └── user │ │ ├── index.js │ │ ├── services.js │ │ └── user.rest └── tailwind.config.js ├── local.Dockerfile ├── manage.py ├── payment ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── pyproject.toml ├── readme ├── Docker_guide.md ├── Esquema.drawio ├── Guide_Pipenv.md └── Problemas.md ├── requirements-deploy.txt ├── requirements.txt ├── runtime.txt ├── setup.cfg ├── shopping_cart ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── shopping_cart.py ├── tests.py ├── urls.py └── views.py ├── staticfiles └── css │ └── main.css ├── templates ├── base.html ├── cart │ ├── cart.html │ └── images.html ├── categories.html ├── ecommerce │ ├── home.html │ ├── images.html │ ├── product_delete.html │ ├── product_detail.html │ └── product_edit.html ├── footer.html ├── hero.html ├── navbar.html ├── payment │ ├── cancel.html │ ├── payment.html │ └── success.html └── users │ ├── login.html │ ├── panel.html │ ├── register.html │ ├── seller_register.html │ ├── user_panel.html │ └── users_edit.html └── users ├── __init__.py ├── admin.py ├── apps.py ├── decorators.py ├── forms.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── tests.py ├── urls.py └── views.py /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Devcontainer Config for MarktPlace", 3 | 4 | // Update the 'dockerComposeFile' list if you have more compose files or use different names. 5 | // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. 6 | "dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"], 7 | 8 | // The 'service' property is the name of the service for the container that VS Code should 9 | // use. Update this value and .devcontainer/docker-compose.yml to the real service name. 10 | "service": "django", 11 | 12 | // The optional 'workspaceFolder' property is the path VS Code should open by default when 13 | // connected. This is typically a file mount in .devcontainer/docker-compose.yml 14 | "workspaceFolder": "/workspace", 15 | 16 | // Set *default* container specific settings.json values on container create. 17 | "settings": { 18 | "python.defaultInterpreterPath": "/usr/local/bin/python", // Para que vs code encuentre los modulos necesarios 19 | "python.linting.enabled": true, 20 | "python.linting.pylintEnabled": true, 21 | "python.linting.pylintPath": "/usr/local/bin/pylint", 22 | "python.languageServer": "Pylance" // Activar autocompletado 23 | }, 24 | 25 | // Add the IDs of extensions you want installed when the container is created. 26 | "extensions": ["ms-python.python", "VisualStudioExptTeam.vscodeintellicode"], 27 | 28 | "shutdownAction": "stopCompose" 29 | } 30 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | # Update this to the name of the service you want to work with in your docker-compose.yml file 4 | django: 5 | # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" 6 | # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 7 | # debugging) to execute as the user. Uncomment the next line if you want the entire 8 | # container to run as this user instead. Note that, on Linux, you may need to 9 | # ensure the UID and GID of the container user you create matches your local user. 10 | # See https://aka.ms/vscode-remote/containers/non-root for details. 11 | # 12 | # user: vscode 13 | 14 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 15 | # folder. Note that the path of the Dockerfile and context is relative to the *primary* 16 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 17 | # array). The sample below assumes your primary file is in the root of your project. 18 | # 19 | # build: 20 | # context: . 21 | # dockerfile: .devcontainer/Dockerfile 22 | 23 | volumes: 24 | # Update this to wherever you want VS Code to mount the folder of your project 25 | - .:/workspace:cached 26 | 27 | # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. 28 | # - /var/run/docker.sock:/var/run/docker.sock 29 | 30 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 31 | # cap_add: 32 | # - SYS_PTRACE 33 | # security_opt: 34 | # - seccomp:unconfined 35 | 36 | # Overrides default command so things don't shut down after the process ends. 37 | command: /bin/sh -c "while sleep 1000; do :; done" 38 | 39 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | db.sqlite3 3 | Pipfile 4 | Pipfile.lock -------------------------------------------------------------------------------- /.github/workflows/django-test.yml: -------------------------------------------------------------------------------- 1 | name: Django tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | services: 13 | postgres: 14 | image: postgres:latest 15 | env: 16 | POSTGRES_USER: postgres 17 | POSTGRES_PASSWORD: postgres 18 | POSTGRES_DB: github_actions 19 | ports: 20 | - 5432:5432 21 | # needed because the postgres container does not provide a healthcheck 22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 23 | env: 24 | DEBUG: ${{ secrets.DEBUG }} 25 | DJANGO_KEY: ${{ secrets.DJANGO_KEY }} 26 | NAME_DB_HEROKU: ${{ secrets.NAME_DB_HEROKU }} 27 | USER_DB_HEROKU: ${{ secrets.USER_DB_HEROKU }} 28 | PASSWORD_DB_HEROKU: ${{ secrets.PASSWORD_DB_HEROKU }} 29 | HOST_DB_HEROKU: ${{ secrets.HOST_DB_HEROKU }} 30 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 31 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 32 | AWS_STORAGE_BUCKET_NAME: ${{ secrets.AWS_STORAGE_BUCKET_NAME }} 33 | STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }} 34 | STRIPE_PRIVATE_KEY: ${{ secrets.STRIPE_PRIVATE_KEY }} 35 | STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }} 36 | POSTGRES_NAME: github_actions 37 | POSTGRES_USER: postgres 38 | POSTGRES_PASSWORD: postgres 39 | POSTGRES_HOST: 127.0.0.1 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Set up Python 3.9 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: 3.9 46 | - name: Install pipenv 47 | run: | 48 | python -m pip install pipenv 49 | - name: Create virtual environment with dependencies 50 | run: | 51 | python -m pipenv install 52 | - name: Use isort 53 | run: | 54 | pipenv run isort . 55 | - name: Use Black 56 | run: | 57 | pipenv run black . 58 | - name: Flake 59 | run: | 60 | pipenv run flake8 61 | - name: Pylint 62 | run: | 63 | pipenv run pylint ./ecommerce 64 | - name: Run migrations 65 | run: | 66 | pipenv run python manage.py migrate 67 | - name: Run Tests 68 | run: | 69 | pipenv run python manage.py test 70 | -------------------------------------------------------------------------------- /.github/workflows/prod.workflow.yml: -------------------------------------------------------------------------------- 1 | name: Master Workflow - Build and deploy to production 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | services: 12 | postgres: 13 | image: postgres:latest 14 | env: 15 | POSTGRES_USER: postgres 16 | POSTGRES_PASSWORD: postgres 17 | POSTGRES_DB: github_actions 18 | ports: 19 | - 5432:5432 20 | # needed because the postgres container does not provide a healthcheck 21 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 22 | env: 23 | DEBUG: ${{ secrets.DEBUG }} 24 | DJANGO_KEY: ${{ secrets.DJANGO_KEY }} 25 | NAME_DB_HEROKU: ${{ secrets.NAME_DB_HEROKU }} 26 | USER_DB_HEROKU: ${{ secrets.USER_DB_HEROKU }} 27 | PASSWORD_DB_HEROKU: ${{ secrets.PASSWORD_DB_HEROKU }} 28 | HOST_DB_HEROKU: ${{ secrets.HOST_DB_HEROKU }} 29 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 30 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 31 | AWS_STORAGE_BUCKET_NAME: ${{ secrets.AWS_STORAGE_BUCKET_NAME }} 32 | STRIPE_PUBLIC_KEY: ${{ secrets.STRIPE_PUBLIC_KEY }} 33 | STRIPE_PRIVATE_KEY: ${{ secrets.STRIPE_PRIVATE_KEY }} 34 | STRIPE_WEBHOOK_KEY: ${{ secrets.STRIPE_WEBHOOK_KEY }} 35 | POSTGRES_NAME: github_actions 36 | POSTGRES_USER: postgres 37 | POSTGRES_PASSWORD: postgres 38 | POSTGRES_HOST: 127.0.0.1 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Set up Python 3.9 42 | uses: actions/setup-python@v2 43 | with: 44 | python-version: 3.9 45 | - name: Install pipenv 46 | run: | 47 | python -m pip install pipenv 48 | - name: Create virtual environment with dependencies 49 | run: | 50 | python -m pipenv install 51 | - name: Use isort 52 | run: | 53 | pipenv run isort . 54 | - name: Use Black 55 | run: | 56 | pipenv run black . 57 | - name: Flake 58 | run: | 59 | pipenv run flake8 60 | - name: Pylint 61 | run: | 62 | pipenv run pylint ./ecommerce 63 | - name: Run migrations 64 | run: | 65 | pipenv run python manage.py migrate 66 | - name: Run Tests 67 | run: | 68 | pipenv run python manage.py test 69 | deploy: 70 | needs: test 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v2 74 | - uses: akhileshns/heroku-deploy@v3.12.12 # This is the action 75 | with: 76 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 77 | heroku_app_name: "django-ecommerce-v1" #Must be unique in Heroku 78 | heroku_email: "arturo.mtz8@gmail.com" 79 | usedocker: true 80 | env: 81 | HD_POSTGRES_NAME: ${{secrets.NAME_DB_HEROKU}} 82 | HD_POSTGRES_USER: ${{secrets.USER_DB_HEROKU}} 83 | HD_POSTGRES_PASSWORD: ${{secrets.PASSWORD_DB_HEROKU}} 84 | HD_POSTGRES_HOST: ${{secrets.HOST_DB_HEROKU}} 85 | HD_DJANGO_KEY: ${{secrets.DJANGO_KEY}} 86 | HD_DEBUG: ${{secrets.DEBUG}} 87 | HD_AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}} 88 | HD_AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}} 89 | HD_AWS_STORAGE_BUCKET_NAME: ${{secrets.AWS_STORAGE_BUCKET_NAME}} 90 | HD_STRIPE_PRIVATE_KEY: ${{secrets.STRIPE_PRIVATE_KEY}} 91 | HD_STRIPE_PUBLIC_KEY: ${{secrets.STRIPE_PUBLIC_KEY}} 92 | HD_STRIPE_WEBHOOK_KEY: ${{secrets.STRIPE_WEBHOOK_KEY}} 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,django 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,django 4 | 5 | ### Django ### 6 | *.log 7 | *.pot 8 | *.pyc 9 | __pycache__/ 10 | local_settings.py 11 | db.sqlite3 12 | db.sqlite3-journal 13 | media 14 | .env 15 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 16 | # in your Git repository. Update and uncomment the following line accordingly. 17 | # /staticfiles/ 18 | 19 | ### Django.Python Stack ### 20 | # Byte-compiled / optimized / DLL files 21 | *.py[cod] 22 | *$py.class 23 | 24 | # C extensions 25 | *.so 26 | 27 | # Distribution / packaging 28 | .Python 29 | build/ 30 | develop-eggs/ 31 | dist/ 32 | downloads/ 33 | eggs/ 34 | .eggs/ 35 | lib/ 36 | lib64/ 37 | parts/ 38 | sdist/ 39 | var/ 40 | wheels/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | *.py,cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | cover/ 71 | 72 | # Translations 73 | *.mo 74 | 75 | # Django stuff: 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | .pybuilder/ 89 | target/ 90 | 91 | # Jupyter Notebook 92 | .ipynb_checkpoints 93 | 94 | # IPython 95 | profile_default/ 96 | ipython_config.py 97 | 98 | # pyenv 99 | # For a library or package, you might want to ignore these files since the code is 100 | # intended to run in multiple environments; otherwise, check them in: 101 | # .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # poetry 111 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 112 | # This is especially recommended for binary packages to ensure reproducibility, and is more 113 | # commonly ignored for libraries. 114 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 115 | #poetry.lock 116 | 117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 118 | __pypackages__/ 119 | 120 | # Celery stuff 121 | celerybeat-schedule 122 | celerybeat.pid 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | data/ 136 | 137 | # Spyder project settings 138 | .spyderproject 139 | .spyproject 140 | 141 | # Rope project settings 142 | .ropeproject 143 | 144 | # mkdocs documentation 145 | /site 146 | 147 | # mypy 148 | .mypy_cache/ 149 | .dmypy.json 150 | dmypy.json 151 | 152 | # Pyre type checker 153 | .pyre/ 154 | 155 | # pytype static type analyzer 156 | .pytype/ 157 | 158 | # Cython debug symbols 159 | cython_debug/ 160 | 161 | # PyCharm 162 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 163 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 164 | # and can be added to the global gitignore or merged into this file. For a more nuclear 165 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 166 | #.idea/ 167 | 168 | ### Python ### 169 | # Byte-compiled / optimized / DLL files 170 | 171 | # C extensions 172 | 173 | # Distribution / packaging 174 | 175 | # PyInstaller 176 | # Usually these files are written by a python script from a template 177 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 178 | 179 | # Installer logs 180 | 181 | # Unit test / coverage reports 182 | 183 | # Translations 184 | 185 | # Django stuff: 186 | 187 | # Flask stuff: 188 | 189 | # Scrapy stuff: 190 | 191 | # Sphinx documentation 192 | 193 | # PyBuilder 194 | 195 | # Jupyter Notebook 196 | 197 | # IPython 198 | 199 | # pyenv 200 | # For a library or package, you might want to ignore these files since the code is 201 | # intended to run in multiple environments; otherwise, check them in: 202 | # .python-version 203 | 204 | # pipenv 205 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 206 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 207 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 208 | # install all needed dependencies. 209 | 210 | # poetry 211 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 212 | # This is especially recommended for binary packages to ensure reproducibility, and is more 213 | # commonly ignored for libraries. 214 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 215 | 216 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 217 | 218 | # Celery stuff 219 | 220 | # SageMath parsed files 221 | 222 | # Environments 223 | 224 | # Spyder project settings 225 | 226 | # Rope project settings 227 | 228 | # mkdocs documentation 229 | 230 | # mypy 231 | 232 | # Pyre type checker 233 | 234 | # pytype static type analyzer 235 | 236 | # Cython debug symbols 237 | 238 | # PyCharm 239 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 240 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 241 | # and can be added to the global gitignore or merged into this file. For a more nuclear 242 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 243 | 244 | ### VisualStudioCode ### 245 | .vscode/* 246 | !.vscode/settings.json 247 | !.vscode/tasks.json 248 | !.vscode/launch.json 249 | !.vscode/extensions.json 250 | !.vscode/*.code-snippets 251 | 252 | # Local History for Visual Studio Code 253 | .history/ 254 | 255 | # Built Visual Studio Code Extensions 256 | *.vsix 257 | 258 | ### VisualStudioCode Patch ### 259 | # Ignore all local history of files 260 | .history 261 | .ionide 262 | 263 | # Support for Project snippet scope 264 | 265 | # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,django 266 | 267 | # frotend 268 | node_modules/ 269 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable = line-too-long, missing-module-docstring, missing-function-docstring, 3 | no-member, no-value-for-parameter, no-else-return, too-few-public-methods, 4 | missing-class-docstring, unused-variable, invalid-str-returned, unused-argument, 5 | invalid-name, wildcard-import, unused-wildcard-import, super-with-arguments, 6 | raise-missing-from, broad-except 7 | ignore = migrations 8 | 9 | [MESSAGES CONTROL] 10 | disable= 11 | W0622 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense para saber los atributos posibles. 3 | // Mantenga el puntero para ver las descripciones de los existentes atributos. 4 | // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Django", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/manage.py", 12 | "args": [ 13 | "runserver" 14 | ], 15 | "django": true 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine3.15 2 | 3 | # Keeps Python from generating .pyc files in the container 4 | ENV PYTHONDONTWRITEBYTECODE=1 5 | # Turns off buffering for easier container logging 6 | ENV PYTHONUNBUFFERED=1 7 | 8 | WORKDIR /app 9 | 10 | COPY ./requirements-deploy.txt ./ 11 | 12 | RUN apk update \ 13 | && apk add --no-cache gcc musl-dev postgresql-dev python3-dev jpeg-dev zlib-dev \ 14 | && pip install --upgrade pip 15 | 16 | RUN python -m pip install -r requirements-deploy.txt 17 | 18 | COPY ./ ./ 19 | 20 | CMD [ "sh", "entrypoint.sh" ] 21 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | psycopg2 = "*" 8 | django-redis = "*" 9 | pillow = "*" 10 | flake8 = "*" 11 | black = "==18.9b0" 12 | isort = "*" 13 | pylint = "*" 14 | django = "*" 15 | gunicorn = "*" 16 | whitenoise = "*" 17 | django-storages = "*" 18 | boto3 = "*" 19 | django-heroku = "*" 20 | django-debug-toolbar = "*" 21 | django-environ = "*" 22 | stripe = "*" 23 | djangorestframework-simplejwt = "*" 24 | djangorestframework = "*" 25 | drf-yasg2 = "*" 26 | django-cors-headers = "*" 27 | django-filter = "*" 28 | 29 | [dev-packages] 30 | environ = "*" 31 | 32 | [requires] 33 | python_version = "3.8" 34 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn config.wsgi --log-file - -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 1) AisModa 2 | 3 | AisModa es una aplicación web Marketplace que tiene el objetivo de poder comprar y vender artículos de moda. La aplicación está desarrollada principalmente por la tecnología Django. 4 | [View Live](https://django-ecommerce-v1.herokuapp.com/) 5 | 6 | ## 2) Equipo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 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 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |

Nombre

Rol

Stack

Residencia

LinkedIn

GitHub

Arturo Martínez PachecoBackEndPython, django, postgres, github actions, beautiful soupMéxicoArturo Arturomtz8
David MoránBackEndPython, FastAPI, PostgresSQL, Javascript, git, Flask, Linux, CSS, Diseño de Base de datosPanamáDavid davidmr27
Fernando GuerreroBackEndPython, Django, Docker, Bash, JS, ReactMéxicoFernando devrrior
Angel RieraFull StackPython, django, PostgresSQL, tailwind, bootstrap, CSS, javascript, react, vue, dockerVenezuelaAngel RagAndRoll
Danny SolanoFrontEndHTML,CSS, JS, REACT, NODE, PHP, PYTHON,Tailwind, bootstrap, sass EcuadorDanny Drastick17
Oriana PellegriniTeam leader - Scrum MasterJava 8, Spring Framework, Thymeleaf, HTML,CSS, Bootstrap, Git y GithubArgentinaOriana Oriana10
72 | 73 | ## 2.2) Metodologia de trabajo 74 | 75 | Metodología Ágiles orientada a Scrum y Kanban. Haciendo uso de roles, reuniones y gestión de tareas. 76 | 77 | ## 3) User Story 78 | Los usuarios podrán: registrarse y comprar productos. Podrán registrarse como vendedores para poner en venta productos y recibir remuneración por la compra de los mismos. 79 | 80 | ## 4) Páginas y Secciones 81 | 82 | 1. Home page 83 | 1. productos 84 | 2. buscador 85 | 3. categorías 86 | 4. footer 87 | 1. recursos 88 | 2. redes sociales 89 | 3. legalidad 90 | 4. form de contacto 91 | 3. Login 92 | 4. Registro de Usuarios 93 | 5. Panel de usuarios 94 | 1. datos de usuarios 95 | 2. panel de tienda 96 | 3. historial productos comprados 97 | 4. carrito de compras 98 | 5. registro de vendedores 99 | 6. panel de tienda 100 | 101 | ## 5) Stack de Desarrollo 102 | 103 | - Backend: 104 | Django 105 | - Relación Base de Datos 106 | PostgresSql 107 | - Frontend: JavaScript / Tailwind / SASS 108 | - Entornos virtualels: 109 | Virtualenv 110 | Pipenv 111 | Devcointainer 112 | - Testing: 113 | Github Actions 114 | TestCase 115 | Isort 116 | Black 117 | Flake8 118 | 119 | ## 6) Funcionalidades 120 | 121 | - Registro de usuarios 122 | - Registro de vendedores 123 | - Post Productos 124 | - Categorías 125 | - Precio 126 | - Buscador 127 | - Productos similares 128 | - Stock de productos 129 | - Productos destacados 130 | - Agregar productos al carrito 131 | - Remuneración a cada vendedor por cada producto del carrito 132 | 133 | ## 7) Herramientas 134 | 135 | ### 7.2) Pip install 136 | 137 | ##### Option 1 138 | ``` 139 | $ sudo apt update 140 | $ sudo apt install python3-venv python3-pip 141 | ``` 142 | ##### Option 2 143 | ``` 144 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py ⇒ descarga paquetes iniciales 145 | python get-pip.py 146 | python -m pip install --upgrade pip 147 | ``` 148 | 149 | ### 7.3) Install Postgres SQl 150 | ``` 151 | $ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 152 | $ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 153 | $ sudo apt-get update 154 | $ sudo apt-get -y install postgresql postgresql-contrib 155 | $ sudo apt-get install libpq-dev 156 | ``` 157 | 158 | ### 7.4) Execute Postgresql 159 | ``` 160 | $ sudo service postgresql status 161 | $ sudo service postgresql start 162 | ``` 163 | 164 | ### 7.5) Create Database and Password 165 | ``` 166 | sudo -u postgres psql // start console 167 | ``` 168 | ``` 169 | postgres=# alter user Postgres with password 'newPasword'; 170 | postgres=# CREATE DATABASE name 171 | ``` 172 | - set this dates in your .env 173 | POSTGRES_NAME=name 174 | POSTGRES_PASSWORD=newPasword 175 | 176 | ### 7.6) Install Environment Pipenv 177 | ``` 178 | python3 -m pip install pipenv 179 | ``` 180 | 181 | ### 7.7) Activate Virtual Environment 182 | ``` 183 | pipenv shell or python3 -m pipenv shell 184 | pipenv install 185 | pipenv lock -r 186 | ``` 187 | 188 | ### Get Server Run 189 | ``` 190 | python manage.py runserver 191 | ``` 192 | 193 | ## 8) Docker 194 | 195 | [Ejecución del proyecto en docker como entorno de desarrollo](./readme/Docker_guide.md) 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | Cart : 3 | - [] Cart_iten images 4 | - [] agregar varios productos con descripciones y imagenes elegantes 5 | - [] imagenes de productos 6 | - [] view de los formularios 7 | - add producto 8 | - become vendor 9 | - -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/config/__init__.py -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /config/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | Generated by 'django-admin startproject' using Django 3.2.7. 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/3.2/topics/settings/ 6 | For the full list of settings and their values, see 7 | https://docs.djangoproject.com/en/3.2/ref/settings/ 8 | """ 9 | # en caso de error 111 10 | # sudo apt-get install redis-server 11 | # sudo service redis-server start 12 | 13 | 14 | import os 15 | import sys 16 | from datetime import timedelta 17 | from pathlib import Path 18 | 19 | import environ 20 | 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | 25 | env = environ.Env() 26 | 27 | environ.Env.read_env(os.path.join(BASE_DIR, ".env")) # add this 28 | 29 | 30 | DEBUG = env.bool("DEBUG", True) 31 | 32 | 33 | SECRET_KEY = env("DJANGO_KEY") 34 | 35 | 36 | ALLOWED_HOSTS = [ 37 | "localhost", 38 | "127.0.0.1", 39 | "http://127.0.0.1:8000/", 40 | "django-ecommerce-v1.herokuapp.com", 41 | "test-marketplace-django.herokuapp.com", 42 | ] 43 | 44 | 45 | CSRF_TRUSTED_ORIGINS = ["https://test-marketplace-django.herokuapp.com", "https://django-ecommerce-v1.herokuapp.com"] 46 | 47 | 48 | # Application definitionds 49 | 50 | INSTALLED_APPS = [ 51 | "django.contrib.admin", 52 | "django.contrib.auth", 53 | "django.contrib.contenttypes", 54 | "django.contrib.sessions", 55 | "django.contrib.messages", 56 | "django.contrib.staticfiles", 57 | # local apps 58 | "ecommerce", 59 | "users", 60 | "shopping_cart", 61 | "payment", 62 | # 3rd apps 63 | "stripe", 64 | "storages", 65 | "rest_framework", 66 | "drf_yasg2", 67 | "corsheaders", 68 | "django_filters", 69 | ] 70 | 71 | MIDDLEWARE = [ 72 | "django.middleware.security.SecurityMiddleware", 73 | "whitenoise.middleware.WhiteNoiseMiddleware", 74 | "django.contrib.sessions.middleware.SessionMiddleware", 75 | "corsheaders.middleware.CorsMiddleware", 76 | "django.middleware.common.CommonMiddleware", 77 | "django.middleware.csrf.CsrfViewMiddleware", 78 | "django.contrib.auth.middleware.AuthenticationMiddleware", 79 | "django.contrib.messages.middleware.MessageMiddleware", 80 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 81 | ] 82 | 83 | CORS_ALLOWED_ORIGINS = ["http://localhost:8000", "http://localhost:3000", "https://django-ecommerce-v1.herokuapp.com"] 84 | 85 | INTERNAL_IPS = ["127.0.0.1"] 86 | 87 | ROOT_URLCONF = "config.urls" 88 | 89 | TEMPLATES = [ 90 | { 91 | "BACKEND": "django.template.backends.django.DjangoTemplates", 92 | "DIRS": [BASE_DIR, "templates"], 93 | "APP_DIRS": True, 94 | "OPTIONS": { 95 | "context_processors": [ 96 | "django.template.context_processors.debug", 97 | "django.template.context_processors.request", 98 | "django.contrib.auth.context_processors.auth", 99 | "django.contrib.messages.context_processors.messages", 100 | "ecommerce.context_processors.menu_categories", 101 | ] 102 | }, 103 | } 104 | ] 105 | 106 | WSGI_APPLICATION = "config.wsgi.application" 107 | 108 | 109 | # Database 110 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 111 | 112 | if DEBUG: 113 | DATABASES = { 114 | "default": { 115 | "ENGINE": "django.db.backends.postgresql_psycopg2", 116 | "NAME": env("POSTGRES_NAME"), 117 | "USER": env("POSTGRES_USER"), 118 | "PASSWORD": env("POSTGRES_PASSWORD"), 119 | "HOST": env("POSTGRES_HOST"), 120 | "PORT": "5432", 121 | } 122 | } 123 | else: 124 | DATABASES = { 125 | "default": { 126 | "ENGINE": "django.db.backends.postgresql_psycopg2", 127 | "NAME": env("NAME_DB_HEROKU"), 128 | "USER": env("USER_DB_HEROKU"), 129 | "PASSWORD": env("PASSWORD_DB_HEROKU"), 130 | "HOST": env("HOST_DB_HEROKU"), 131 | "PORT": "5432", 132 | } 133 | } 134 | 135 | 136 | if os.environ.get("GITHUB_WORKFLOW"): 137 | DATABASES = { 138 | "default": { 139 | "ENGINE": "django.db.backends.postgresql", 140 | "NAME": "github_actions", 141 | "USER": "postgres", 142 | "PASSWORD": "postgres", 143 | "HOST": "127.0.0.1", 144 | "PORT": "5432", 145 | } 146 | } 147 | 148 | if "test" in sys.argv: 149 | DATABASES["default"] = {"ENGINE": "django.db.backends.sqlite3", "NAME": "mydatabase"} 150 | 151 | 152 | # user personalizado 153 | AUTH_USER_MODEL = "users.CustomUser" 154 | 155 | AUTH_PASSWORD_VALIDATORS = [ 156 | {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, 157 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 158 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 159 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 160 | ] 161 | 162 | 163 | # Internationalization 164 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 165 | 166 | LANGUAGE_CODE = "es-MX" 167 | 168 | # TIME_ZONE = "America/Mexico_city" me causa confictos 169 | TIME_ZONE = "UTC" 170 | 171 | 172 | USE_I18N = True 173 | 174 | USE_L10N = True 175 | 176 | USE_TZ = True 177 | 178 | 179 | # Static files (CSS, JavaScript, Images) 180 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 181 | # this is used for internal use 182 | 183 | # static files config 184 | 185 | STATICFILES_STORAGE = "whitenoise.storage.CompressedStaticFilesStorage" 186 | 187 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "staticfiles")] 188 | 189 | STATIC_URL = "/static/" 190 | 191 | # django app related files, its used for external use 192 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 193 | 194 | # media config 195 | 196 | MEDIA_URL = "/media/" 197 | 198 | # user uploaded files 199 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 200 | 201 | 202 | # Default primary key field type 203 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 204 | 205 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 206 | 207 | # CACHE REDIS 208 | # CACHES = { 209 | # "default": { 210 | # "BACKEND": "django_redis.cache.RedisCache", 211 | # "LOCATION": "redis://127.0.0.1:6379/1", 212 | # "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, 213 | # "KEY_PREFIX": "ecomm", 214 | # } 215 | # } 216 | 217 | 218 | # Cache time to live is 15 minutes. 219 | CACHE_TTL = 60 * 15 220 | 221 | 222 | # para la consola 223 | # EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 224 | 225 | # para enviar realmente 226 | 227 | 228 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 229 | EMAIL_HOST = "smtp.gmail.com" 230 | # EMAIL_HOST_USER = env("EMAIL") 231 | # EMAIL_HOST_PASSWORD = env('GMAIL_TOKEN') 232 | EMAIL_PORT = 587 233 | EMAIL_USE_TLS = True 234 | 235 | LOGIN_REDIRECT_URL = "/" 236 | ACCOUNT_LOGOUT_REDIRECT_URL = "/" 237 | 238 | STRIPE_PUBLIC_KEY = env("STRIPE_PUBLIC_KEY") 239 | STRIPE_PRIVATE_KEY = env("STRIPE_PRIVATE_KEY") 240 | STRIPE_WEBHOOK_KEY = env("STRIPE_WEBHOOK_KEY") 241 | 242 | 243 | REST_FRAMEWORK = { 244 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", 245 | "PAGE_SIZE": 10, 246 | "DEFAULT_AUTHENTICATION_CLASSES": ( 247 | "rest_framework_simplejwt.authentication.JWTAuthentication", 248 | "rest_framework.authentication.SessionAuthentication", 249 | ), 250 | } 251 | 252 | SWAGGER_SETTINGS = { 253 | "SECURITY_DEFINITIONS": { 254 | "Basic": {"type": "basic"}, 255 | "Bearer": {"type": "apiKey", "name": "Authorization", "description": "SimpleJWT", "in": "header"}, 256 | } 257 | } 258 | 259 | 260 | SIMPLE_JWT = { 261 | "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), 262 | "REFRESH_TOKEN_LIFETIME": timedelta(days=1), 263 | "ROTATE_REFRESH_TOKENS": False, 264 | "BLACKLIST_AFTER_ROTATION": False, 265 | "UPDATE_LAST_LOGIN": False, 266 | "ALGORITHM": "HS256", 267 | "SIGNING_KEY": SECRET_KEY, 268 | "VERIFYING_KEY": None, 269 | "AUDIENCE": None, 270 | "ISSUER": None, 271 | "JWK_URL": None, 272 | "LEEWAY": 0, 273 | "AUTH_HEADER_TYPES": ("Bearer",), 274 | "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", 275 | "USER_ID_FIELD": "id", 276 | "USER_ID_CLAIM": "user_id", 277 | "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule", 278 | "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), 279 | "TOKEN_TYPE_CLAIM": "token_type", 280 | "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", 281 | "JTI_CLAIM": "jti", 282 | "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", 283 | "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), 284 | "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), 285 | } 286 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.urls import include, path 5 | from drf_yasg2 import openapi 6 | from drf_yasg2.views import get_schema_view 7 | from rest_framework import permissions 8 | 9 | urlpatterns = [ 10 | path("api/v1/", include("ecommerce.urls")), 11 | path("admin/", admin.site.urls), 12 | path("api/v1/", include("users.urls")), 13 | path("cart/", include("shopping_cart.urls")), 14 | path("payment/", include("payment.urls")), 15 | ] 16 | 17 | # Schema of Documentation API 18 | schema_view = get_schema_view( 19 | openapi.Info( 20 | title="Ecommerce API", 21 | default_version="v1", 22 | description="Rest API del Ecommerce ", 23 | contact=openapi.Contact(email="contact@snippets.local"), 24 | license=openapi.License(name="BSD License"), 25 | ), 26 | public=True, 27 | permission_classes=(permissions.AllowAny,), 28 | ) 29 | # swagger 30 | urlpatterns += [ 31 | path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"), # noqa E501 32 | path("docs/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), # noqa E501 33 | ] 34 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 35 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 36 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | db: 5 | restart: always 6 | #restart: unless-stopped 7 | image: postgres:14.1-alpine3.15 8 | volumes: 9 | - ./data/db:/var/lib/postgresql/data 10 | ports: 11 | - 5431:5432 # para que sea visible en nuestro entorno local 12 | environment: 13 | - POSTGRES_NAME=postgres 14 | - POSTGRES_USER=postgres 15 | - POSTGRES_PASSWORD=postgres 16 | 17 | django: 18 | restart: always 19 | container_name: django 20 | build: 21 | context: . 22 | dockerfile: ./local.Dockerfile 23 | command: > 24 | sh -c "python manage.py migrate && python manage.py runserver 0.0.0.0:8000" 25 | ports: 26 | - "8000:8000" 27 | environment: 28 | - POSTGRES_NAME=postgres 29 | - POSTGRES_USER=postgres 30 | - POSTGRES_PASSWORD=postgres 31 | - POSTGRES_HOST=db 32 | - ALLOWED_HOSTS=127.0.0.1, localhost 33 | - DJANGO_KEY=123 34 | - STRIPE_PUBLIC_KEY=env("STRIPE_PUBLIC_KEY") 35 | - STRIPE_PRIVATE_KEY=env("STRIPE_PRIVATE_KEY") 36 | - STRIPE_WEBHOOK_KEY=env("STRIPE_WEBHOOK_KEY") 37 | 38 | volumes: 39 | - .:/app 40 | depends_on: 41 | - db 42 | -------------------------------------------------------------------------------- /ecommerce/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/ecommerce/__init__.py -------------------------------------------------------------------------------- /ecommerce/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category, Image, Product 4 | 5 | 6 | class ProductAdmin(admin.ModelAdmin): 7 | prepopulated_fields = {"slug": ("title",)} 8 | list_display = ("title", "slug", "category") 9 | 10 | 11 | class CategoryAdmin(admin.ModelAdmin): 12 | prepopulated_fields = {"slug": ("title",)} 13 | list_display = ("title", "slug") 14 | 15 | 16 | class ImageAdmin(admin.ModelAdmin): 17 | list_display = ("product",) 18 | 19 | 20 | admin.site.register(Product, ProductAdmin) 21 | admin.site.register(Image, ImageAdmin) 22 | admin.site.register(Category, CategoryAdmin) 23 | -------------------------------------------------------------------------------- /ecommerce/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EcommerceConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "ecommerce" 7 | -------------------------------------------------------------------------------- /ecommerce/context_processors.py: -------------------------------------------------------------------------------- 1 | from .models import Category 2 | 3 | 4 | def menu_categories(request): # NOMBRE DE LA FUNCION DEBEN SER IGUALES 5 | categories = Category.objects.all() 6 | 7 | return {"menu_categories": categories} # NOMBRE DEL OBJETO LLAMABLE DEBEN SER IGUALES 8 | -------------------------------------------------------------------------------- /ecommerce/custom_permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import SAFE_METHODS, BasePermission 2 | 3 | 4 | class IsStaffOrReadOnly(BasePermission): 5 | """ 6 | The request is authenticated as staff, or is a read-only request. 7 | """ 8 | 9 | def has_permission(self, request, view): 10 | return bool(request.method in SAFE_METHODS or request.user.is_staff) 11 | 12 | 13 | class IsOwnerOrReadOnly(BasePermission): 14 | """ 15 | Object-level permission to only allow owners of an object to edit it. 16 | Assumes the model instance has a `seller` attribute. 17 | """ 18 | 19 | def has_object_permission(self, request, view, obj): 20 | if request.method in SAFE_METHODS: 21 | return True 22 | try: 23 | return obj.seller.profile.email == request.user.email 24 | except Exception as e: 25 | print(e) 26 | return False 27 | -------------------------------------------------------------------------------- /ecommerce/decorators.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import redirect 2 | from rest_framework.exceptions import PermissionDenied 3 | 4 | from ecommerce.models import Product 5 | 6 | 7 | def unauthenticated_user(function): 8 | def wrapper_function(request, *args, **kwargs): 9 | if request.user.is_authenticated: 10 | return redirect("ecommerce:home") 11 | else: 12 | return function(request, *args, **kwargs) 13 | 14 | return wrapper_function 15 | 16 | 17 | def user_is_seller(function): 18 | def wrapper_function(request, *args, **kwargs): 19 | print(kwargs) 20 | product = Product.objects.get(slug=kwargs["slug"]) 21 | # seller = Seller.objects.get(profile__email=request.user.email) 22 | # print(seller.profile.email) 23 | # print(request.user.email) 24 | # print(product.seller.profile.email) 25 | 26 | if request.method == "GET": 27 | return function(request, *args, **kwargs) 28 | # en caso que sea otro verbo 29 | try: 30 | if request.user.email == product.seller.profile.email: 31 | return function(request, *args, **kwargs) 32 | raise PermissionDenied("No tienes los permisos para editar este producto") 33 | except Exception as e: 34 | print(e) 35 | raise PermissionDenied("No tienes los permisos para editar este producto") 36 | 37 | return wrapper_function 38 | -------------------------------------------------------------------------------- /ecommerce/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters.rest_framework import FilterSet 2 | 3 | from ecommerce.models import Product 4 | 5 | 6 | class ProductFilter(FilterSet): 7 | class Meta: 8 | model = Product 9 | fields = {"category": ["exact"], "seller": ["exact"], "price": ["gt", "lt"]} 10 | -------------------------------------------------------------------------------- /ecommerce/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Image, Product 4 | 5 | 6 | class ProductForm(forms.ModelForm): 7 | description = forms.CharField(widget=forms.Textarea(attrs={"class": "form-control w-100", "rows": "3"})) 8 | 9 | class Meta: 10 | model = Product 11 | fields = ["title", "description", "price", "category", "stock"] 12 | 13 | # def clean_title(self, *args, **kwargs): 14 | # title = self.cleaned_data.get('title') 15 | # if not title.startswith('Shein'): 16 | # raise forms.ValidationError('Tu producto debe empezar con Shein') 17 | # return title 18 | 19 | def clean_price(self, *args, **kwargs): 20 | price = self.cleaned_data.get("price") 21 | if price < 0: 22 | raise forms.ValidationError("No puedes ingresar números negativos.") 23 | return price 24 | 25 | 26 | class ImageForm(forms.ModelForm): 27 | image_location = forms.ImageField(required=True, label="Select an image file") 28 | 29 | class Meta: 30 | model = Image 31 | fields = ["image_location"] 32 | -------------------------------------------------------------------------------- /ecommerce/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-18 01:13 2 | 3 | import django.core.validators 4 | import django.db.models.deletion 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Category", 17 | fields=[ 18 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 19 | ("title", models.CharField(max_length=255, unique=True)), 20 | ("slug", models.SlugField(max_length=255)), 21 | ], 22 | options={"verbose_name": "category", "verbose_name_plural": "categories"}, 23 | ), 24 | migrations.CreateModel( 25 | name="Image", 26 | fields=[ 27 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 28 | ("image_location", models.ImageField(blank=True, null=True, upload_to="media/products/")), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name="Product", 33 | fields=[ 34 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 35 | ("title", models.CharField(max_length=250)), 36 | ("description", models.TextField()), 37 | ("price", models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(50)])), 38 | ("slug", models.SlugField(blank=True, null=True)), 39 | ("stock", models.PositiveIntegerField(default=0)), 40 | ("is_available", models.BooleanField(default=False)), 41 | ( 42 | "category", 43 | models.ForeignKey( 44 | blank=True, 45 | null=True, 46 | on_delete=django.db.models.deletion.CASCADE, 47 | related_name="products", 48 | to="ecommerce.category", 49 | ), 50 | ), 51 | ], 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /ecommerce/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-18 01:13 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("ecommerce", "0001_initial"), ("users", "0001_initial")] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="product", 16 | name="seller", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.CASCADE, 21 | related_name="products", 22 | to="users.seller", 23 | ), 24 | ), 25 | migrations.AddField( 26 | model_name="image", 27 | name="product", 28 | field=models.ForeignKey( 29 | on_delete=django.db.models.deletion.CASCADE, related_name="product_images", to="ecommerce.product" 30 | ), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /ecommerce/migrations/0003_alter_category_slug_alter_category_title_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-19 04:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("ecommerce", "0002_initial")] 9 | 10 | operations = [ 11 | migrations.AlterField(model_name="category", name="slug", field=models.SlugField(max_length=100, unique=True)), 12 | migrations.AlterField( 13 | model_name="category", name="title", field=models.CharField(max_length=100, unique=True) 14 | ), 15 | migrations.AlterField( 16 | model_name="product", name="slug", field=models.SlugField(blank=True, null=True, unique=True) 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ecommerce/migrations/0004_alter_category_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-19 05:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("ecommerce", "0003_alter_category_slug_alter_category_title_and_more")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="category", 13 | name="slug", 14 | field=models.SlugField(blank=True, max_length=100, null=True, unique=True), 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /ecommerce/migrations/0005_alter_category_slug_alter_product_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-06-16 01:22 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("ecommerce", "0004_alter_category_slug")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="category", 14 | name="slug", 15 | field=models.SlugField(default="djangodbmodelsfieldscharfield", max_length=100, unique=True), 16 | ), 17 | migrations.AlterField( 18 | model_name="product", 19 | name="category", 20 | field=models.ForeignKey( 21 | default=1, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | related_name="products", 24 | to="ecommerce.category", 25 | ), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /ecommerce/migrations/0006_alter_product_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-06-16 04:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("ecommerce", "0005_alter_category_slug_alter_product_category")] 9 | 10 | operations = [ 11 | migrations.AlterField(model_name="product", name="title", field=models.CharField(max_length=250, unique=True)) 12 | ] 13 | -------------------------------------------------------------------------------- /ecommerce/migrations/0007_alter_image_image_location.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.6 on 2022-09-10 16:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("ecommerce", "0006_alter_product_title")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="image", 13 | name="image_location", 14 | field=models.ImageField(blank=True, null=True, upload_to="products/"), 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /ecommerce/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/ecommerce/migrations/__init__.py -------------------------------------------------------------------------------- /ecommerce/models.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import MinValueValidator 2 | from django.db import models 3 | from django.template.defaultfilters import slugify 4 | 5 | from users.models import Seller 6 | 7 | 8 | class Category(models.Model): 9 | title = models.CharField(max_length=100, unique=True) 10 | slug = models.SlugField(max_length=100, unique=True, default=slugify(title)) 11 | 12 | class Meta: 13 | verbose_name = "category" 14 | verbose_name_plural = "categories" 15 | 16 | def __str__(self): 17 | return self.title 18 | 19 | def save(self, *args, **kwargs): 20 | self.slug = slugify(self.title) 21 | super(Category, self).save(*args, **kwargs) 22 | 23 | 24 | class Product(models.Model): 25 | category = models.ForeignKey(Category, related_name="products", on_delete=models.CASCADE, default=1) 26 | seller = models.ForeignKey( 27 | Seller, related_name="products", on_delete=models.CASCADE, null=True, blank=True 28 | ) # momentaneo, hay que modificar tests!! 29 | title = models.CharField(max_length=250, unique=True) 30 | description = models.TextField() 31 | price = models.PositiveIntegerField(validators=[MinValueValidator(50)]) 32 | slug = models.SlugField(null=True, blank=True, unique=True) 33 | stock = models.PositiveIntegerField(default=0) 34 | is_available = models.BooleanField(default=False) 35 | 36 | def __str__(self): 37 | return self.title 38 | 39 | def was_created(self): 40 | if self.price < 50: 41 | return False 42 | return True 43 | 44 | def save(self, *args, **kwargs): 45 | self.is_available = False 46 | self.slug = slugify(self.title) 47 | if self.stock > 0: 48 | self.is_available = True 49 | super(Product, self).save(*args, **kwargs) 50 | 51 | 52 | class Image(models.Model): 53 | product = models.ForeignKey(Product, related_name="product_images", on_delete=models.CASCADE) 54 | image_location = models.ImageField(upload_to="products/", null=True, blank=True) 55 | -------------------------------------------------------------------------------- /ecommerce/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from ecommerce.models import Category, Image, Product 4 | from users.models import Seller 5 | 6 | 7 | class ModelListField(serializers.ListField): 8 | def to_representation(self, data): 9 | """ 10 | List of object instances -> List of dicts of primitive datatypes. 11 | """ 12 | return [self.child.to_representation(item) if item is not None else None for item in data.all()] 13 | 14 | 15 | class ImageSerializer(serializers.ModelSerializer): 16 | class Meta: 17 | model = Image 18 | fields = ["image_location"] 19 | 20 | 21 | class ProductSerializer(serializers.ModelSerializer): 22 | category = serializers.CharField(source="category.title", read_only=False) 23 | seller = serializers.CharField(source="seller.seller_name", read_only=True) 24 | 25 | images = ModelListField(child=serializers.ImageField(), allow_empty=False, min_length=1, write_only=True) 26 | product_images = ImageSerializer(many=True, read_only=True) 27 | 28 | class Meta: 29 | model = Product 30 | fields = ( 31 | "id", 32 | "slug", 33 | "title", 34 | "description", 35 | "product_images", 36 | "price", 37 | "category", 38 | "stock", 39 | "seller", 40 | "images", 41 | ) 42 | 43 | extra_kwargs = { 44 | "id": {"read_only": True}, 45 | "slug": {"read_only": True}, 46 | "title": {"required": True}, 47 | "category": {"required": True}, 48 | "description": {"required": True}, 49 | "price": {"required": True}, 50 | "stock": {"required": True}, 51 | } 52 | 53 | def create(self, validated_data): 54 | print(validated_data) 55 | category = validated_data.pop("category") 56 | product_images = validated_data.pop("images") 57 | category_title = Category.objects.get(title=category["title"]) 58 | email = self.context["request"].user 59 | try: 60 | seller_email = Seller.objects.get(profile__email=email) 61 | new_product = Product.objects.create(**validated_data, category=category_title, seller=seller_email) 62 | for image in product_images: 63 | Image.objects.create(product=new_product, image_location=image) 64 | return new_product 65 | except Seller.DoesNotExist: 66 | raise serializers.ValidationError("Crea antes una tienda para poder publicar productos") 67 | 68 | def update(self, instance, validated_data): 69 | category = validated_data.pop("category") 70 | category_title = Category.objects.get(title=category["title"]) 71 | print(category_title) 72 | instance.category = category_title 73 | return super().update(instance, validated_data) 74 | 75 | 76 | class CategorySerializer(serializers.ModelSerializer): 77 | products = ProductSerializer(many=True, read_only=True) 78 | 79 | class Meta: 80 | model = Category 81 | fields = ["id", "slug", "title", "products"] 82 | 83 | extra_kwargs = {"id": {"read_only": True}, "slug": {"read_only": True}} 84 | -------------------------------------------------------------------------------- /ecommerce/tests.py: -------------------------------------------------------------------------------- 1 | # not working yet 2 | # from django.urls import reverse_lazy 3 | # from rest_framework import status 4 | # from rest_framework.test import APITestCase 5 | 6 | # from users.models import CustomUser, Seller 7 | 8 | # import tempfile 9 | # from PIL import Image 10 | 11 | # class ImageTest(APITestCase): 12 | # def setUp(self) -> None: 13 | # # Create a user 14 | # self.user = CustomUser.objects.create_user( 15 | # email="mark@mail.com", first_name="Mark", last_name="Bruen", password="123456" 16 | # ) 17 | 18 | # # Create a seller 19 | # self.seller = Seller.objects.create(seller_name="Mark Store", profile=self.user) 20 | 21 | # # Create a token 22 | # url = reverse_lazy("users:login") 23 | # response = self.client.post( 24 | # url, {"email": "mark@mail.com", "password": "123456"}, format="json" 25 | # ) 26 | 27 | # # Set the token in the header 28 | # self.token = response.data["access"] 29 | # self.client.credentials(HTTP_AUTHORIZATION="Bearer " + self.token) 30 | 31 | # # Create an image 32 | # self.tmp_file = tempfile.NamedTemporaryFile(suffix='.png') 33 | # image = Image.new('RGB', (100, 100)) 34 | # image.save(self.tmp_file.name) 35 | # self.params = { 36 | 37 | # "title": "prueba", 38 | # "description": "prueba", 39 | # "product_images": self.tmp_file, 40 | # "price": 1900, 41 | # "category": "prueba", 42 | # "stock": 3 43 | # } 44 | # self.HTTP_HOST = "localhost:8000" 45 | 46 | # def test_valid_authenticated_post_returns_201(self): 47 | # url = reverse_lazy("ecommerce:product-list") 48 | # print(self.params) 49 | # print(self.HTTP_HOST) 50 | # response = self.client.post( 51 | # url, { 52 | # "title": "prueba", 53 | # "description": "prueba", 54 | # "product_images": self.tmp_file, 55 | # "price": 1900, 56 | # "category": "prueba", 57 | # "stock": 3 58 | # }, format='multipart', HTTP_HOST=self.HTTP_HOST) 59 | 60 | # self.assertEqual(response.status_code, status.HTTP_201_CREATED) 61 | -------------------------------------------------------------------------------- /ecommerce/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from ecommerce import views 4 | 5 | app_name = "ecommerce" 6 | 7 | urlpatterns = [ 8 | path("", views.APIRootView.as_view()), 9 | path("products/", views.ProductList.as_view(), name="product-list"), 10 | path("products/", views.ProductDetail.as_view(), name="product-detail"), 11 | path("categories/", views.CategoryList.as_view(), name="category-list"), 12 | path("categories/", views.CategoryDetail.as_view(), name="category-detail"), 13 | ] 14 | -------------------------------------------------------------------------------- /ecommerce/views.py: -------------------------------------------------------------------------------- 1 | from django_filters.rest_framework import DjangoFilterBackend 2 | from rest_framework import status 3 | from rest_framework.filters import OrderingFilter, SearchFilter 4 | from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView 5 | from rest_framework.response import Response 6 | from rest_framework.reverse import reverse 7 | from rest_framework.views import APIView 8 | 9 | from ecommerce.custom_permissions import IsOwnerOrReadOnly, IsStaffOrReadOnly 10 | from ecommerce.filters import ProductFilter 11 | from ecommerce.models import Category, Product 12 | from ecommerce.serializers import CategorySerializer, ProductSerializer 13 | 14 | 15 | class APIRootView(APIView): 16 | def get(self, request): 17 | data = { 18 | "ecommerce": reverse("ecommerce:product-list", request=request), 19 | "category": reverse("ecommerce:category-list", request=request), 20 | } 21 | return Response(data) 22 | 23 | 24 | class ProductList(ListCreateAPIView): 25 | queryset = Product.objects.all().order_by("id") 26 | serializer_class = ProductSerializer 27 | permission_classes = [IsStaffOrReadOnly] 28 | filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] 29 | filterset_class = ProductFilter 30 | search_fields = ["title", "description", "category__title"] 31 | ordering_fields = ["price", "stock", "category"] 32 | 33 | 34 | class ProductDetail(RetrieveUpdateDestroyAPIView): 35 | serializer_class = ProductSerializer 36 | queryset = Product.objects.all() 37 | lookup_field = "slug" 38 | permission_classes = [IsOwnerOrReadOnly] 39 | 40 | 41 | class CategoryList(ListCreateAPIView): 42 | serializer_class = CategorySerializer 43 | queryset = Category.objects.all().order_by("id") 44 | permission_classes = [IsStaffOrReadOnly] 45 | 46 | 47 | class CategoryDetail(RetrieveUpdateDestroyAPIView): 48 | serializer_class = CategorySerializer 49 | queryset = Category.objects.all() 50 | lookup_field = "slug" 51 | permission_classes = [IsStaffOrReadOnly] 52 | 53 | def destroy(self, request, *args, **kwargs): 54 | instance = self.get_object() 55 | if instance.products.count() > 0: 56 | return Response({"error": "La colección no se puede eliminar porque tiene productos"}) 57 | self.perform_destroy(instance) 58 | return Response(status=status.HTTP_204_NO_CONTENT) 59 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python manage.py collectstatic --noinput 4 | python manage.py migrate 5 | 6 | gunicorn config.wsgi:application --bind 0.0.0.0:$PORT 7 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.8.2", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.2.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-hook-form": "^7.33.1", 13 | "react-icons": "^4.3.1", 14 | "react-redux": "^8.0.2", 15 | "react-router-dom": "^6.3.0", 16 | "react-scripts": "5.0.1", 17 | "web-vitals": "^2.1.4" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "autoprefixer": "^10.4.7", 45 | "postcss": "^8.4.13", 46 | "tailwindcss": "^3.0.24" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from "react-router-dom"; 2 | import Layout from "./components/Layout"; 3 | import PageNotFound from "./components/PageNotFound"; 4 | import About from "./pages/About/About"; 5 | import Contact from "./pages/Contact/Contact"; 6 | import Home from "./pages/Home/Home"; 7 | import Header from "./components/Header"; 8 | import Login from "./pages/Login/Login"; 9 | import Product from "./pages/Product/Product"; 10 | import Products from "./pages/Products/Products"; 11 | import Profile from "./pages/Profile/Profile"; 12 | import ProfileSeller from "./pages/ProfileSeller/ProfileSeller"; 13 | 14 | function App() { 15 | return ( 16 | <> 17 | 18 | } /> 19 | }> 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /frontend/src/assets/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/img/media/01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/01.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/02.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/03.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/04.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/06.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/background.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/bolso.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/bolso.webp -------------------------------------------------------------------------------- /frontend/src/assets/img/media/card1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/card1.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/card2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/card2.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/card3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/card3.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/correo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/correo.webp -------------------------------------------------------------------------------- /frontend/src/assets/img/media/image 52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/image 52.png -------------------------------------------------------------------------------- /frontend/src/assets/img/media/img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/img3.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/logo.png -------------------------------------------------------------------------------- /frontend/src/assets/img/media/mano.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/assets/img/media/mano.jpg -------------------------------------------------------------------------------- /frontend/src/components/Cart.js: -------------------------------------------------------------------------------- 1 | function Cart() { 2 | return
Cart
; 3 | } 4 | 5 | export default Cart; 6 | -------------------------------------------------------------------------------- /frontend/src/components/Carusel.js: -------------------------------------------------------------------------------- 1 | import {useRef} from "react"; 2 | import img1 from "../assets/img/media/03.jpg"; 3 | const Carousel = () => { 4 | const carusel = useRef(null) 5 | const moveSlidertoLeft = () =>{ 6 | const caruselE = carusel.current 7 | caruselE.scrollLeft += -Math.floor((caruselE.clientWidth)/2); 8 | } 9 | const moveSlidertoRight = () =>{ 10 | const caruselE = carusel.current 11 | caruselE.scrollLeft += Math.floor((caruselE.clientWidth)/2); 12 | } 13 | return ( 14 |
15 |
16 | 19 |
20 |
21 |
22 | product 23 |

24 | Im a product 25 |

26 |

27 | $100.00 28 |

29 |
30 |
31 | product 32 |

33 | Im a product 34 |

35 |

36 | $100.00 37 |

38 |
39 |
40 | product 41 |

42 | Im a product 43 |

44 |

45 | $100.00 46 |

47 |
48 |
49 | product 50 |

51 | Im a product 52 |

53 |

54 | $100.00 55 |

56 |
57 |
58 | product 59 |

60 | Im a product 61 |

62 |

63 | $100.00 64 |

65 |
66 |
67 | product 68 |

69 | Im a product 70 |

71 |

72 | $100.00 73 |

74 |
75 |
76 |
77 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default Carousel; 86 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => { 4 | return ( 5 | 74 | ); 75 | }; 76 | 77 | export default Footer; 78 | -------------------------------------------------------------------------------- /frontend/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import {useState} from 'react' 2 | import {useSelector} from 'react-redux' 3 | import Logo from '../assets/img/media/logo.png' 4 | 5 | const Header = () => { 6 | const { user } = useSelector(state => state.userReducer) 7 | const [open, setOpen] = useState(false) 8 | const [showmenu, setShowMenu] = useState(false) 9 | const [y, setY] = useState(0) 10 | //console.log(y) 11 | window.onscroll = function() { 12 | let scroll = window.scrollY; 13 | setY(scroll) 14 | if(y > Number(window.innerHeight)-200){ 15 | setOpen(true) 16 | } 17 | if(y <= 300) { 18 | setOpen(false) 19 | } 20 | if(open === false) { 21 | setShowMenu(false) 22 | } 23 | } 24 | return ( 25 | <> 26 |
27 |
28 |
29 |
32 | 35 |
36 |
37 | Adalane. 38 |
setShowMenu(!showmenu)} className='relative w-16 h-16 flex md:hidden flex-col gap-3 py-3 px-1 items-center justify-center rounded-xl group'> 39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | {user ?Login:{user.name}} 47 | 48 |
49 |
50 |
51 | 57 |
58 |
59 | ) 60 | } 61 | 62 | export default Header -------------------------------------------------------------------------------- /frontend/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | import Footer from "./Footer"; 4 | import Header from "./Header"; 5 | 6 | function Layout() { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default Layout; 17 | -------------------------------------------------------------------------------- /frontend/src/components/PageNotFound.js: -------------------------------------------------------------------------------- 1 | function PageNotFound() { 2 | return
Page not found
; 3 | } 4 | 5 | export default PageNotFound; 6 | -------------------------------------------------------------------------------- /frontend/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | 2 | const Spinner = () => { 3 | return ( 4 |
5 |
6 |
7 | ) 8 | } 9 | 10 | export default Spinner -------------------------------------------------------------------------------- /frontend/src/fonts.css: -------------------------------------------------------------------------------- 1 | @import url('http://fonts.cdnfonts.com/css/avenir-lt-std'); 2 | @import url('http://fonts.cdnfonts.com/css/didot'); -------------------------------------------------------------------------------- /frontend/src/hooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/hooks/.gitkeep -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .parallax{ 6 | background: #fff fixed no-repeat 50% 50%; 7 | } 8 | 9 | .parallax2{ 10 | background-size:cover; 11 | background-position:50% 50%; 12 | background-attachment:fixed; 13 | background-repeat:no-repeat; 14 | } 15 | 16 | @media (min-width: 640px){ 17 | .parallax2{ 18 | background-size:50% 100%; 19 | background-position:100% 0%; 20 | } 21 | 22 | } 23 | 24 | .background{ 25 | background: rgba(0, 0, 0, 0.4); 26 | background-blend-mode: darken; 27 | } 28 | 29 | .background2{ 30 | background: rgba(0, 0, 0, 0.2); 31 | background-blend-mode: darken; 32 | } 33 | 34 | .scroll::-webkit-scrollbar{ 35 | display: none; 36 | } 37 | 38 | body { 39 | margin: 0; 40 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 41 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 42 | sans-serif; 43 | -webkit-font-smoothing: antialiased; 44 | -moz-osx-font-smoothing: grayscale; 45 | } 46 | 47 | code { 48 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 49 | monospace; 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import "./fonts.css" 5 | import App from "./App"; 6 | import { Provider } from 'react-redux' 7 | import store from "./store"; 8 | import { BrowserRouter } from "react-router-dom"; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById("root")); 11 | root.render( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | -------------------------------------------------------------------------------- /frontend/src/pages/About/About.js: -------------------------------------------------------------------------------- 1 | function About() { 2 | return
About
; 3 | } 4 | 5 | export default About; 6 | -------------------------------------------------------------------------------- /frontend/src/pages/Contact/Contact.js: -------------------------------------------------------------------------------- 1 | function Contact() { 2 | return ( 3 |
4 |

Get in touch

5 |
6 |

Custom Service

7 |
8 |
9 |

Flagship Store

10 |

500 Terry Francisco St

11 |

San Francisco, CA 94158

12 |
13 |
14 |

Opening Hours

15 |

Monday-Friday

16 |

9:00am - 7:00pm EST

17 |
18 |
19 |

Contact Us

20 |

1-800-000-0000

21 |

info@mysite.com

22 |
23 |
24 |
25 |

Inquiries

26 |

for questions regarding our products and services

27 |

you can also contact us by filling out the form below

28 |
29 |
30 |
31 |
32 |
33 | 39 | 45 |
46 | 53 | 60 | 66 | 71 |
72 |
73 |
74 | ); 75 | } 76 | 77 | export default Contact; 78 | -------------------------------------------------------------------------------- /frontend/src/pages/Login/Login.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { useForm } from 'react-hook-form' 3 | import { useSelector, useDispatch } from "react-redux" 4 | import { createUser, loginUser } from "../../store/slices/user/services"; 5 | import { useNavigate } from "react-router-dom"; 6 | import "./Logins.css"; 7 | 8 | function Login() { 9 | const { register, formState:{ errors }, handleSubmit} = useForm() 10 | const { register:reg, setError:setErrorL , formState:{errors:errorsL}, handleSubmit:submitLogin } = useForm() 11 | 12 | const [e, setE] = useState(false) 13 | const dispatch = useDispatch() 14 | const to = useNavigate() 15 | 16 | const fatherBgRef = useRef() 17 | const signRef = useRef() 18 | const btnRef = useRef() 19 | const bgRef = useRef() 20 | 21 | const handleBackground = () =>{ 22 | btnRef.current.classList.toggle("signing-up") 23 | signRef.current.classList.toggle("init-animation") 24 | signRef.current.classList.contains("init-animation") 25 | ?fatherBgRef.current.scrollTo({ behavior: "smooth", left: bgRef.current.clientWidth }) 26 | :fatherBgRef.current.scrollTo({ behavior: "smooth", left: 100 }); 27 | } 28 | 29 | useEffect(() => { 30 | bgRef.current.style.width = `${signRef.current.offsetWidth}px`; 31 | handleBackground(); 32 | },[dispatch]) 33 | 34 | const onSubmit = (data) =>{ 35 | dispatch(createUser(data)) 36 | to('/') 37 | } 38 | 39 | const onLogin = (data) =>{ 40 | setE(loginUser(data)) 41 | to('/') 42 | } 43 | 44 | return ( 45 |
46 |
47 |
48 |
49 |
50 |

Create Account

51 |
52 | 53 | 54 |
55 |
56 | 57 | {errors.firstName?.type === 'required' && "This field is required"} 58 | {errors.firstName?.type === 'minLength' && "The min lenght is 5"} 59 | 60 | 61 |
62 |
63 | 64 | {errors.lastName?.type === 'required' && "This field is required"} 65 | {errors.lastName?.type === 'minLength' && "The min lenght is 5"} 66 | 67 | 68 |
69 |
70 | 71 | {errors.email?.type === 'required' && "This field is required"} 72 | 73 | 74 |
75 |
76 | 77 | {errors.password?.type === 'required' && "This field is required"} 78 | {errors.password?.type === 'minLength' && "The min lenght is 10"} 79 | 80 | 86 |
87 |
88 | 89 | {errors.confirmPassword?.type === 'required' && "This field is required"} 90 | {errors.confirmPassword?.type === 'minLength' && "The min lenght is 10"} 91 | 92 | 98 |
99 | 102 |
103 |
104 |

Sign In to Adalene

105 |
106 | 107 | 108 |
109 |
110 | 111 | {errorsL.email?.type === 'required' && "This field is required"} 112 | 113 | 114 |
115 |
116 | 117 | {errorsL.password?.type === 'required' && "This field is required"} 118 | 119 | 125 |
126 | 130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |

Hello, Friend

138 |

Enter your personal details and start journey with us

139 |
140 |
141 |

Welcome Back

142 |

To keep connected with us please login with your personal info

143 |
144 | 148 |
149 |
150 | ); 151 | } 152 | 153 | export default Login; 154 | -------------------------------------------------------------------------------- /frontend/src/pages/Login/Logins.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | .wrap-sign { 7 | height: 100vh; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | .sign { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | background-color: #fff; 17 | border-radius: 1rem; 18 | width: 100%; 19 | height: 550px; 20 | padding: 0 1rem; 21 | position: relative; 22 | overflow: hidden; 23 | } 24 | 25 | .error{ 26 | position: absolute; 27 | bottom: 0.25rem; 28 | right:0.5rem; 29 | color:red; 30 | opacity: 0.7; 31 | font-size: 13px; 32 | } 33 | .sign__title { 34 | font-size: 1.5rem; 35 | font-weight: 500; 36 | } 37 | .sign__up, 38 | .sign__in { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | } 44 | .sign__icons { 45 | font-size: 2rem; 46 | } 47 | .sign__inputWrap { 48 | position: relative; 49 | 50 | align-self: normal; 51 | } 52 | .sign__inputWrap > input { 53 | width: 100%; 54 | margin-top: 0.5rem; 55 | padding: 1rem 0.5rem; 56 | outline-color: gray; 57 | } 58 | .btn-form, 59 | .btn-welcome { 60 | width: 200px; 61 | padding: 1rem 0.5rem; 62 | border: none; 63 | border-radius: 5rem; 64 | margin-top: 2rem; 65 | font-size: 12px; 66 | } 67 | .btn-form { 68 | background-color: rgb(32, 32, 32); 69 | color: #fff; 70 | } 71 | .btn-welcome { 72 | background-color: transparent; 73 | border: 1px solid #fff; 74 | color: #fff; 75 | position: fixed; 76 | top: 0; 77 | right: 1rem; 78 | overflow: hidden; 79 | } 80 | .sign__text { 81 | display: none; 82 | } 83 | .sign__text > h2 { 84 | font-size: 1.5rem; 85 | } 86 | .sign__in, 87 | .init-animation .sign__up { 88 | display: flex; 89 | } 90 | .init-animation .sign__in, 91 | .sign__up { 92 | display: none; 93 | } 94 | 95 | .btn__in, 96 | .btn__up { 97 | width: 100%; 98 | height: 100%; 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | position: absolute; 103 | top: 50%; 104 | transform: translate(-50%, -50%); 105 | transition: opacity 1.5s ease, visibility 1.5s ease, left 1s ease, 106 | transform 0.5s ease; 107 | } 108 | .btn__up, 109 | .signing-up .btn__in { 110 | opacity: 1; 111 | visibility: visible; 112 | left: 50%; 113 | } 114 | .btn__in, 115 | .signing-up .btn__up { 116 | opacity: 0; 117 | visibility: hidden; 118 | } 119 | 120 | .btn__in { 121 | left: 140%; 122 | } 123 | .signing-up .btn__up { 124 | left: -40%; 125 | } 126 | 127 | @media only screen and (min-width: 650px) { 128 | .wrap-sign { 129 | background-color: teal; 130 | } 131 | .sign { 132 | display: block; 133 | padding: 0; 134 | max-width: 750px; 135 | } 136 | .sign__inputWrap > input { 137 | background-color: rgb(242, 242, 242); 138 | border: none; 139 | border-radius: 0.5rem; 140 | } 141 | .btn-form { 142 | background-color: rgb(44, 199, 199); 143 | color: #fff; 144 | align-self: center; 145 | cursor: pointer; 146 | margin-top: 0.5rem; 147 | } 148 | 149 | .sign__user { 150 | min-width: 450px; 151 | background-color: #fff; 152 | display: flex; 153 | flex-direction: column; 154 | justify-content: center; 155 | height: 100%; 156 | position: absolute; 157 | top: 0; 158 | right: 0; 159 | transition: right 1s ease, transform 1s ease; 160 | } 161 | .init-animation .sign__user { 162 | right: 100%; 163 | transform: translateX(100%); 164 | } 165 | 166 | .sign__in, 167 | .sign__up { 168 | /* align-items: normal; */ 169 | padding: 3rem; 170 | width: 100%; 171 | transition: opacity 0.1s ease 0.3s, visibility 0.1s ease 0.3s; 172 | } 173 | .sign__in, 174 | .init-animation .sign__up { 175 | opacity: 1; 176 | visibility: visible; 177 | animation: PositionR 0.3s ease 0.3s; 178 | } 179 | .sign__in { 180 | position: relative; 181 | } 182 | 183 | .init-animation .sign__in, 184 | .sign__up { 185 | display: flex; 186 | opacity: 0; 187 | visibility: hidden; 188 | } 189 | .sign__up { 190 | top:0; 191 | position: absolute; 192 | } 193 | .sign__title, 194 | .sign__icons { 195 | align-self: center; 196 | } 197 | .sign__welcome { 198 | width: 300px; 199 | height: 100%; 200 | position: absolute; 201 | top: 0; 202 | left: 0; 203 | overflow: hidden; 204 | transition: left 1s ease, transform 1s ease; 205 | z-index: 10; 206 | animation: exitWelcome 1s ease; 207 | } 208 | .btn-welcome { 209 | height: 50px; 210 | position: absolute; 211 | top: 50%; 212 | transform: translateY(100%) translateX(25%); 213 | left: 0; 214 | z-index: 10; 215 | animation: exitWelcomeBtn 1s ease; 216 | } 217 | 218 | @keyframes exitWelcomeBtn { 219 | 0% { 220 | left: 100%; 221 | transform: translateY(100%) translateX(-125%); 222 | } 223 | 40% { 224 | width: 300px; 225 | } 226 | 100% { 227 | left: 0%; 228 | } 229 | } 230 | @keyframes exitWelcome { 231 | 0% { 232 | left: 100%; 233 | width: 300px; 234 | transform: translate(-100%); 235 | } 236 | 40% { 237 | width: 380px; 238 | } 239 | 100% { 240 | width: 300px; 241 | left: 0%; 242 | } 243 | } 244 | .sign__bg { 245 | width: 50px; 246 | background-color: rgb(44, 199, 199); 247 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='0' x2='0' y1='1' y2='0' gradientTransform='rotate(48,0.5,0.5)'%3E%3Cstop offset='0' stop-color='%2332DCB7'/%3E%3Cstop offset='1' stop-color='%2353FFDF'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='0' x2='0' y1='0' y2='1' gradientTransform='rotate(32,0.5,0.5)'%3E%3Cstop offset='0' stop-color='%2350F1D4'/%3E%3Cstop offset='1' stop-color='%2330CCBC'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cg fill='%23FFF' fill-opacity='0' stroke-miterlimit='10'%3E%3Cg stroke='url(%23a)' stroke-width='18.81'%3E%3Cpath transform='translate(-74.9 10.399999999999999) rotate(4.7 1409 581) scale(1.000632)' d='M1409 581 1450.35 511 1490 581z'/%3E%3Ccircle stroke-width='6.2700000000000005' transform='translate(-68 50) rotate(9 800 450) scale(1.012886)' cx='500' cy='100' r='40'/%3E%3Cpath transform='translate(25.800000000000004 -96) rotate(84 401 736) scale(1.012886)' d='M400.86 735.5h-83.73c0-23.12 18.74-41.87 41.87-41.87S400.86 712.38 400.86 735.5z'/%3E%3C/g%3E%3Cg stroke='url(%23b)' stroke-width='5.7'%3E%3Cpath transform='translate(300 -7.999999999999999) rotate(1.9999999999999998 150 345) scale(0.973544)' d='M149.8 345.2 118.4 389.8 149.8 434.4 181.2 389.8z'/%3E%3Crect stroke-width='12.540000000000001' transform='translate(-134 -131) rotate(93.60000000000001 1089 759)' x='1039' y='709' width='100' height='100'/%3E%3Cpath transform='translate(-232.8 71.2) rotate(15.600000000000001 1400 132) scale(0.94)' d='M1426.8 132.4 1405.7 168.8 1363.7 168.8 1342.7 132.4 1363.7 96 1405.7 96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); 248 | background-attachment: scroll; 249 | background-size: cover; 250 | height: 100%; 251 | position: absolute; 252 | z-index: -1; 253 | } 254 | .sign__text { 255 | display: block; 256 | width: 300px; 257 | color: #fff; 258 | position: absolute; 259 | padding: 1rem; 260 | left: 0; 261 | top: 50%; 262 | z-index: 10; 263 | } 264 | .left-txt { 265 | opacity: 1; 266 | transform: translateY(-70%) translateX(0); 267 | transition: opacity 0.5s ease, transform 0.8s ease, width 0.3s ease; 268 | } 269 | .init-animation .left-txt { 270 | opacity: 0; 271 | transform: translateY(-70%) translateX(-100%); 272 | } 273 | .right-txt { 274 | opacity: 0; 275 | left: 100%; 276 | transform: translateY(-70%) translateX(0); 277 | transition: opacity 0.5s ease, transform 0.8s ease, width 0.3s ease; 278 | } 279 | .init-animation .right-txt { 280 | opacity: 1; 281 | transform: translateY(-70%) translateX(-100%); 282 | } 283 | .init-animation .sign__welcome { 284 | animation: initWelcome 1s ease forwards; 285 | } 286 | .init-animation .btn-welcome { 287 | animation: initWelcomeBtn 1s ease forwards; 288 | } 289 | @keyframes initWelcomeBtn { 290 | 40% { 291 | width: 300px; 292 | } 293 | 90%, 294 | 100% { 295 | left: 100%; 296 | transform: translateY(100%) translateX(-125%); 297 | } 298 | } 299 | @keyframes initWelcome { 300 | 40% { 301 | width: 380px; 302 | } 303 | 100% { 304 | width: 300px; 305 | left: 100%; 306 | transform: translateX(-100%); 307 | } 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /frontend/src/pages/Product/Product.js: -------------------------------------------------------------------------------- 1 | function Product() { 2 | return ( 3 |
4 |
5 |
6 |
7 | 12 | 17 | 22 | 27 |
28 |
29 | 34 |
35 |
36 | 37 |
38 |
39 |

Recently

40 |
41 |
42 |
43 | 48 |
49 | 50 | Product $300.00 51 | 52 |
53 |
54 |
55 | 60 |
61 | 62 | Product $300.00 63 | 64 |
65 |
66 |
67 | 72 |
73 | 74 | Product $300.00 75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 |

Product

84 |
85 |

86 | A sectional sofa or an L shaped sofa can make a great addition on 87 | your living room based on your needs 88 |

89 |
90 | $430.99 91 |
92 |
93 | 1 94 | 2 95 | 3 96 | 4 97 | 5 98 |
99 |

441 reviews

100 |
101 |
102 |
103 |
107 |

Colour

108 |
109 | 115 | 121 | 127 | 133 |
134 |
135 |
136 |
137 | 140 | 143 |
144 |
145 |
146 |
147 |

148 | Dispatched in 5-7 weeks ¡ 149 |

150 |

Why the longer lead time?

151 |
152 |
153 |

Home Delivery - $10

154 |
155 |
156 |
157 |
158 | ); 159 | } 160 | 161 | export default Product; 162 | -------------------------------------------------------------------------------- /frontend/src/pages/Products/Products.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Products = () => { 4 | return ( 5 |
Products
6 | ) 7 | } 8 | 9 | export default Products -------------------------------------------------------------------------------- /frontend/src/pages/Profile/Profile.js: -------------------------------------------------------------------------------- 1 | import { IoIosArrowBack } from "react-icons/io"; 2 | 3 | function Profile() { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 |

Profile

11 |
12 |
13 |
14 |
15 | 20 |
21 |

Name

22 |
23 | Datos 24 |
25 |
26 |
27 |
28 | Historial, estado de las compras 29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default Profile; 37 | -------------------------------------------------------------------------------- /frontend/src/pages/ProfileSeller/ProfileSeller.js: -------------------------------------------------------------------------------- 1 | import { IoIosArrowBack } from "react-icons/io"; 2 | function ProfileSeller() { 3 | return ( 4 |
5 |
6 | 7 | 8 | 9 |

Profile

10 |
11 |
12 |
13 |
14 | 19 |
20 |

Name

21 |
22 | Datos 23 |
24 |
25 |
26 |
27 | {" "} 28 | historial de ventas 29 |
30 |
31 | {" "} 32 | historial de compras 33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default ProfileSeller; 41 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit' 2 | import categoryReducer from './slices/category' 3 | import productReducer from './slices/product' 4 | import userReducer from './slices/user' 5 | export default configureStore({ 6 | reducer:{ 7 | categoryReducer, 8 | productReducer, 9 | userReducer 10 | } 11 | }) -------------------------------------------------------------------------------- /frontend/src/store/slices/category/category.rest: -------------------------------------------------------------------------------- 1 | get https://django-ecommerce-v1.herokuapp.com/api/v1/categories -------------------------------------------------------------------------------- /frontend/src/store/slices/category/index.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | export const categorySlice = createSlice({ 3 | name:'category', 4 | initialState: { 5 | categories: [], 6 | loading: true 7 | }, 8 | reducers:{ 9 | setLoading: (state, action) =>{ 10 | state.loading = !action.payload 11 | }, 12 | setCategories: (state, action) =>{ 13 | state.categories = action.payload 14 | } 15 | } 16 | }) 17 | 18 | export const { setCategories, setLoading } = categorySlice.actions 19 | 20 | export default categorySlice.reducer -------------------------------------------------------------------------------- /frontend/src/store/slices/category/services.js: -------------------------------------------------------------------------------- 1 | import { setCategories } from './index' 2 | 3 | const baseUrl = "http://django-ecommerce-v1.herokuapp.com/api/v1" 4 | export const getCategories = () => (dispatch) => { 5 | try { 6 | fetch(`${baseUrl}/categories/`) 7 | .then(res => res.json()) 8 | .then(res =>{ 9 | dispatch(setCategories(res)) 10 | }) 11 | 12 | } catch (error) { 13 | console.error(error) 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/src/store/slices/product/index.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const productSlice = createSlice({ 4 | name:'product', 5 | initialState: { 6 | list: [], 7 | }, 8 | reducers:{ 9 | 10 | } 11 | }) 12 | 13 | //export const { } = projectsSlice.actions 14 | 15 | export default productSlice.reducer -------------------------------------------------------------------------------- /frontend/src/store/slices/product/services.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/frontend/src/store/slices/product/services.js -------------------------------------------------------------------------------- /frontend/src/store/slices/user/index.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const userSlice = createSlice({ 4 | name:'user', 5 | initialState: { 6 | user: { 7 | email:'', 8 | firstName: '', 9 | lastName:'' 10 | }, 11 | loading: false, 12 | }, 13 | reducers:{ 14 | setUserData:(state, action) =>{ 15 | state.user.email = action.payload.email 16 | state.user.firstName = action.payload.first_name 17 | state.user.lastName = action.payload.last_name 18 | } 19 | } 20 | }) 21 | 22 | export const { setUserData } = userSlice.actions 23 | 24 | export default userSlice.reducer -------------------------------------------------------------------------------- /frontend/src/store/slices/user/services.js: -------------------------------------------------------------------------------- 1 | import { setUserData } from './index' 2 | 3 | export const createUser = (data) => (dispatch) =>{ 4 | const body = new FormData() 5 | body.append("email",data.email) 6 | body.append("first_name", data.firstName) 7 | body.append("last_name", data.lastName) 8 | body.append("password", data.password) 9 | body.append("password2", data.confirmPassword) 10 | fetch('http://django-ecommerce-v1.herokuapp.com/api/v1/users/',{ 11 | method:'POST', 12 | body 13 | }) 14 | .then(res => res.json()) 15 | .then(res =>{ 16 | dispatch(setUserData(res)) 17 | saveToken({email:data.email, password:data.password}) 18 | }) 19 | } 20 | 21 | export const loginUser = async (data) =>{ 22 | if(await saveToken(data)){ 23 | return true 24 | } 25 | return false 26 | } 27 | 28 | export const saveToken = (data) =>{ 29 | const body = new FormData() 30 | body.append("email", data.email) 31 | body.append("password", data.password) 32 | fetch('http://django-ecommerce-v1.herokuapp.com/api/v1/users/token/',{ 33 | method:'POST', 34 | body 35 | }) 36 | .then(res => res.json()) 37 | .then(res =>{ 38 | window.localStorage.setItem('token', res.access) 39 | window.localStorage.setItem('refresh', res.refresh) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/store/slices/user/user.rest: -------------------------------------------------------------------------------- 1 | POST http://django-ecommerce-v1.herokuapp.com/api/v1/users/ 2 | Content-Type:"application/json" 3 | 4 | { 5 | "email": "dannny@example.com", 6 | "first_name": "dannyasAsa", 7 | "last_name": "ArpiSAsaSA", 8 | "password": "123456789", 9 | "password2": "123456789" 10 | } -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /local.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine3.15 2 | 3 | # Keeps Python from generating .pyc files in the container 4 | ENV PYTHONDONTWRITEBYTECODE=1 5 | # Turns off buffering for easier container logging 6 | ENV PYTHONUNBUFFERED=1 7 | 8 | WORKDIR /app 9 | 10 | COPY ./requirements.txt ./ 11 | 12 | RUN apk update \ 13 | && apk add --no-cache gcc musl-dev postgresql-dev python3-dev jpeg-dev zlib-dev \ 14 | && pip install --upgrade pip 15 | 16 | RUN python -m pip install -r requirements.txt 17 | 18 | COPY ./ ./ 19 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | """Couldn't import Django. Are you sure it's installed and 15 | available on your PYTHONPATH environment variable? Did you 16 | forget to activate a virtual environment?""" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /payment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/payment/__init__.py -------------------------------------------------------------------------------- /payment/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/payment/admin.py -------------------------------------------------------------------------------- /payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "payment" 7 | -------------------------------------------------------------------------------- /payment/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/payment/migrations/__init__.py -------------------------------------------------------------------------------- /payment/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/payment/models.py -------------------------------------------------------------------------------- /payment/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/payment/tests.py -------------------------------------------------------------------------------- /payment/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = "payment" 6 | 7 | urlpatterns = [ 8 | path("checkout-session/", views.create_checkout_session, name="create-checkout-session"), 9 | path("webhooks/stripe/", views.stripe_webhook, name="stripe-webhook"), 10 | path("test/", views.payment_test, name="test"), 11 | path("success/", views.payment_success, name="success"), 12 | path("cancel/", views.payment_cancel, name="cancel"), 13 | ] 14 | -------------------------------------------------------------------------------- /payment/views.py: -------------------------------------------------------------------------------- 1 | import stripe 2 | from django.conf import settings 3 | from django.core.mail import send_mail 4 | from django.http import HttpResponse 5 | from django.shortcuts import redirect, render 6 | from django.urls import reverse_lazy 7 | from django.views.decorators.csrf import csrf_exempt 8 | 9 | from config.settings import STRIPE_PRIVATE_KEY, STRIPE_WEBHOOK_KEY 10 | from shopping_cart.models import CartItem, CartSession 11 | from shopping_cart.views import _get_session_id 12 | 13 | YOUR_DOMAIN = "http://127.0.0.1:8000" 14 | 15 | stripe.api_key = STRIPE_PRIVATE_KEY 16 | 17 | 18 | def payment_test(request): 19 | return render(request, "payment/payment.html") 20 | 21 | 22 | def payment_success(request): 23 | return render(request, "payment/success.html") 24 | 25 | 26 | def payment_cancel(request): 27 | return render(request, "payment/cancel.html") 28 | 29 | 30 | def create_checkout_session(request): 31 | if request.method == "POST": 32 | cart = CartSession.objects.get(session_id=_get_session_id(request)) 33 | cart_items = CartItem.objects.filter(cart=cart) 34 | print(f"cart: {cart}") 35 | print(f"cart items {cart_items}") 36 | line_items = [] 37 | for item in cart_items: 38 | # nombre de producto 39 | name = item.product.title 40 | # precio 41 | price = item.product.price 42 | # cantidad 43 | quantity = item.quantity 44 | item_dict = { 45 | "price_data": { 46 | "currency": "mxn", # moneda de pago 47 | "product_data": { # informacion del producto, tambien se puede agregar descripcion, pero no veo necesario 48 | "name": name 49 | }, 50 | "unit_amount": price * 100, # precio del articulo 51 | }, 52 | "quantity": quantity, # cantidad de productos que se comprara 53 | } 54 | line_items.append(item_dict) 55 | 56 | print(line_items) 57 | 58 | try: 59 | checkout_session = stripe.checkout.Session.create( 60 | line_items=line_items, 61 | # customer_email=self.request.user.email, # for pass user email 62 | payment_method_types=["card", "oxxo"], 63 | billing_address_collection="required", # pedir direccion 64 | mode="payment", 65 | success_url=request.build_absolute_uri(reverse_lazy("payment:success")), 66 | cancel_url=request.build_absolute_uri(reverse_lazy("payment:cancel")), 67 | ) 68 | except Exception as e: 69 | print(e) 70 | return str(e) 71 | 72 | return redirect(checkout_session.url) 73 | 74 | 75 | @csrf_exempt 76 | def stripe_webhook(request): 77 | payload = request.body 78 | sig_header = request.META["HTTP_STRIPE_SIGNATURE"] 79 | event = None 80 | 81 | try: 82 | event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_KEY) 83 | except ValueError as e: 84 | # Invalid payload 85 | print("error") 86 | return HttpResponse(status=400) 87 | except stripe.error.SignatureVerificationError as e: 88 | # Invalid signature 89 | print("error") 90 | return HttpResponse(status=400) 91 | 92 | # Handle the checkout.session.completed event 93 | if event["type"] == "checkout.session.completed": 94 | session = event["data"]["object"] 95 | 96 | # Fulfill the purchase... 97 | fulfill_order(session) 98 | print("éxito") 99 | # Passed signature verification 100 | return HttpResponse(status=200) 101 | 102 | 103 | def fulfill_order(session): 104 | customer_email = session["customer_details"]["email"] 105 | customer_shipping = session["shipping"] 106 | 107 | print(settings.EMAIL_HOST_USER) 108 | 109 | send_mail( 110 | subject="Orden de compra", 111 | # TODO, NO GUARDA EL VALOR DE customer_shipping 112 | message=f"Gracias por tu compra, te la enviaremos a {customer_shipping}", 113 | recipient_list=[customer_email], 114 | from_email=settings.EMAIL_HOST_USER, 115 | fail_silently=False, 116 | ) 117 | 118 | print("Fulfilling order") 119 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 119 3 | 4 | [tool.isort] 5 | profile = "black" 6 | -------------------------------------------------------------------------------- /readme/Guide_Pipenv.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## 1.- Pip install 4 | 5 | Option 1 6 | ``` 7 | $ sudo apt update 8 | $ sudo apt install python3-venv python3-pip 9 | ``` 10 | Option 2 11 | 12 | ``` 13 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py ⇒ descarga paquetes iniciales 14 | python get-pip.py 15 | python -m pip install --upgrade pip 16 | ``` 17 | 18 | 19 | ## 2.- Install Postgres SQl 20 | 21 | 22 | ``` 23 | $ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 24 | $ wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 25 | $ sudo apt-get update 26 | $ sudo apt-get -y install postgresql postgresql-contrib 27 | $ sudo apt-get install libpq-dev 28 | ``` 29 | 30 | ### 2.2.- execute postgresql 31 | 32 | $ sudo service postgresql status 33 | $ sudo service postgresql start 34 | 35 | 36 | ### 2.3.- Create database and password 37 | 38 | sudo -u postgres psql => start console 39 | 40 | ``` 41 | postgres=# alter user Postgres with password 'newPasword'; 42 | postgres=# CREATE DATABASE name 43 | ``` 44 | 45 | - set this dates in your .env 46 | 47 | POSTGRES_NAME=name 48 | POSTGRES_PASSWORD=newPasword 49 | 50 | 51 | 52 | 53 | 54 | ## 3.- install environment pipenv 55 | 56 | python3 -m pip install pipenv 57 | 58 | 59 | ## 4.- activate Virtual environment 60 | 61 | pipenv shell 62 | or 63 | python3 -m pipenv shell 64 | 65 | pipenv install 66 | pipenv lock -r 67 | 68 | ## get server run 69 | 70 | python manage.py runserver 71 | 72 | 73 | -------------------------------------------------------------------------------- /readme/Problemas.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Vendor 4 | - un vendedor puede comprar productos a otros vendedores 5 | - notificaciones si su producto fue vendido 6 | - los vendedores no pueden eliminar productos si no son propietarios del producto 7 | --- 8 | 9 | ## Client 10 | 11 | - solo los vendedores pueden postear 12 | --- 13 | 14 | ## Productos 15 | - cada iten debe tener una url unica ya que dos vendedores pueden vender exactamente un mismo articulo 16 | - un iten puede tener un stock de mas de 1 por que puede vender varias unidades 17 | - agregar categorias de productos 18 | - los usuarios pueden hacer comentarios o review respecto a un produto 19 | - 20 | --- 21 | 22 | ## carrito de compras 23 | - puede realizar una compra a varios vendedores en un mismo carrito de compras -------------------------------------------------------------------------------- /requirements-deploy.txt: -------------------------------------------------------------------------------- 1 | django==4.0.2 2 | django-environ==0.8.1 3 | django-filter==22.1 4 | psycopg2==2.9.3 5 | pillow==9.0.1 6 | gunicorn==20.1.0 7 | django-storages==1.12.3 8 | stripe==2.68.0 9 | djangorestframework==3.13.1 10 | djangorestframework-simplejwt==5.1.0 11 | drf-yasg2==1.19.4 12 | django-cors-headers==3.13.0 13 | whitenoise==6.2.0 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # These requirements were autogenerated by pipenv 3 | # To regenerate from the project's Pipfile, run: 4 | # 5 | # pipenv lock --requirements 6 | # 7 | 8 | -i https://pypi.org/simple 9 | appdirs==1.4.4 10 | asgiref==3.5.2; python_version >= '3.7' 11 | astroid==2.11.7; python_full_version >= '3.6.2' 12 | async-timeout==4.0.2; python_version >= '3.6' 13 | attrs==22.1.0; python_version >= '3.5' 14 | backports.zoneinfo==0.2.1; python_version < '3.9' 15 | black==18.9b0 16 | boto3==1.24.42 17 | botocore==1.27.42; python_version >= '3.7' 18 | certifi==2022.6.15; python_version >= '3.6' 19 | charset-normalizer==2.1.0; python_version >= '3.6' 20 | click==8.1.3; python_version >= '3.7' 21 | coreapi==2.3.3 22 | coreschema==0.0.4 23 | deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 24 | dill==0.3.5.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6' 25 | dj-database-url==1.0.0 26 | django-cors-headers==3.13.0 27 | django-debug-toolbar==3.5.0 28 | django-environ==0.9.0 29 | django-filter==22.1 30 | django-heroku==0.3.1 31 | django-redis==5.2.0 32 | django-storages==1.12.3 33 | django==4.0.6 34 | djangorestframework-simplejwt==5.2.0 35 | djangorestframework==3.13.1 36 | drf-yasg2==1.19.4 37 | flake8==5.0.2 38 | gunicorn==20.1.0 39 | idna==3.3; python_version >= '3.5' 40 | inflection==0.5.1; python_version >= '3.5' 41 | isort==5.10.1 42 | itypes==1.2.0 43 | jinja2==3.1.2; python_version >= '3.7' 44 | jmespath==1.0.1; python_version >= '3.7' 45 | lazy-object-proxy==1.7.1; python_version >= '3.6' 46 | markupsafe==2.1.1; python_version >= '3.7' 47 | mccabe==0.7.0; python_version >= '3.6' 48 | packaging==21.3; python_version >= '3.6' 49 | pillow==9.2.0 50 | platformdirs==2.5.2; python_version >= '3.7' 51 | psycopg2==2.9.3 52 | pycodestyle==2.9.0; python_version >= '3.6' 53 | pyflakes==2.5.0; python_version >= '3.6' 54 | pyjwt==2.4.0; python_version >= '3.6' 55 | pylint==2.14.5 56 | pyparsing==3.0.9; python_full_version >= '3.6.8' 57 | python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 58 | pytz==2022.1 59 | redis==4.3.4; python_version >= '3.6' 60 | requests==2.28.1; python_version >= '3' 61 | ruamel.yaml.clib==0.2.6; python_version < '3.11' and platform_python_implementation == 'CPython' 62 | ruamel.yaml==0.17.21; python_version >= '3' 63 | s3transfer==0.6.0; python_version >= '3.7' 64 | six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' 65 | sqlparse==0.4.2; python_version >= '3.5' 66 | stripe==3.5.0 67 | toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' 68 | tomli==2.0.1; python_version < '3.11' 69 | tomlkit==0.11.1; python_version >= '3.6' and python_version < '4' 70 | typing-extensions==4.3.0; python_version < '3.10' 71 | uritemplate==4.1.1; python_version >= '3.6' 72 | urllib3==1.26.11; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4' 73 | whitenoise==6.2.0 74 | wrapt==1.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' 75 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.12 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git, *migrations* 3 | max-line-length = 119 4 | ignore = F841, E501, F403, F405 5 | -------------------------------------------------------------------------------- /shopping_cart/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/shopping_cart/__init__.py -------------------------------------------------------------------------------- /shopping_cart/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/shopping_cart/admin.py -------------------------------------------------------------------------------- /shopping_cart/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ShoppingCartConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "shopping_cart" 7 | -------------------------------------------------------------------------------- /shopping_cart/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-18 01:13 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("ecommerce", "0001_initial")] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="CartSession", 16 | fields=[ 17 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 18 | ("session_id", models.CharField(blank=True, max_length=255)), 19 | ("data_created", models.DateTimeField(auto_now_add=True)), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name="CartItem", 24 | fields=[ 25 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 26 | ("quantity", models.IntegerField(default=0)), 27 | ("is_active", models.BooleanField(default=True)), 28 | ("created_at", models.DateTimeField(auto_now_add=True)), 29 | ( 30 | "cart", 31 | models.ForeignKey( 32 | null=True, on_delete=django.db.models.deletion.SET_NULL, to="shopping_cart.cartsession" 33 | ), 34 | ), 35 | ( 36 | "product", 37 | models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to="ecommerce.product"), 38 | ), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /shopping_cart/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/shopping_cart/migrations/__init__.py -------------------------------------------------------------------------------- /shopping_cart/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from ecommerce.models import Product 4 | 5 | # Create your models here. 6 | 7 | 8 | class CartSession(models.Model): 9 | session_id = models.CharField(max_length=255, blank=True) 10 | data_created = models.DateTimeField(auto_now_add=True) 11 | 12 | def __str__(self): 13 | return self.session_id 14 | 15 | 16 | class CartItem(models.Model): 17 | product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True) 18 | cart = models.ForeignKey(CartSession, on_delete=models.SET_NULL, null=True) 19 | quantity = models.IntegerField(default=0) 20 | is_active = models.BooleanField(default=True) 21 | created_at = models.DateTimeField(auto_now_add=True) 22 | 23 | def get_subtotal(self): 24 | return self.product.price * self.quantity 25 | 26 | def __unicode__(self): 27 | return self.product 28 | -------------------------------------------------------------------------------- /shopping_cart/shopping_cart.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | class ShoppingCart: 5 | # TODO: add object session 6 | def __init__(self,) -> None: 7 | self.cart = defaultdict(dict) 8 | 9 | def add_product(self, product): 10 | if product["id"] not in self.cart.keys(): 11 | self.cart[product["id"]] = { 12 | "product_id": product["id"], 13 | "title": product["title"], 14 | "description": product["description"], 15 | "quantity": 1, 16 | "price": product["price"], 17 | } 18 | else: 19 | self.increment_product(product) 20 | 21 | def delete_product(self, product): 22 | product_id = product["id"] 23 | old_product = self.cart.get(product_id, None) 24 | if old_product is not None: 25 | del self.cart[product_id] 26 | 27 | def decrement_product(self, product): 28 | product_id = product["id"] 29 | old_product = self.cart.get(product_id, None) 30 | if old_product is None: 31 | print("El producto no existe en el carrito de compras") 32 | return 33 | 34 | if old_product["quantity"] > 1: 35 | old_product["quantity"] -= 1 36 | else: 37 | self.delete_product(product) 38 | 39 | def increment_product(self, product): 40 | product_id = product["id"] 41 | old_product = self.cart[product_id] 42 | old_product["quantity"] += 1 43 | self.cart[product_id] = old_product 44 | 45 | def reset_cart(self): 46 | self.cart = defaultdict(dict) 47 | 48 | def save(self): 49 | pass 50 | 51 | 52 | # product1 = { 53 | # "id": 1, 54 | # "title": "Sandalia gucci", 55 | # "description": "Las sandalias gucci de cartier", 56 | # "quantity": 1, 57 | # "price": 18.9, 58 | # } 59 | # product2 = { 60 | # "id": 1, 61 | # "title": "Sandalia gucci", 62 | # "description": "Las sandalias gucci de cartier black", 63 | # "quantity": 1, 64 | # "price": 18.9, 65 | # } 66 | # product3 = { 67 | # "id": 1, 68 | # "title": "Sandalia gucci Rojas", 69 | # "description": "Las sandalias gucci de cartier Rojas", 70 | # "quantity": 2, 71 | # "price": 18.9, 72 | # } 73 | # product4 = { 74 | # "id": 2, 75 | # "title": "Suerter Polo Azul", 76 | # "description": "Sueter Polo Azul de algodon 100% puro", 77 | # "quantity": 2, 78 | # "price": 50.23, 79 | # } 80 | # product5 = { 81 | # "id": 2, 82 | # "title": "Suerter Polo Azul", 83 | # "description": "Sueter Polo Azul de algodon 100% puro", 84 | # "quantity": 2, 85 | # "price": 50.23, 86 | # } 87 | # shopping_cart = ShoppingCart() 88 | # shopping_cart.add_product(product1) 89 | # shopping_cart.add_product(product2) 90 | # shopping_cart.add_product(product3) 91 | # pprint(shopping_cart.cart) 92 | # shopping_cart.decrement_product(product1) 93 | # # shopping_cart.reset_cart() 94 | # shopping_cart.add_product(product4) 95 | # # print(shopping_cart.cart) 96 | # pprint(shopping_cart.cart) 97 | -------------------------------------------------------------------------------- /shopping_cart/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/shopping_cart/tests.py -------------------------------------------------------------------------------- /shopping_cart/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import add_product_cart, delete_product_cart, list_products_cart, remove_product_cart 4 | 5 | app_name = "shopping_cart" 6 | 7 | urlpatterns = [ 8 | path("", list_products_cart, name="home"), 9 | path("add_product/", add_product_cart, name="add_product"), 10 | path("remove_product/", remove_product_cart, name="remove_product"), 11 | path("delete_product/", delete_product_cart, name="delete_product"), 12 | ] 13 | -------------------------------------------------------------------------------- /shopping_cart/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.shortcuts import get_object_or_404, redirect, render 3 | 4 | from .models import CartItem, CartSession, Product 5 | 6 | # from ecommerce.models import Product 7 | 8 | 9 | def _get_session_id(request): 10 | cart_session = request.session.session_key 11 | if not cart_session: 12 | cart_session = request.session.create() 13 | return cart_session 14 | 15 | 16 | @login_required 17 | # Create your views here. 18 | def add_product_cart(request, product_id): 19 | product = Product.objects.get(id=product_id) 20 | 21 | try: 22 | cart = CartSession.objects.get(session_id=_get_session_id(request)) 23 | except CartSession.DoesNotExist: 24 | cart = CartSession.objects.create(session_id=_get_session_id(request)) 25 | 26 | cart.save() 27 | print("Added product") 28 | 29 | try: 30 | cart_item = CartItem.objects.get(product=product, cart=cart) 31 | cart_item.quantity += 1 32 | cart_item.save() 33 | except CartItem.DoesNotExist: 34 | print("Added new product") 35 | cart_item = CartItem.objects.create(product=product, quantity=1, cart=cart) 36 | cart_item.save() 37 | 38 | # base_url = reverse('ecommerce') 39 | return redirect("shopping_cart:home") 40 | 41 | 42 | @login_required 43 | def list_products_cart(request): 44 | cart_items = None 45 | total = 0 46 | quantity = 0 47 | try: 48 | cart = CartSession.objects.get(session_id=_get_session_id(request)) 49 | cart_items = CartItem.objects.filter(cart=cart, is_active=True) 50 | for item in cart_items: 51 | total += item.get_subtotal() 52 | 53 | print(f"Total: {total}") 54 | except CartItem.DoesNotExist: 55 | pass 56 | except CartItem.DoesNotExist: 57 | pass 58 | 59 | return render(request, "cart/cart.html", {"cart_items": cart_items, "total": total}) 60 | 61 | 62 | @login_required 63 | def delete_product_cart(request, product_id): 64 | cart = CartSession.objects.get(session_id=_get_session_id(request)) 65 | product = Product.objects.get(id=product_id) 66 | cart_item = CartItem.objects.get(product=product, cart=cart) 67 | cart_item.delete() 68 | return redirect("shopping_cart:home") 69 | 70 | 71 | @login_required 72 | def remove_product_cart(request, product_id): 73 | cart = CartSession.objects.get(session_id=_get_session_id(request)) 74 | product = get_object_or_404(Product, id=product_id) 75 | item = CartItem.objects.get(product=product, cart=cart) 76 | if item.quantity > 1: 77 | item.quantity -= 1 78 | item.save() 79 | else: 80 | item.delete() 81 | 82 | return redirect("shopping_cart:home") 83 | -------------------------------------------------------------------------------- /staticfiles/css/main.css: -------------------------------------------------------------------------------- 1 | a { 2 | color: black; 3 | } 4 | a:link { 5 | text-decoration: none; 6 | } 7 | a:hover { 8 | background-color: yellow; 9 | } 10 | a:visited { 11 | text-decoration: none 12 | } 13 | .top-menu { 14 | position: absolute; 15 | top: 300; 16 | right: 0; 17 | margin-right: 20px; 18 | } 19 | 20 | .user-msg { 21 | font-size: 18px; 22 | float: right; 23 | margin-right: 20px; 24 | margin-top: 20px; 25 | } 26 | .table-shopping-cart.table td, .table-shopping-cart.table th { 27 | padding-left: 1.25rem; 28 | padding-right: 1.25rem; 29 | } 30 | .table-shopping-cart .price-wrap { 31 | line-height: 1.2; 32 | } 33 | .table-shopping-cart .price { 34 | font-weight: bold; 35 | margin-right: 5px; 36 | display: block; 37 | } 38 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | AisModa {% block title %} {% endblock title %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% include 'navbar.html' %} 25 | 26 | 27 | 28 | 29 |
30 | {% block content %} 31 | 32 | 33 | {% endblock content %} 34 |
35 | 36 | 37 | {% include 'footer.html' %} 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /templates/cart/cart.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | {% block content %} 3 | 4 |
5 | 6 | {% if cart_items.count is 0 %} 7 |
8 | 9 | Cart empty.... 10 | 11 |
12 | {% else %} 13 |
15 |
16 |

My cart

17 | {% for item in cart_items %} 18 |
19 |
20 | {% include "cart/images.html" %} 21 | A black chair with wooden legs 22 |
23 |
24 |

{{item.product.title}}

25 |

${{item.product.price }}

26 | 27 |
28 | 29 |
30 | 31 |
32 | 33 | 35 | {{item.quantity}} 36 | 38 |
39 |
${{item.get_subtotal}}
40 | x 42 | 43 | 44 |
45 | {% endfor %} 46 |
47 | 48 | 49 | 50 |
51 |

Order summary

52 |
53 |
Total
54 |
$ {{total}}
55 |
56 | 57 | 58 | 59 |
{% csrf_token %} 61 |
62 | 63 | 64 |
65 |
66 | {% endif %} 67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | {% endblock content %} -------------------------------------------------------------------------------- /templates/cart/images.html: -------------------------------------------------------------------------------- 1 | {% for image in item.product_images.all %} 2 |
4 | Front of men's Basic Tee in black. 6 |
7 | 8 | {% endfor %} -------------------------------------------------------------------------------- /templates/categories.html: -------------------------------------------------------------------------------- 1 |
2 |

categories

3 |
4 |
5 |
6 |
7 |
8 |
10 | 12 | 14 | 15 |
16 |
17 |
18 |
black chair and white table 21 |
22 |

23 | Catalog 1

24 |
26 |

28 | Minimal Interior

29 |
30 |
31 |
32 |
sitting area 35 |
36 | 37 | 38 |
40 |

42 | Minimal Interior 43 |

44 |
45 |
46 |
47 |
sitting area 50 |
51 |

52 | Catalog 1

53 |
55 |

57 | Minimal Interior

58 |
59 |
60 |
61 |
sitting area 64 |
65 |

66 | Catalog 1

67 |
69 |

71 | Minimal Interior

72 |
73 |
74 |
75 |
black chair and white table 78 |
79 |

80 | Catalog 1

81 |
83 |

85 | Minimal Interior

86 |
87 |
88 |
89 |
90 |
97 |
98 |
99 |
-------------------------------------------------------------------------------- /templates/ecommerce/home.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | {% block title %}MarketPlace{% endblock title %} 3 | {% block content %} 4 | 5 | {% include '../hero.html' %} 6 | {% include '../categories.html' %} 7 | 8 |
9 |

Products List

10 |
11 |
12 |
13 | 14 |
15 | 16 | {% for product in products %} 17 |
18 | 19 | {% include "ecommerce/images.html" %} 20 | 21 | 22 |
23 |
24 |

25 | {{product.title}} 27 |

28 |
29 |

${{product.price}}

30 | add to cart 31 |
32 |
33 | {% endfor %} 34 |
35 |
36 | 37 | 38 | 39 | 40 | {% endblock content %} 41 | 42 | 43 | By clicking Sign up, you agree to the terms of use. -------------------------------------------------------------------------------- /templates/ecommerce/images.html: -------------------------------------------------------------------------------- 1 | {% for image in product.product_images.all %} 2 | 3 |
5 | Front of men's Basic Tee in black. 7 |
8 | 9 | {% endfor %} -------------------------------------------------------------------------------- /templates/ecommerce/product_delete.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | 4 | 5 | {% block title %}{{product.title}}{% endblock title %} 6 | {% block content %} 7 | 8 |
9 | 10 |
11 |
12 |
Are you sure you want to delete
13 |

"{{product.title}}"

14 | {% if product.product_images.image_location %} 15 | 16 |

si existo

17 | {% endif %} 18 | 19 | 28 |
29 |
30 | 31 | 32 | 33 | {% endblock content %} -------------------------------------------------------------------------------- /templates/ecommerce/product_detail.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |
9 |
11 |
18 |
19 |
20 |
21 | 22 | {% include "ecommerce/images.html" %} 23 | 24 |
25 |
26 |
27 |
34 |
35 |
36 |

{{product.title}}

37 |

{{product.seller}}

38 |

{{ product.description}}

39 |

${{product.price}}

40 |
42 |
43 | 44 | 46 | Add to cart 47 | 48 | 49 | buy 50 | now 51 | 52 | 53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 | 63 | 67 | 68 | {% endblock content %} -------------------------------------------------------------------------------- /templates/ecommerce/product_edit.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | 12 |
13 | 14 | {{ product_form.title }} 15 |
16 | 17 | 18 | 19 |
20 | 21 | {{ product_form.description }} 22 |
23 | 24 |
25 | 26 | {{ product_form.price }} 27 |
28 | 29 |
30 | 31 | {{ product_form.category }} 32 |
33 | 34 |
35 | 36 | {{ product_form.stock }} 37 |
38 | 39 |
40 | 41 | {{ image_form.image_location }} 42 |
43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | {% endblock content %} 56 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /templates/hero.html: -------------------------------------------------------------------------------- 1 |
3 |
4 |

MarketPlace

5 |
8 |
9 |
-------------------------------------------------------------------------------- /templates/payment/cancel.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |

Pago no exitoso

5 |

Lo sentimos, tu pago no fue exitoso, intenta de nuevo

6 | 7 | 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /templates/payment/payment.html: -------------------------------------------------------------------------------- 1 |
2 | {% csrf_token %} 3 | {{ form.as_p }} 4 |

5 |
6 | -------------------------------------------------------------------------------- /templates/payment/success.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 |

Gracias por tu compra

5 |

No olvides, checar tu correo para estar al tanto de tu compra

6 | 7 | 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /templates/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block content %} 6 |
7 |
9 |
10 |
AisModa. 12 |
13 |
14 |
15 | {% csrf_token %} 16 |
17 | 18 | 20 |
21 | 22 |
23 | 24 | 26 | 27 |
28 | {% if user_registration.errors %} 29 |
30 | {{ user_registration.errors }} 31 |
32 | {% endif %} 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 |
No have acount? 42 | Sing in 43 |
44 |
45 |
46 | 47 | 48 | {% endblock content %} -------------------------------------------------------------------------------- /templates/users/panel.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/templates/users/panel.html -------------------------------------------------------------------------------- /templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 |
10 |
11 |
12 | 14 | AisModa. 15 |
16 |
17 |
18 | {% csrf_token %} 19 |
20 | 21 | {{ user_registration.first_name }} 22 |
23 | 24 |
25 | 26 | {{ user_registration.last_name }} 27 | 28 |
29 |
30 | 31 | {{ user_registration.email }} 32 | 33 |
34 |
35 | 36 | {{ user_registration.password1 }} 37 | 38 |
39 |
40 | 41 | {{ user_registration.password2 }} 42 | 43 | {% if user_registration.errors %} 44 | 45 |
46 | 47 | {{ user_registration.errors }} 48 |
49 | {% endif %} 50 | 51 |
52 |
53 | 54 |
55 | 56 | 57 | 58 |
59 |
Already have acount?Login
61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 | 79 | 80 | 81 | {% endblock content %} -------------------------------------------------------------------------------- /templates/users/seller_register.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 |
10 |

Become Vendor!

11 |
12 | {% csrf_token %} 13 | {{seller_form.as_p}} 14 |
16 |
17 |
18 | 19 | 24 | 25 | {% endblock content %} 26 |
27 | {% csrf_token %} 28 | {{seller_form.as_p}} 29 | 30 |
-------------------------------------------------------------------------------- /templates/users/user_panel.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %}Login{% endblock title %} 4 | 5 | {% block content %} 6 | 7 |
8 | 9 | 10 | 11 | 12 |
13 | 14 |

16 | Login Email: {{ request.user.email}} 17 |

18 | 19 | 20 | View and edi personal acount 21 | 22 |
23 | 24 |


25 | 26 |

compras realizadas

27 | 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 |
#FirstLastHandle
1MarkOtto@mdo
2JacobThornton@fat
3Larry the Bird@twitter
56 | 57 |


58 | 59 |
60 |
Tienda: 62 | {{request.user.seller}} 63 |
64 |
65 | 66 | {% if request.user.seller %} 67 | new product 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {% for product in products %} 82 | 83 | 84 | 85 | 86 | 90 | 91 | 92 | 93 | 94 | 95 | {% if request.user.is_staff or request.user.seller %} 96 | 102 | 107 | {% endif %} 108 | 109 | 110 | {% endfor %} 111 | 112 |
CategoríaNombrePrecioStock
{{product.category}} 87 | 89 | {{product.price}}{{product.stock}} 97 | 98 | 100 | 101 | 103 | 104 | 106 |
113 | 114 | {% else %} 115 | Aún no has creado tu tienda 116 | 117 | Become a vendor 118 | 119 | {% endif %} 120 | 121 |
122 | {% endblock content %} -------------------------------------------------------------------------------- /templates/users/users_edit.html: -------------------------------------------------------------------------------- 1 | {% extends '../base.html' %} 2 | 3 | {% block title %}Cambia tus datos{% endblock title %} 4 | 5 | {% block content %} 6 | 7 | {% if form.errors %} 8 | {{ form.errors }} 9 | {% endif %} 10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 | {% csrf_token %} 18 |
19 | 20 | {{ user_form.first_name }} 21 |
22 |
23 | 24 | {{ user_form.last_name }} 25 |
26 | 27 |
28 | 29 | {{ user_form.email }} 30 |
31 | {% if request.user.seller %} 32 |
33 | 34 | {{ seller_form.seller_name }} 35 |
36 | {% endif %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 | {% endblock content %} -------------------------------------------------------------------------------- /users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/users/__init__.py -------------------------------------------------------------------------------- /users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.admin import UserAdmin 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | from .models import Seller 7 | 8 | 9 | class CustomUserAdmin(UserAdmin): 10 | """Define admin model for custom User model with no username field.""" 11 | 12 | fieldsets = ( 13 | (None, {"fields": ("email", "password")}), 14 | (_("Personal info"), {"fields": ("first_name", "last_name")}), 15 | (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}), 16 | (_("Important dates"), {"fields": ("last_login", "date_joined")}), 17 | ) 18 | add_fieldsets = ((None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),) 19 | list_display = ("email", "first_name", "last_name", "is_staff") 20 | search_fields = ("email", "first_name", "last_name") 21 | ordering = ("email",) 22 | 23 | 24 | admin.site.register(get_user_model(), CustomUserAdmin) 25 | admin.site.register(Seller) 26 | -------------------------------------------------------------------------------- /users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "users" 7 | -------------------------------------------------------------------------------- /users/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from django.shortcuts import get_object_or_404 4 | from rest_framework.exceptions import PermissionDenied 5 | 6 | from users.models import CustomUser 7 | 8 | 9 | def own_user_required(func): 10 | @functools.wraps(func) 11 | def wrapper(request, *args, **kwargs): 12 | user = get_object_or_404(CustomUser, pk=int(kwargs["pk"])) 13 | 14 | if request.method == "GET": 15 | return func(request, *args, **kwargs) 16 | if request.user == user: 17 | return func(request, *args, **kwargs) 18 | raise PermissionDenied("Usted no es el propietario de este objeto") 19 | 20 | return wrapper 21 | -------------------------------------------------------------------------------- /users/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | 4 | from .models import CustomUser, Seller 5 | 6 | 7 | class UserForm(UserCreationForm): 8 | class Meta: 9 | model = CustomUser 10 | fields = ("first_name", "last_name", "email", "password1", "password2") 11 | 12 | 13 | class UserEditForm(forms.ModelForm): 14 | class Meta: 15 | model = CustomUser 16 | fields = ("first_name", "last_name", "email") 17 | 18 | 19 | class SellerForm(forms.ModelForm): 20 | class Meta: 21 | model = Seller 22 | fields = ("seller_name",) 23 | -------------------------------------------------------------------------------- /users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.3 on 2022-05-18 01:13 2 | 3 | import django.db.models.deletion 4 | import django.utils.timezone 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [("auth", "0012_alter_user_first_name_max_length")] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="CustomUser", 18 | fields=[ 19 | ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 20 | ("password", models.CharField(max_length=128, verbose_name="password")), 21 | ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), 22 | ( 23 | "is_superuser", 24 | models.BooleanField( 25 | default=False, 26 | help_text="Designates that this user has all permissions without explicitly assigning them.", 27 | verbose_name="superuser status", 28 | ), 29 | ), 30 | ("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")), 31 | ("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")), 32 | ( 33 | "is_staff", 34 | models.BooleanField( 35 | default=False, 36 | help_text="Designates whether the user can log into this admin site.", 37 | verbose_name="staff status", 38 | ), 39 | ), 40 | ( 41 | "is_active", 42 | models.BooleanField( 43 | default=True, 44 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 45 | verbose_name="active", 46 | ), 47 | ), 48 | ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")), 49 | ("email", models.EmailField(max_length=254, unique=True, verbose_name="email address")), 50 | ( 51 | "groups", 52 | models.ManyToManyField( 53 | blank=True, 54 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 55 | related_name="user_set", 56 | related_query_name="user", 57 | to="auth.group", 58 | verbose_name="groups", 59 | ), 60 | ), 61 | ( 62 | "user_permissions", 63 | models.ManyToManyField( 64 | blank=True, 65 | help_text="Specific permissions for this user.", 66 | related_name="user_set", 67 | related_query_name="user", 68 | to="auth.permission", 69 | verbose_name="user permissions", 70 | ), 71 | ), 72 | ], 73 | options={"verbose_name": "user", "verbose_name_plural": "users", "abstract": False}, 74 | ), 75 | migrations.CreateModel( 76 | name="Seller", 77 | fields=[ 78 | ( 79 | "profile", 80 | models.OneToOneField( 81 | on_delete=django.db.models.deletion.CASCADE, 82 | primary_key=True, 83 | related_name="seller", 84 | serialize=False, 85 | to=settings.AUTH_USER_MODEL, 86 | ), 87 | ), 88 | ("seller_name", models.CharField(max_length=50, unique=True)), 89 | ], 90 | ), 91 | ] 92 | -------------------------------------------------------------------------------- /users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Python-en-equipo/MarketPlace/aaed49de28e1a0303d92f10536d6dd05ab7a08b8/users/migrations/__init__.py -------------------------------------------------------------------------------- /users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser, BaseUserManager 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | # administra al customuser 7 | class CustomUserManager(BaseUserManager): 8 | """Define a model manager for User model with no username field.""" 9 | 10 | def _create_user(self, email, password=None, **extra_fields): 11 | """Create and save a User with the given email and password.""" 12 | if not email: 13 | raise ValueError("The given email must be set") 14 | email = self.normalize_email(email) 15 | user = self.model(email=email, **extra_fields) 16 | user.set_password(password) 17 | user.save(using=self._db) 18 | return user 19 | 20 | def create_user(self, email, password=None, **extra_fields): 21 | extra_fields.setdefault("is_staff", False) 22 | extra_fields.setdefault("is_superuser", False) 23 | return self._create_user(email, password, **extra_fields) 24 | 25 | def create_superuser(self, email, password=None, **extra_fields): 26 | """Create and save a SuperUser with the given email and password.""" 27 | extra_fields.setdefault("is_staff", True) 28 | extra_fields.setdefault("is_superuser", True) 29 | if extra_fields.get("is_staff") is not True: 30 | raise ValueError("Superuser must have is_staff=True.") 31 | if extra_fields.get("is_superuser") is not True: 32 | raise ValueError("Superuser must have is_superuser=True.") 33 | return self._create_user(email, password, **extra_fields) 34 | 35 | 36 | # user general que se autenticará con email 37 | class CustomUser(AbstractUser): 38 | username = None 39 | email = models.EmailField(_("email address"), unique=True) 40 | 41 | USERNAME_FIELD = "email" 42 | REQUIRED_FIELDS = [] 43 | 44 | # necesita de un manager que lo administre para crearlo con ciertos campos 45 | objects = CustomUserManager() 46 | 47 | 48 | class Seller(models.Model): 49 | profile = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name="seller", primary_key=True) 50 | seller_name = models.CharField(max_length=50, unique=True) 51 | # is_active = models.BooleanField(default=False) 52 | 53 | def __str__(self): 54 | return f"{self.seller_name}" 55 | -------------------------------------------------------------------------------- /users/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import SAFE_METHODS, BasePermission 2 | 3 | 4 | class IsOwnerUserPermission(BasePermission): 5 | message = "You do not have permission to manipulate user given" 6 | 7 | def has_object_permission(self, request, view, obj): 8 | 9 | if request.method in SAFE_METHODS: 10 | return True 11 | 12 | return obj == request.user 13 | 14 | 15 | class IsOwnerSellerPermission(BasePermission): 16 | message = "You do not have permission to do this" 17 | 18 | def has_object_permission(self, request, view, obj): 19 | 20 | if request.method in SAFE_METHODS: 21 | return True 22 | 23 | return obj.seller == request.user 24 | -------------------------------------------------------------------------------- /users/serializers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from rest_framework import serializers 3 | from rest_framework.validators import UniqueValidator 4 | 5 | from users.models import CustomUser, Seller 6 | 7 | 8 | class UserCreateSerializer(serializers.ModelSerializer): 9 | email = serializers.EmailField(required=True, validators=[UniqueValidator(queryset=CustomUser.objects.all())]) 10 | password2 = serializers.CharField(write_only=True) 11 | 12 | class Meta: 13 | model = CustomUser 14 | fields = ("email", "first_name", "last_name", "password", "password2") 15 | extra_kwargs = { 16 | "email": {"required": True}, 17 | "first_name": {"required": True}, 18 | "last_name": {"required": True}, 19 | "password": {"write_only": True}, 20 | } 21 | 22 | def create(self, validated_data): 23 | # validate password 24 | if validated_data["password"] != validated_data["password2"]: 25 | raise serializers.ValidationError({"password": "Las contraseñas no coinciden"}) 26 | 27 | user = CustomUser.objects.create_user( 28 | email=validated_data["email"], 29 | first_name=validated_data["first_name"], 30 | last_name=validated_data["last_name"], 31 | ) 32 | 33 | user.set_password(validated_data["password"]) 34 | 35 | user.save() 36 | 37 | return user 38 | 39 | 40 | class UserSerializer(serializers.ModelSerializer): 41 | class Meta: 42 | model = CustomUser 43 | fields = ("id", "email", "first_name", "last_name") 44 | extra_kwargs = {"email": {"required": True}, "first_name": {"required": True}, "last_name": {"required": True}} 45 | 46 | # update user 47 | def update(self, instance, validated_data): 48 | instance.email = validated_data.get("email", instance.email) 49 | instance.first_name = validated_data.get("first_name", instance.first_name) 50 | instance.last_name = validated_data.get("last_name", instance.last_name) 51 | 52 | instance.save() 53 | 54 | return instance 55 | 56 | 57 | class CreateSellerSerializer(serializers.ModelSerializer): 58 | class Meta: 59 | model = Seller 60 | fields = ("seller_name",) 61 | 62 | def create(self, validated_data): 63 | user = CustomUser.objects.get(id=self.context["request"].user.id) 64 | print(user) 65 | 66 | try: 67 | seller = Seller.objects.create(profile=user, seller_name=validated_data["seller_name"]) 68 | 69 | return seller 70 | except ObjectDoesNotExist: 71 | error = {"message": "The user is already a seller"} 72 | raise serializers.ValidationError(error) 73 | 74 | 75 | class SellerSerializer(serializers.ModelSerializer): 76 | profile = UserSerializer(read_only=True) 77 | 78 | class Meta: 79 | model = Seller 80 | fields = ("seller_name", "profile") 81 | -------------------------------------------------------------------------------- /users/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | 5 | from .models import CustomUser, Seller 6 | 7 | 8 | class UserTests(APITestCase): 9 | def setUp(self) -> None: 10 | # Create a user 11 | self.user = CustomUser.objects.create_user( 12 | email="mark@mail.com", first_name="Mark", last_name="Bruen", password="123456" 13 | ) 14 | 15 | # Create a seller 16 | self.seller = Seller.objects.create(seller_name="Mark Store", profile=self.user) 17 | 18 | # Create a token 19 | url = reverse_lazy("users:login") 20 | response = self.client.post(url, {"email": "mark@mail.com", "password": "123456"}, format="json") 21 | 22 | # Set the token in the header 23 | self.token = response.data["access"] 24 | self.client.credentials(HTTP_AUTHORIZATION="Bearer " + self.token) 25 | 26 | def test_user_detail(self): 27 | # set up 28 | user = CustomUser.objects.create_user( 29 | email="steve@mail.com", first_name="Steve", last_name="Jobs", password="123456" 30 | ) 31 | user.save() 32 | 33 | # test 34 | url = reverse_lazy("users:user-detail", kwargs={"pk": 2}) 35 | response = self.client.get(url) 36 | 37 | self.assertEqual(response.status_code, status.HTTP_200_OK) 38 | self.assertEqual(response.data["email"], user.email) 39 | self.assertEqual(response.data["first_name"], user.first_name) 40 | 41 | def test_user_create(self): 42 | url = reverse_lazy("users:user-create") 43 | data = { 44 | "email": "steve@mail.com", 45 | "first_name": "Steve", 46 | "last_name": "Jobs", 47 | "password": "123456", 48 | "password2": "123456", 49 | } 50 | response = self.client.post(url, data, format="json") 51 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 52 | self.assertEqual(response.data["email"], data["email"]) 53 | self.assertEqual(response.data["first_name"], data["first_name"]) 54 | 55 | def test_user_login(self): 56 | 57 | # first we're going to create a user 58 | url = reverse_lazy("users:user-create") 59 | user_data = { 60 | "email": "dalto@gmail.com", 61 | "first_name": "Dalto", 62 | "last_name": "Ruecker", 63 | "password": "123456", 64 | "password2": "123456", 65 | } 66 | 67 | response = self.client.post(url, user_data, format="json") 68 | 69 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 70 | 71 | url = reverse_lazy("users:login") 72 | credentials_data = {"email": "dalto@gmail.com", "password": "123456"} 73 | 74 | response = self.client.post(url, credentials_data, format="json") 75 | 76 | self.assertEqual(response.status_code, status.HTTP_200_OK) 77 | 78 | def test_user_update(self): 79 | url = reverse_lazy("users:user-detail", kwargs={"pk": 1}) 80 | data = {"email": "test@mail.com", "first_name": "Test", "last_name": "Test"} 81 | response = self.client.put(url, data, format="json") 82 | self.assertEqual(response.status_code, status.HTTP_200_OK) 83 | self.assertEqual(response.data["email"], data["email"]) 84 | self.assertEqual(response.data["first_name"], data["first_name"]) 85 | 86 | def test_user_delete(self): 87 | url = reverse_lazy("users:user-detail", kwargs={"pk": 1}) 88 | response = self.client.delete(url) 89 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 90 | 91 | def test_seller_detail(self): 92 | url = reverse_lazy("users:seller-detail", kwargs={"pk": 1}) 93 | response = self.client.get(url) 94 | self.assertEqual(response.status_code, status.HTTP_200_OK) 95 | self.assertEqual(response.data["seller_name"], "Mark Store") 96 | self.assertEqual(response.data["profile"]["email"], self.user.email) 97 | 98 | def test_permission_user(self): 99 | 100 | CustomUser.objects.create_user(email="test@mail.com", first_name="Test", last_name="Test", password="123456") 101 | 102 | data = {"email": "test@mail.com", "first_name": "Test", "last_name": "Test", "password": "123456"} 103 | 104 | url = reverse_lazy("users:user-detail", kwargs={"pk": 2}) 105 | 106 | updateResponse = self.client.put(url, data, format="json") 107 | deleteReponse = self.client.delete(url) 108 | 109 | self.assertEqual(updateResponse.status_code, status.HTTP_403_FORBIDDEN) 110 | self.assertEqual(deleteReponse.status_code, status.HTTP_403_FORBIDDEN) 111 | -------------------------------------------------------------------------------- /users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 3 | 4 | from . import views 5 | 6 | app_name = "users" 7 | 8 | urlpatterns = [ 9 | path("users/", views.UserRetrieveUpdateDestroyAPIView.as_view(), name="user-detail"), 10 | path("users/", views.UserCreateAPIView.as_view(), name="user-create"), 11 | path("users/token/", TokenObtainPairView.as_view(), name="login"), 12 | path("users/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), 13 | path("sellers/", views.SellerRetirieveUpdateDestroyAPIView.as_view(), name="seller-detail"), 14 | path("sellers/", views.SellerCreateAPIView.as_view(), name="seller-create"), 15 | ] 16 | -------------------------------------------------------------------------------- /users/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.generics import CreateAPIView, RetrieveUpdateDestroyAPIView 2 | from rest_framework.permissions import IsAuthenticated 3 | 4 | from .models import CustomUser, Seller 5 | from .permissions import IsOwnerSellerPermission, IsOwnerUserPermission 6 | from .serializers import CreateSellerSerializer, SellerSerializer, UserCreateSerializer, UserSerializer 7 | 8 | 9 | class UserCreateAPIView(CreateAPIView): 10 | serializer_class = UserCreateSerializer 11 | 12 | 13 | class UserRetrieveUpdateDestroyAPIView(RetrieveUpdateDestroyAPIView): 14 | serializer_class = UserSerializer 15 | queryset = CustomUser.objects.all() 16 | permission_classes = [IsOwnerUserPermission, IsAuthenticated] 17 | 18 | 19 | class SellerCreateAPIView(CreateAPIView): 20 | serializer_class = CreateSellerSerializer 21 | permission_classes = [IsAuthenticated] 22 | 23 | 24 | class SellerRetirieveUpdateDestroyAPIView(RetrieveUpdateDestroyAPIView): 25 | serializer_class = SellerSerializer 26 | queryset = Seller.objects.all() 27 | permission_classes = [IsAuthenticated, IsOwnerSellerPermission] 28 | --------------------------------------------------------------------------------