mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-18 20:39:36 +02:00
migration to new backend
This commit is contained in:
parent
28a5d423c2
commit
9abe9fb315
309 changed files with 21476 additions and 24132 deletions
|
@ -1,12 +1,13 @@
|
||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
|
// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu
|
||||||
{
|
{
|
||||||
"name": "AdventureLog",
|
"name": "Ubuntu",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
||||||
"features": {
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
"ghcr.io/devcontainers/features/node:1": {},
|
||||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
"ghcr.io/devcontainers/features/python:1": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.gitattributes
|
|
||||||
README.md
|
|
||||||
.npmrc
|
|
||||||
.prettierrc
|
|
||||||
.eslintrc.cjs
|
|
||||||
.graphqlrc
|
|
||||||
.editorconfig
|
|
||||||
.svelte-kit
|
|
||||||
.vscode
|
|
||||||
node_modules
|
|
||||||
build
|
|
||||||
package
|
|
||||||
**/.env
|
|
|
@ -1,9 +0,0 @@
|
||||||
DATABASE_URL=
|
|
||||||
|
|
||||||
# *** COMPATIBLE PROVIDERS: aws, gcp, digitalocean, supabase, minio (local via docker) ***
|
|
||||||
# Want another? - open an issue!
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_S3_ENDPOINT=
|
|
||||||
AWS_REGION=
|
|
31
.github/workflows/docker-publish.yml
vendored
31
.github/workflows/docker-publish.yml
vendored
|
@ -1,31 +0,0 @@
|
||||||
name: Upload Docker image to GHCR
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: "adventurelog"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
upload:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.ACESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: docker build -t $IMAGE_NAME:${{ github.event.release.tag_name }} .
|
|
||||||
|
|
||||||
- name: Tag Docker image
|
|
||||||
run: docker tag $IMAGE_NAME:${{ github.event.release.tag_name }} ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:${{ github.event.release.tag_name }}
|
|
||||||
|
|
||||||
- name: Push Docker image to GHCR
|
|
||||||
run: docker push ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:${{ github.event.release.tag_name }}
|
|
25
.github/workflows/tag-latest.yml
vendored
25
.github/workflows/tag-latest.yml
vendored
|
@ -1,25 +0,0 @@
|
||||||
name: Update Latest Tag
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-latest-tag:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Set up Git user
|
|
||||||
run: |
|
|
||||||
git config user.name "${{ github.actor }}"
|
|
||||||
git config user.email "${{ github.actor }}@users.noreply.github.com"
|
|
||||||
- name: Run latest-tag
|
|
||||||
uses: EndBug/latest-tag@latest
|
|
||||||
with:
|
|
||||||
# You can change the name of the tag or branch with this input. Default is 'latest'
|
|
||||||
ref: latest
|
|
||||||
# If a description is provided, the action will use it to create an annotated tag. If none is given, the action will create a lightweight tag.
|
|
||||||
description: Latest release
|
|
||||||
# Force-update a branch instead of using a tag. Default is false.
|
|
||||||
force-branch: true
|
|
|
@ -31,21 +31,21 @@ orientation.
|
||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to creating a positive environment
|
||||||
include:
|
include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
- Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
advances
|
advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
- Publishing others' private information, such as a physical or electronic
|
||||||
address, without explicit permission
|
address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
### Our Responsibilities
|
### Our Responsibilities
|
||||||
|
|
|
@ -11,7 +11,7 @@ _**⚠️ AdventureLog is in early development and is not recommended for produc
|
||||||
### Docker 🐋 (Recomended)
|
### Docker 🐋 (Recomended)
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
2. Edit the `docker-compose.yml` file and change the database password
|
2. Edit the `docker-compose.yml` file and change the database password as well as the django secret key
|
||||||
3. Run `docker compose up -d` to build the image and start the container
|
3. Run `docker compose up -d` to build the image and start the container
|
||||||
4. Wait for the app to start up and migrate then visit the port and enjoy!
|
4. Wait for the app to start up and migrate then visit the port and enjoy!
|
||||||
5. After navigating to the app, fill out the form to create the admin user.
|
5. After navigating to the app, fill out the form to create the admin user.
|
||||||
|
@ -20,7 +20,7 @@ _**⚠️ AdventureLog is in early development and is not recommended for produc
|
||||||
|
|
||||||
## About AdventureLog
|
## About AdventureLog
|
||||||
|
|
||||||
AdventureLog is a Svelte Kit application that utilizes a PostgreSQL database. Users can log the adventures they have experienced, as well as plan future ones. Key features include:
|
AdventureLog is a Svelte Kit and Django application that utilizes a PostgreSQL database. Users can log the adventures they have experienced, as well as plan future ones. Key features include:
|
||||||
|
|
||||||
- Logging past adventures with fields like name, date, location, description, and rating.
|
- Logging past adventures with fields like name, date, location, description, and rating.
|
||||||
- Planning future adventures with similar fields.
|
- Planning future adventures with similar fields.
|
||||||
|
@ -31,7 +31,7 @@ AdventureLog aims to be your ultimate travel companion, helping you document you
|
||||||
|
|
||||||
AdventureLog is licensed under the GNU General Public License v3.0.
|
AdventureLog is licensed under the GNU General Public License v3.0.
|
||||||
|
|
||||||
## Screenshots 🖼️
|
<!-- ## Screenshots 🖼️
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
@ -43,4 +43,4 @@ AdventureLog is licensed under the GNU General Public License v3.0.
|
||||||
- Improved mobile device support
|
- Improved mobile device support
|
||||||
- Password reset functionality
|
- Password reset functionality
|
||||||
- Improved error handling
|
- Improved error handling
|
||||||
- Handling of adventure cards with variable width
|
- Handling of adventure cards with variable width -->
|
||||||
|
|
25
backend/.devcontainer/devcontainer.json
Normal file
25
backend/.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||||
|
{
|
||||||
|
"name": "Python 3",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
|
||||||
|
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "pip3 install --user -r requirements.txt",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
12
backend/.github/dependabot.yml
vendored
Normal file
12
backend/.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
116
backend/.gitignore
vendored
Normal file
116
backend/.gitignore
vendored
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
coverage_html/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
demo/react-spa/node_modules/
|
||||||
|
demo/react-spa/yarn.lock
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
*/media/*
|
1
backend/AUTHORS
Normal file
1
backend/AUTHORS
Normal file
|
@ -0,0 +1 @@
|
||||||
|
http://github.com/iMerica/dj-rest-auth/contributors
|
30
backend/Dockerfile
Normal file
30
backend/Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Dockerfile
|
||||||
|
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
LABEL Developers="Sean Morley"
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y git postgresql-client \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY ./server/requirements.txt /code/
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy the Django project code into the Docker image
|
||||||
|
COPY ./server /code/
|
||||||
|
|
||||||
|
# Set the entrypoint script
|
||||||
|
COPY ./entrypoint.sh /code/entrypoint.sh
|
||||||
|
RUN chmod +x /code/entrypoint.sh
|
||||||
|
ENTRYPOINT ["/code/entrypoint.sh"]
|
21
backend/LICENSE
Normal file
21
backend/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 iMerica https://github.com/iMerica/
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
5
backend/MANIFEST.in
Normal file
5
backend/MANIFEST.in
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
include AUTHORS
|
||||||
|
include LICENSE
|
||||||
|
include MANIFEST.in
|
||||||
|
include README.md
|
||||||
|
graft dj_rest_auth
|
4
backend/README.md
Normal file
4
backend/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# AdventureLog Django Backend
|
||||||
|
A demo of a possible AdventureLog 2.0 version using Django as the backend with a REST API.
|
||||||
|
|
||||||
|
Based of django-rest-framework and dj-rest-auth.
|
32
backend/docker-compose.yml
Normal file
32
backend/docker-compose.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: database
|
||||||
|
POSTGRES_USER: adventure
|
||||||
|
POSTGRES_PASSWORD: changeme123
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- PGHOST=db
|
||||||
|
- PGDATABASE=database
|
||||||
|
- PGUSER=adventure
|
||||||
|
- PGPASSWORD=changeme123
|
||||||
|
- SECRET_KEY=changeme123
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
volumes:
|
||||||
|
- adventurelog_media:/code/media/
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
adventurelog_media:
|
||||||
|
driver: local
|
37
backend/entrypoint.sh
Normal file
37
backend/entrypoint.sh
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Function to check PostgreSQL availability
|
||||||
|
check_postgres() {
|
||||||
|
PGPASSWORD=$PGPASSWORD psql -h "$PGHOST" -U "$PGUSER" -d "$PGDATABASE" -c '\q' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for PostgreSQL to become available
|
||||||
|
until check_postgres; do
|
||||||
|
>&2 echo "PostgreSQL is unavailable - sleeping"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
>&2 echo "PostgreSQL is up - continuing..."
|
||||||
|
|
||||||
|
# Apply Django migrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Check for default data
|
||||||
|
python manage.py worldtravel-seed
|
||||||
|
|
||||||
|
# Create superuser if environment variables are set
|
||||||
|
if [ -n "$DJANGO_ADMIN_USERNAME" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ]; then
|
||||||
|
echo "Creating superuser..."
|
||||||
|
python manage.py shell << EOF
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
if not User.objects.filter(username='$DJANGO_ADMIN_USERNAME').exists():
|
||||||
|
User.objects.create_superuser('$DJANGO_ADMIN_USERNAME', '$DJANGO_ADMIN_EMAIL', '$DJANGO_ADMIN_PASSWORD')
|
||||||
|
print("Superuser created successfully.")
|
||||||
|
else:
|
||||||
|
print("Superuser already exists.")
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Django server
|
||||||
|
python manage.py runserver 0.0.0.0:8000
|
8
backend/server/.env.example
Normal file
8
backend/server/.env.example
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
PGHOST=''
|
||||||
|
PGDATABASE=''
|
||||||
|
PGUSER=''
|
||||||
|
PGPASSWORD=''
|
||||||
|
|
||||||
|
SECRET_KEY='pleasechangethisbecauseifyoudontitwillbeverybadandyouwillgethackedinlessthanaminuteguaranteed'
|
||||||
|
|
||||||
|
PUBLIC_URL='http://127.0.0.1:8000'
|
0
backend/server/adventures/__init__.py
Normal file
0
backend/server/adventures/__init__.py
Normal file
66
backend/server/adventures/admin.py
Normal file
66
backend/server/adventures/admin.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import mark_safe
|
||||||
|
from .models import Adventure
|
||||||
|
from worldtravel.models import Country, Region, VisitedRegion
|
||||||
|
|
||||||
|
|
||||||
|
class AdventureAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'type', 'user_id', 'date', 'image_display')
|
||||||
|
list_filter = ('type', 'user_id')
|
||||||
|
|
||||||
|
def image_display(self, obj):
|
||||||
|
if obj.image:
|
||||||
|
return mark_safe(f'<img src="{obj.image.url}" width="100px" height="100px"')
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
image_display.short_description = 'Image Preview'
|
||||||
|
|
||||||
|
|
||||||
|
class CountryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'country_code', 'continent', 'number_of_regions')
|
||||||
|
list_filter = ('continent', 'country_code')
|
||||||
|
|
||||||
|
def number_of_regions(self, obj):
|
||||||
|
return Region.objects.filter(country=obj).count()
|
||||||
|
|
||||||
|
number_of_regions.short_description = 'Number of Regions'
|
||||||
|
|
||||||
|
|
||||||
|
class RegionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'country', 'number_of_visits')
|
||||||
|
# list_filter = ('country', 'number_of_visits')
|
||||||
|
|
||||||
|
def number_of_visits(self, obj):
|
||||||
|
return VisitedRegion.objects.filter(region=obj).count()
|
||||||
|
|
||||||
|
number_of_visits.short_description = 'Number of Visits'
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
model = CustomUser
|
||||||
|
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
||||||
|
fieldsets = UserAdmin.fieldsets + (
|
||||||
|
(None, {'fields': ('profile_pic',)}),
|
||||||
|
)
|
||||||
|
def image_display(self, obj):
|
||||||
|
if obj.profile_pic:
|
||||||
|
return mark_safe(f'<img src="{obj.profile_pic.url}" width="100px" height="100px"')
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
admin.site.register(CustomUser, CustomUserAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Adventure, AdventureAdmin)
|
||||||
|
admin.site.register(Country, CountryAdmin)
|
||||||
|
admin.site.register(Region, RegionAdmin)
|
||||||
|
admin.site.register(VisitedRegion)
|
||||||
|
|
||||||
|
admin.site.site_header = 'AdventureLog Admin'
|
||||||
|
admin.site.site_title = 'AdventureLog Admin Site'
|
||||||
|
admin.site.index_title = 'Welcome to AdventureLog Admin Page'
|
6
backend/server/adventures/apps.py
Normal file
6
backend/server/adventures/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AdventuresConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "adventures"
|
0
backend/server/adventures/management/__init__.py
Normal file
0
backend/server/adventures/management/__init__.py
Normal file
50
backend/server/adventures/management/commands/travel-seed.py
Normal file
50
backend/server/adventures/management/commands/travel-seed.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# myapp/management/commands/seed.py
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from adventures.models import Adventure
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Imports the featured adventures'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
User = get_user_model()
|
||||||
|
username = input(
|
||||||
|
"Enter a username to own the featured adventures: ")
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.get(username=username)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
self.stdout.write(self.style.ERROR(
|
||||||
|
f'User with username "{username}" does not exist.'))
|
||||||
|
return
|
||||||
|
|
||||||
|
adventures = [
|
||||||
|
('Yellowstone National Park', 'Wyoming, Montana, Idaho, USA', 'featured'),
|
||||||
|
('Yosemite National Park', 'California, USA', 'featured'),
|
||||||
|
('Banff National Park', 'Alberta, Canada', 'featured'),
|
||||||
|
('Kruger National Park', 'Limpopo, South Africa', 'featured'),
|
||||||
|
('Grand Canyon National Park', 'Arizona, USA', 'featured'),
|
||||||
|
('Great Smoky Mountains National Park',
|
||||||
|
'North Carolina, Tennessee, USA', 'featured'),
|
||||||
|
('Zion National Park', 'Utah, USA', 'featured'),
|
||||||
|
('Glacier National Park', 'Montana, USA', 'featured'),
|
||||||
|
('Rocky Mountain National Park', 'Colorado, USA', 'featured'),
|
||||||
|
('Everglades National Park', 'Florida, USA', 'featured'),
|
||||||
|
('Arches National Park', 'Utah, USA', 'featured'),
|
||||||
|
('Acadia National Park', 'Maine, USA', 'featured'),
|
||||||
|
('Sequoia National Park', 'California, USA', 'featured'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, location, type_ in adventures:
|
||||||
|
Adventure.objects.create(
|
||||||
|
user_id=user,
|
||||||
|
name=name,
|
||||||
|
location=location,
|
||||||
|
type=type_,
|
||||||
|
is_public=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
'Successfully inserted featured adventures!'))
|
13
backend/server/adventures/middleware.py
Normal file
13
backend/server/adventures/middleware.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class AppVersionMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
# Process request (if needed)
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Add custom header to response
|
||||||
|
# Replace with your app version
|
||||||
|
response['X-AdventureLog-Version'] = '1.0.0'
|
||||||
|
|
||||||
|
return response
|
31
backend/server/adventures/migrations/0001_initial.py
Normal file
31
backend/server/adventures/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 01:01
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Adventure',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('type', models.CharField(max_length=100)),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('location', models.CharField(blank=True, max_length=200, null=True)),
|
||||||
|
('activity_types', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, null=True, size=None)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('rating', models.FloatField(blank=True, null=True)),
|
||||||
|
('link', models.URLField(blank=True, null=True)),
|
||||||
|
('image', models.ImageField(blank=True, null=True, upload_to='images/')),
|
||||||
|
('date', models.DateField(blank=True, null=True)),
|
||||||
|
('trip_id', models.IntegerField(blank=True, null=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
23
backend/server/adventures/migrations/0002_initial.py
Normal file
23
backend/server/adventures/migrations/0002_initial.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 01:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='user_id',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 15:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0002_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='is_public',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 18:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('adventures', '0003_adventure_is_public'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='adventure',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||||
|
),
|
||||||
|
]
|
0
backend/server/adventures/migrations/__init__.py
Normal file
0
backend/server/adventures/migrations/__init__.py
Normal file
33
backend/server/adventures/models.py
Normal file
33
backend/server/adventures/models.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
|
||||||
|
|
||||||
|
# Assuming you have a default user ID you want to use
|
||||||
|
default_user_id = 1 # Replace with an actual user ID
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Adventure(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
user_id = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, default=default_user_id)
|
||||||
|
type = models.CharField(max_length=100)
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
location = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
activity_types = ArrayField(models.CharField(
|
||||||
|
max_length=100), blank=True, null=True)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
rating = models.FloatField(blank=True, null=True)
|
||||||
|
link = models.URLField(blank=True, null=True)
|
||||||
|
image = models.ImageField(null=True, blank=True, upload_to='images/')
|
||||||
|
date = models.DateField(blank=True, null=True)
|
||||||
|
trip_id = models.IntegerField(blank=True, null=True)
|
||||||
|
is_public = models.BooleanField(default=False)
|
||||||
|
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||||
|
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
19
backend/server/adventures/serializers.py
Normal file
19
backend/server/adventures/serializers.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import os
|
||||||
|
from .models import Adventure
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
class AdventureSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Adventure
|
||||||
|
fields = '__all__' # Serialize all fields of the Adventure model
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
representation = super().to_representation(instance)
|
||||||
|
if instance.image:
|
||||||
|
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||||
|
print(public_url)
|
||||||
|
# remove any ' from the url
|
||||||
|
public_url = public_url.replace("'", "")
|
||||||
|
representation['image'] = f"{public_url}/media/{instance.image.name}"
|
||||||
|
return representation
|
3
backend/server/adventures/tests.py
Normal file
3
backend/server/adventures/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
11
backend/server/adventures/urls.py
Normal file
11
backend/server/adventures/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import AdventureViewSet
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'adventures', AdventureViewSet, basename='adventures')
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
# Include the router under the 'api/' prefix
|
||||||
|
path('', include(router.urls)),
|
||||||
|
]
|
44
backend/server/adventures/views.py
Normal file
44
backend/server/adventures/views.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from .models import Adventure
|
||||||
|
from .serializers import AdventureSerializer
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
class AdventureViewSet(viewsets.ModelViewSet):
|
||||||
|
serializer_class = AdventureSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Allow any user to see public adventures or their own adventures
|
||||||
|
return Adventure.objects.filter(
|
||||||
|
Q(is_public=True) | Q(user_id=self.request.user.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user_id=self.request.user)
|
||||||
|
|
||||||
|
# Custom actions to return visited and planned adventures
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def visited(self, request):
|
||||||
|
visited_adventures = Adventure.objects.filter(
|
||||||
|
type='visited', user_id=request.user.id, trip_id=None)
|
||||||
|
serializer = self.get_serializer(visited_adventures, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def planned(self, request):
|
||||||
|
planned_adventures = Adventure.objects.filter(
|
||||||
|
type='planned', user_id=request.user.id, trip_id=None)
|
||||||
|
serializer = self.get_serializer(planned_adventures, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def featured(self, request):
|
||||||
|
featured_adventures = Adventure.objects.filter(
|
||||||
|
type='featured', is_public=True, trip_id=None)
|
||||||
|
serializer = self.get_serializer(featured_adventures, many=True)
|
||||||
|
return Response(serializer.data)
|
0
backend/server/demo/__init__.py
Normal file
0
backend/server/demo/__init__.py
Normal file
192
backend/server/demo/settings.py
Normal file
192
backend/server/demo/settings.py
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
"""
|
||||||
|
Django settings for demo project.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.7/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from datetime import timedelta
|
||||||
|
from os import getenv
|
||||||
|
# Load environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = getenv('SECRET_KEY')
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
# ALLOWED_HOSTS = [
|
||||||
|
# 'localhost',
|
||||||
|
# '127.0.0.1',
|
||||||
|
# 'server'
|
||||||
|
# ]
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'django.contrib.sites',
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
'dj_rest_auth',
|
||||||
|
'allauth',
|
||||||
|
'allauth.account',
|
||||||
|
'dj_rest_auth.registration',
|
||||||
|
'allauth.socialaccount',
|
||||||
|
'allauth.socialaccount.providers.facebook',
|
||||||
|
'drf_yasg',
|
||||||
|
'corsheaders',
|
||||||
|
'adventures',
|
||||||
|
'worldtravel',
|
||||||
|
'users',
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
MIDDLEWARE = (
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'allauth.account.middleware.AccountMiddleware',
|
||||||
|
)
|
||||||
|
|
||||||
|
# For backwards compatibility for Django 1.8
|
||||||
|
MIDDLEWARE_CLASSES = MIDDLEWARE
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'demo.urls'
|
||||||
|
|
||||||
|
# WSGI_APPLICATION = 'demo.wsgi.application'
|
||||||
|
|
||||||
|
SIMPLE_JWT = {
|
||||||
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||||
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=365),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': getenv('PGDATABASE'),
|
||||||
|
'USER': getenv('PGUSER'),
|
||||||
|
'PASSWORD': getenv('PGPASSWORD'),
|
||||||
|
'HOST': getenv('PGHOST'),
|
||||||
|
'PORT': getenv('PGPORT', 5432),
|
||||||
|
'OPTIONS': {
|
||||||
|
'sslmode': 'prefer', # Prefer SSL, but allow non-SSL connections
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'America/New_York'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
# STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
|
||||||
|
|
||||||
|
# TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
REST_AUTH = {
|
||||||
|
'SESSION_LOGIN': True,
|
||||||
|
'USE_JWT': True,
|
||||||
|
'JWT_AUTH_COOKIE': 'auth',
|
||||||
|
'JWT_AUTH_HTTPONLY': False,
|
||||||
|
'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer',
|
||||||
|
'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer',
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'users.CustomUser'
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
SITE_ID = 1
|
||||||
|
ACCOUNT_EMAIL_REQUIRED = True
|
||||||
|
ACCOUNT_AUTHENTICATION_METHOD = 'username'
|
||||||
|
ACCOUNT_EMAIL_VERIFICATION = 'optional'
|
||||||
|
|
||||||
|
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
|
# EMAIL_HOST = 'smtp.resend.com'
|
||||||
|
# EMAIL_USE_TLS = False
|
||||||
|
# EMAIL_PORT = 2465
|
||||||
|
# EMAIL_USE_SSL = True
|
||||||
|
# EMAIL_HOST_USER = 'resend'
|
||||||
|
# EMAIL_HOST_PASSWORD = ''
|
||||||
|
# DEFAULT_FROM_EMAIL = 'mail@mail.user.com'
|
||||||
|
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
|
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
|
||||||
|
),
|
||||||
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||||
|
# 'DEFAULT_PERMISSION_CLASSES': [
|
||||||
|
# 'rest_framework.permissions.IsAuthenticated',
|
||||||
|
# ],
|
||||||
|
}
|
||||||
|
|
||||||
|
SWAGGER_SETTINGS = {
|
||||||
|
'LOGIN_URL': 'login',
|
||||||
|
'LOGOUT_URL': 'logout',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# For demo purposes only. Use a white list in the real world.
|
||||||
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
71
backend/server/demo/urls.py
Normal file
71
backend/server/demo/urls.py
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
from django.urls import include, re_path, path
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.views.generic import RedirectView, TemplateView
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from adventures import urls as adventures
|
||||||
|
from users.views import ChangeEmailView
|
||||||
|
from .views import get_csrf_token
|
||||||
|
from drf_yasg.views import get_schema_view
|
||||||
|
|
||||||
|
from drf_yasg import openapi
|
||||||
|
|
||||||
|
schema_view = get_schema_view(
|
||||||
|
openapi.Info(
|
||||||
|
title='API Docs',
|
||||||
|
default_version='v1',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
urlpatterns = [
|
||||||
|
path('api/', include('adventures.urls')),
|
||||||
|
path('api/', include('worldtravel.urls')),
|
||||||
|
|
||||||
|
path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'),
|
||||||
|
|
||||||
|
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||||
|
re_path(r'^$', TemplateView.as_view(
|
||||||
|
template_name="home.html"), name='home'),
|
||||||
|
re_path(r'^signup/$', TemplateView.as_view(template_name="signup.html"),
|
||||||
|
name='signup'),
|
||||||
|
re_path(r'^email-verification/$',
|
||||||
|
TemplateView.as_view(template_name="email_verification.html"),
|
||||||
|
name='email-verification'),
|
||||||
|
re_path(r'^login/$', TemplateView.as_view(template_name="login.html"),
|
||||||
|
name='login'),
|
||||||
|
re_path(r'^logout/$', TemplateView.as_view(template_name="logout.html"),
|
||||||
|
name='logout'),
|
||||||
|
re_path(r'^password-reset/$',
|
||||||
|
TemplateView.as_view(template_name="password_reset.html"),
|
||||||
|
name='password-reset'),
|
||||||
|
re_path(r'^password-reset/confirm/$',
|
||||||
|
TemplateView.as_view(template_name="password_reset_confirm.html"),
|
||||||
|
name='password-reset-confirm'),
|
||||||
|
|
||||||
|
re_path(r'^user-details/$',
|
||||||
|
TemplateView.as_view(template_name="user_details.html"),
|
||||||
|
name='user-details'),
|
||||||
|
re_path(r'^password-change/$',
|
||||||
|
TemplateView.as_view(template_name="password_change.html"),
|
||||||
|
name='password-change'),
|
||||||
|
re_path(r'^resend-email-verification/$',
|
||||||
|
TemplateView.as_view(
|
||||||
|
template_name="resend_email_verification.html"),
|
||||||
|
name='resend-email-verification'),
|
||||||
|
|
||||||
|
|
||||||
|
# this url is used to generate email content
|
||||||
|
re_path(r'^password-reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$',
|
||||||
|
TemplateView.as_view(template_name="password_reset_confirm.html"),
|
||||||
|
name='password_reset_confirm'),
|
||||||
|
|
||||||
|
re_path(r'^auth/', include('dj_rest_auth.urls')),
|
||||||
|
re_path(r'^auth/registration/',
|
||||||
|
include('dj_rest_auth.registration.urls')),
|
||||||
|
re_path(r'^account/', include('allauth.urls')),
|
||||||
|
re_path(r'^admin/', admin.site.urls),
|
||||||
|
re_path(r'^accounts/profile/$', RedirectView.as_view(url='/',
|
||||||
|
permanent=True), name='profile-redirect'),
|
||||||
|
re_path(r'^docs/$', schema_view.with_ui('swagger',
|
||||||
|
cache_timeout=0), name='api_docs'),
|
||||||
|
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'),
|
||||||
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
6
backend/server/demo/views.py
Normal file
6
backend/server/demo/views.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.middleware.csrf import get_token
|
||||||
|
|
||||||
|
def get_csrf_token(request):
|
||||||
|
csrf_token = get_token(request)
|
||||||
|
return JsonResponse({'csrfToken': csrf_token})
|
16
backend/server/demo/wsgi.py
Normal file
16
backend/server/demo/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for demo project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
10
backend/server/manage.py
Normal file
10
backend/server/manage.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
11
backend/server/requirements.txt
Normal file
11
backend/server/requirements.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Django==5.0.6
|
||||||
|
dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master
|
||||||
|
djangorestframework>=3.15.2
|
||||||
|
djangorestframework-simplejwt==5.3.1
|
||||||
|
django-allauth==0.63.3
|
||||||
|
drf-yasg==1.21.4
|
||||||
|
django-cors-headers==4.4.0
|
||||||
|
coreapi==2.3.3
|
||||||
|
python-dotenv
|
||||||
|
psycopg2-binary
|
||||||
|
Pillow
|
144
backend/server/templates/base.html
Normal file
144
backend/server/templates/base.html
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="Django-dj-rest-auth demo" />
|
||||||
|
<meta name="author" content="iMerica, Inc." />
|
||||||
|
|
||||||
|
<title>AdventureLog API Server</title>
|
||||||
|
|
||||||
|
<!-- Latest compiled and minified CSS -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Optional theme -->
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
|
||||||
|
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body role="document">
|
||||||
|
<div class="navbar navbar-inverse" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
|
||||||
|
>API endpoints <span class="caret"></span
|
||||||
|
></a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<!-- these pages don't require user token -->
|
||||||
|
<li><a href="{% url 'signup' %}">Signup</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'email-verification' %}">E-mail verification</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'resend-email-verification' %}"
|
||||||
|
>Resend E-mail verification</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li><a href="{% url 'login' %}">Login</a></li>
|
||||||
|
<li><a href="{% url 'password-reset' %}">Password Reset</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'password-reset-confirm' %}"
|
||||||
|
>Password Reset Confirm</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<!-- these pages require user token -->
|
||||||
|
<li><a href="{% url 'user-details' %}">User details</a></li>
|
||||||
|
<li><a href="{% url 'logout' %}">Logout</a></li>
|
||||||
|
<li><a href="{% url 'password-change' %}">Password change</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="navbar-toggle collapsed"
|
||||||
|
data-toggle="collapse"
|
||||||
|
data-target=".navbar-collapse"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="/">AdventureLog API Server</a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li class="active"><a href="/">Demo</a></li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="http://dj-rest-auth.readthedocs.org/en/latest/"
|
||||||
|
>Documentation</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a target="_blank" href="https://github.com/iMerica/dj-rest-auth"
|
||||||
|
>Source code</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li><a target="_blank" href="{% url 'api_docs' %}">API Docs</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!--/.nav-collapse -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container theme-showcase" role="main">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
<!-- Bootstrap core JavaScript
|
||||||
|
================================================== -->
|
||||||
|
<!-- Placed at the end of the document so the pages load faster -->
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var error_response = function (data) {
|
||||||
|
$(".api-response").html(
|
||||||
|
"API Response: " +
|
||||||
|
data.status +
|
||||||
|
" " +
|
||||||
|
data.statusText +
|
||||||
|
"<br/>Content: " +
|
||||||
|
data.responseText
|
||||||
|
);
|
||||||
|
};
|
||||||
|
var susccess_response = function (data) {
|
||||||
|
$(".api-response").html(
|
||||||
|
"API Response: OK<br/>Content: " + JSON.stringify(data)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
$().ready(function () {
|
||||||
|
$("form.ajax-post button[type=submit]").click(function () {
|
||||||
|
var form = $("form.ajax-post");
|
||||||
|
$.post(form.attr("action"), form.serialize())
|
||||||
|
.fail(function (data) {
|
||||||
|
error_response(data);
|
||||||
|
})
|
||||||
|
.done(function (data) {
|
||||||
|
susccess_response(data);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block script %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
8
backend/server/templates/email_verification.html
Normal file
8
backend/server/templates/email_verification.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>E-mail verification</h3><hr/>
|
||||||
|
{% include "fragments/email_verification_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_verify_email' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="key" class="col-sm-2 control-label">Key</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="key" type="text" class="form-control" id="key" placeholder="Key">
|
||||||
|
<p class="help-block">Put here a key which was sent in verification email</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Verify</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
24
backend/server/templates/fragments/login_form.html
Normal file
24
backend/server/templates/fragments/login_form.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_login' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="col-sm-2 control-label">Username</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="username" type="text" class="form-control" id="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password" class="col-sm-2 control-label">Password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="password" type="password" class="form-control" id="password" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
20
backend/server/templates/fragments/logout_form.html
Normal file
20
backend/server/templates/fragments/logout_form.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_logout' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||||
|
<p class="help-block">Token received after login</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
24
backend/server/templates/fragments/password_change_form.html
Normal file
24
backend/server/templates/fragments/password_change_form.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_change' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password1" class="col-sm-2 control-label">Password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="new_password1" type="password" class="form-control" id="new_password1" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password2" class="col-sm-2 control-label">Repeat password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="new_password2" type="password" class="form-control" id="new_password2" placeholder="Repeat password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Set new password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset_confirm' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="uid" class="col-sm-2 control-label">Uid</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="uid" type="text" class="form-control" id="uid" placeholder="Uid">
|
||||||
|
<p class="help-block">Uid value sent in email</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token" class="col-sm-2 control-label">Token</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||||
|
<p class="help-block">Token value sent in email</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password1" class="col-sm-2 control-label">Password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="new_password1" type="password" class="form-control" id="new_password1" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password2" class="col-sm-2 control-label">Repeat password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="new_password2" type="password" class="form-control" id="new_password2" placeholder="Repeat password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Set new password</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
16
backend/server/templates/fragments/password_reset_form.html
Normal file
16
backend/server/templates/fragments/password_reset_form.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_password_reset' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="col-sm-2 control-label">E-mail</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="email" type="text" class="form-control" id="email" placeholder="Email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" role="form" action="{% url 'rest_resend_email' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="col-sm-2 control-label">E-mail</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="email" type="text" class="form-control" id="email" placeholder="Email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Resend</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
38
backend/server/templates/fragments/signup_form.html
Normal file
38
backend/server/templates/fragments/signup_form.html
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal ajax-post" id="signup" role="form" action="{% url 'rest_register' %}">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="col-sm-2 control-label">Email</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="email" type="text" class="form-control" id="email" placeholder="Email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="col-sm-2 control-label">Username</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="username" type="text" class="form-control" id="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password1" class="col-sm-2 control-label">Password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="password1" type="password" class="form-control" id="password1" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password2" class="col-sm-2 control-label">Repeat password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="password2" type="password" class="form-control" id="password2" placeholder="Repeat password">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Sign up</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
39
backend/server/templates/fragments/user_details_form.html
Normal file
39
backend/server/templates/fragments/user_details_form.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!-- Signup form -->
|
||||||
|
<form class="form-horizontal" id="signup" role="form" action="{% url 'rest_user_details' %}">{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email" class="col-sm-2 control-label">Email</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="email" type="text" class="form-control" id="email" placeholder="Email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username" class="col-sm-2 control-label">Username</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="username" type="text" class="form-control" id="username" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="first_name" class="col-sm-2 control-label">First name</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="first_name" type="text" class="form-control" id="first_name" placeholder="First name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="last_name" class="col-sm-2 control-label">Last name</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="last_name" type="text" class="form-control" id="last_name" placeholder="Last name">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
|
<button type="submit" class="btn btn-default">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group api-response"></div>
|
||||||
|
</form>
|
10
backend/server/templates/home.html
Normal file
10
backend/server/templates/home.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||||
|
<div class="jumbotron">
|
||||||
|
<h1>AdventureLog API Server</h1>
|
||||||
|
<p>Welcome to the server side of AdventureLog!</p>
|
||||||
|
<p>This site is only ment for administrative users</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
8
backend/server/templates/login.html
Normal file
8
backend/server/templates/login.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Login</h3><hr/>
|
||||||
|
{% include "fragments/login_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
8
backend/server/templates/logout.html
Normal file
8
backend/server/templates/logout.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Logout</h3><hr/>
|
||||||
|
{% include "fragments/logout_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
39
backend/server/templates/password_change.html
Normal file
39
backend/server/templates/password_change.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||||
|
<p class="help-block">Token received after login</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h3>Update User Details</h3><hr/>
|
||||||
|
{% include "fragments/password_change_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$().ready(function(){
|
||||||
|
$('form button[type=submit]').click(function(){
|
||||||
|
var token = $('input[name=token]').val();
|
||||||
|
var form = $('form');
|
||||||
|
$.ajax({
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: $('form').serialize(),
|
||||||
|
type: "POST",
|
||||||
|
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||||
|
}).fail(function(data){error_response(data);})
|
||||||
|
.done(function(data){susccess_response(data);});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
8
backend/server/templates/password_reset.html
Normal file
8
backend/server/templates/password_reset.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Password reset</h3><hr/>
|
||||||
|
{% include "fragments/password_reset_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
26
backend/server/templates/password_reset_confirm.html
Normal file
26
backend/server/templates/password_reset_confirm.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Password reset confirmation</h3><hr/>
|
||||||
|
{% include "fragments/password_reset_confirm_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
var url_elements = window.location.pathname.split('/');
|
||||||
|
if (url_elements.length == 6){
|
||||||
|
var uid = url_elements[url_elements.length - 3];
|
||||||
|
if (uid !== undefined){
|
||||||
|
$('input[name=uid]').val(uid);
|
||||||
|
}
|
||||||
|
var token = url_elements[url_elements.length - 2];
|
||||||
|
if (token !== undefined){
|
||||||
|
$('input[name=token]').val(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
8
backend/server/templates/resend_email_verification.html
Normal file
8
backend/server/templates/resend_email_verification.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Resend E-mail verification</h3><hr/>
|
||||||
|
{% include "fragments/resend_email_verification_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
52
backend/server/templates/rest_framework/api.html
Normal file
52
backend/server/templates/rest_framework/api.html
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "rest_framework/base.html" %}
|
||||||
|
|
||||||
|
{% block style %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
#btn-link {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
display: block;
|
||||||
|
padding: 3px 20px;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
color: #A30000;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
#btn-link:hover {
|
||||||
|
background: #EEEEEE;
|
||||||
|
color: #C20000;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block userlinks %}
|
||||||
|
{% if user.is_authenticated or response.data.access_token %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||||
|
{% firstof user.username 'Registered' %}
|
||||||
|
<b class="caret"></b>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-right">
|
||||||
|
{% url 'rest_user_details' as user_url %}
|
||||||
|
<li><a href="{{ user_url }}">User</a></li>
|
||||||
|
<li>
|
||||||
|
{% url 'rest_logout' as logout_url %}
|
||||||
|
<form action="{{ logout_url }}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" id="btn-link">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
{% url 'rest_login' as login_url %}
|
||||||
|
<li><a href="{{ login_url }}">Login</a></li>
|
||||||
|
{% url 'rest_register' as register_url %}
|
||||||
|
<li><a href="{{ register_url }}">Register</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
8
backend/server/templates/signup.html
Normal file
8
backend/server/templates/signup.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<h3>Signup</h3><hr/>
|
||||||
|
{% include "fragments/signup_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
58
backend/server/templates/user_details.html
Normal file
58
backend/server/templates/user_details.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h3>Retrieve User Details</h3><hr/>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||||
|
<p class="help-block">Token received after login</p>
|
||||||
|
</div>
|
||||||
|
<button id="get-user-details" class="btn btn-primary">GET user details</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<h3>Update User Details</h3><hr/>
|
||||||
|
{% include "fragments/user_details_form.html" %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$().ready(function(){
|
||||||
|
$('#get-user-details').click(function(){
|
||||||
|
var token = $('input[name=token]').val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "{% url 'rest_user_details' %}",
|
||||||
|
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);},
|
||||||
|
type: "GET",
|
||||||
|
success: function(data) {
|
||||||
|
$('input[name=username]').val(data.username);
|
||||||
|
$('input[name=email]').val(data.email);
|
||||||
|
$('input[name=first_name]').val(data.first_name);
|
||||||
|
$('input[name=last_name]').val(data.last_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('form button[type=submit]').click(function(){
|
||||||
|
var token = $('input[name=token]').val();
|
||||||
|
var form = $('form');
|
||||||
|
$.ajax({
|
||||||
|
url: form.attr('action'),
|
||||||
|
data: $('form').serialize(),
|
||||||
|
type: "PUT",
|
||||||
|
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||||
|
}).fail(function(data){error_response(data);})
|
||||||
|
.done(function(data){susccess_response(data);});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
0
backend/server/users/__init__.py
Normal file
0
backend/server/users/__init__.py
Normal file
3
backend/server/users/admin.py
Normal file
3
backend/server/users/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
backend/server/users/apps.py
Normal file
6
backend/server/users/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'users'
|
45
backend/server/users/migrations/0001_initial.py
Normal file
45
backend/server/users/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 01:01
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CustomUser',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('profile_pic', models.ImageField(blank=True, null=True, upload_to='profile-pics/')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
backend/server/users/migrations/__init__.py
Normal file
0
backend/server/users/migrations/__init__.py
Normal file
8
backend/server/users/models.py
Normal file
8
backend/server/users/models.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class CustomUser(AbstractUser):
|
||||||
|
profile_pic = models.ImageField(null=True, blank=True, upload_to='profile-pics/')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
179
backend/server/users/serializers.py
Normal file
179
backend/server/users/serializers.py
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from adventures.models import Adventure
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
try:
|
||||||
|
from allauth.account import app_settings as allauth_account_settings
|
||||||
|
from allauth.account.adapter import get_adapter
|
||||||
|
from allauth.account.utils import setup_user_email
|
||||||
|
from allauth.socialaccount.models import EmailAddress
|
||||||
|
from allauth.utils import get_username_max_length
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
|
||||||
|
|
||||||
|
class ChangeEmailSerializer(serializers.Serializer):
|
||||||
|
new_email = serializers.EmailField(required=True)
|
||||||
|
|
||||||
|
def validate_new_email(self, value):
|
||||||
|
user = self.context['request'].user
|
||||||
|
if User.objects.filter(email=value).exclude(pk=user.pk).exists():
|
||||||
|
raise serializers.ValidationError("This email is already in use.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterSerializer(serializers.Serializer):
|
||||||
|
username = serializers.CharField(
|
||||||
|
max_length=get_username_max_length(),
|
||||||
|
min_length=allauth_account_settings.USERNAME_MIN_LENGTH,
|
||||||
|
required=allauth_account_settings.USERNAME_REQUIRED,
|
||||||
|
)
|
||||||
|
email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)
|
||||||
|
password1 = serializers.CharField(write_only=True)
|
||||||
|
password2 = serializers.CharField(write_only=True)
|
||||||
|
first_name = serializers.CharField(required=False)
|
||||||
|
last_name = serializers.CharField(required=False)
|
||||||
|
|
||||||
|
def validate_username(self, username):
|
||||||
|
username = get_adapter().clean_username(username)
|
||||||
|
return username
|
||||||
|
|
||||||
|
def validate_email(self, email):
|
||||||
|
email = get_adapter().clean_email(email)
|
||||||
|
if allauth_account_settings.UNIQUE_EMAIL:
|
||||||
|
if email and EmailAddress.objects.is_verified(email):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
_('A user is already registered with this e-mail address.'),
|
||||||
|
)
|
||||||
|
return email
|
||||||
|
|
||||||
|
def validate_password1(self, password):
|
||||||
|
return get_adapter().clean_password(password)
|
||||||
|
|
||||||
|
def validate(self, data):
|
||||||
|
if data['password1'] != data['password2']:
|
||||||
|
raise serializers.ValidationError(_("The two password fields didn't match."))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def custom_signup(self, request, user):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_cleaned_data(self):
|
||||||
|
return {
|
||||||
|
'username': self.validated_data.get('username', ''),
|
||||||
|
'password1': self.validated_data.get('password1', ''),
|
||||||
|
'email': self.validated_data.get('email', ''),
|
||||||
|
'first_name': self.validated_data.get('first_name', ''),
|
||||||
|
'last_name': self.validated_data.get('last_name', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self, request):
|
||||||
|
adapter = get_adapter()
|
||||||
|
user = adapter.new_user(request)
|
||||||
|
self.cleaned_data = self.get_cleaned_data()
|
||||||
|
user = adapter.save_user(request, user, self, commit=False)
|
||||||
|
if "password1" in self.cleaned_data:
|
||||||
|
try:
|
||||||
|
adapter.clean_password(self.cleaned_data['password1'], user=user)
|
||||||
|
except DjangoValidationError as exc:
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
detail=serializers.as_serializer_error(exc)
|
||||||
|
)
|
||||||
|
user.save()
|
||||||
|
self.custom_signup(request, user)
|
||||||
|
setup_user_email(request, user, [])
|
||||||
|
return user
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
UserModel = get_user_model()
|
||||||
|
from dj_rest_auth.serializers import UserDetailsSerializer
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from django.conf import settings
|
||||||
|
import os
|
||||||
|
|
||||||
|
class AdventureSerializer(serializers.ModelSerializer):
|
||||||
|
image = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Adventure
|
||||||
|
fields = ['id', 'user_id', 'type', 'name', 'location', 'activity_types', 'description',
|
||||||
|
'rating', 'link', 'image', 'date', 'trip_id', 'is_public', 'longitude', 'latitude']
|
||||||
|
|
||||||
|
def get_image(self, obj):
|
||||||
|
if obj.image:
|
||||||
|
public_url = os.environ.get('PUBLIC_URL', '')
|
||||||
|
return f'{public_url}/media/{obj.image.name}'
|
||||||
|
return None
|
||||||
|
|
||||||
|
class UserDetailsSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
User model w/o password
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_username(username):
|
||||||
|
if 'allauth.account' not in settings.INSTALLED_APPS:
|
||||||
|
# We don't need to call the all-auth
|
||||||
|
# username validator unless it's installed
|
||||||
|
return username
|
||||||
|
|
||||||
|
from allauth.account.adapter import get_adapter
|
||||||
|
username = get_adapter().clean_username(username)
|
||||||
|
return username
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
extra_fields = ['profile_pic']
|
||||||
|
profile_pic = serializers.ImageField(required=False)
|
||||||
|
# see https://github.com/iMerica/dj-rest-auth/issues/181
|
||||||
|
# UserModel.XYZ causing attribute error while importing other
|
||||||
|
# classes from `serializers.py`. So, we need to check whether the auth model has
|
||||||
|
# the attribute or not
|
||||||
|
if hasattr(UserModel, 'USERNAME_FIELD'):
|
||||||
|
extra_fields.append(UserModel.USERNAME_FIELD)
|
||||||
|
if hasattr(UserModel, 'EMAIL_FIELD'):
|
||||||
|
extra_fields.append(UserModel.EMAIL_FIELD)
|
||||||
|
if hasattr(UserModel, 'first_name'):
|
||||||
|
extra_fields.append('first_name')
|
||||||
|
if hasattr(UserModel, 'last_name'):
|
||||||
|
extra_fields.append('last_name')
|
||||||
|
if hasattr(UserModel, 'date_joined'):
|
||||||
|
extra_fields.append('date_joined')
|
||||||
|
if hasattr(UserModel, 'is_staff'):
|
||||||
|
extra_fields.append('is_staff')
|
||||||
|
|
||||||
|
class Meta(UserDetailsSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = UserDetailsSerializer.Meta.fields + ('profile_pic',)
|
||||||
|
|
||||||
|
model = UserModel
|
||||||
|
fields = ('pk', *extra_fields)
|
||||||
|
read_only_fields = ('email', 'date_joined', 'is_staff')
|
||||||
|
|
||||||
|
class CustomUserDetailsSerializer(UserDetailsSerializer):
|
||||||
|
|
||||||
|
|
||||||
|
class Meta(UserDetailsSerializer.Meta):
|
||||||
|
model = CustomUser
|
||||||
|
fields = UserDetailsSerializer.Meta.fields + ('profile_pic',)
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
representation = super().to_representation(instance)
|
||||||
|
if instance.profile_pic:
|
||||||
|
public_url = os.environ.get('PUBLIC_URL', 'http://127.0.0.1:8000').rstrip('/')
|
||||||
|
print(public_url)
|
||||||
|
# remove any ' from the url
|
||||||
|
public_url = public_url.replace("'", "")
|
||||||
|
representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}"
|
||||||
|
return representation
|
3
backend/server/users/tests.py
Normal file
3
backend/server/users/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
28
backend/server/users/views.py
Normal file
28
backend/server/users/views.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from .serializers import ChangeEmailSerializer
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from drf_yasg import openapi
|
||||||
|
|
||||||
|
class ChangeEmailView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
request_body=ChangeEmailSerializer,
|
||||||
|
responses={
|
||||||
|
200: openapi.Response('Email successfully changed'),
|
||||||
|
400: 'Bad Request'
|
||||||
|
},
|
||||||
|
operation_description="Change the email address for the authenticated user."
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = ChangeEmailSerializer(data=request.data, context={'request': request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
user = request.user
|
||||||
|
new_email = serializer.validated_data['new_email']
|
||||||
|
user.email = new_email
|
||||||
|
user.save()
|
||||||
|
return Response({"detail": "Email successfully changed."}, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
0
backend/server/worldtravel/__init__.py
Normal file
0
backend/server/worldtravel/__init__.py
Normal file
3
backend/server/worldtravel/admin.py
Normal file
3
backend/server/worldtravel/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
backend/server/worldtravel/apps.py
Normal file
6
backend/server/worldtravel/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WorldtravelConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'worldtravel'
|
0
backend/server/worldtravel/management/__init__.py
Normal file
0
backend/server/worldtravel/management/__init__.py
Normal file
|
@ -0,0 +1,517 @@
|
||||||
|
# myapp/management/commands/seed.py
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from worldtravel.models import Country, Region
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Imports the world travel data'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# if the country or regions tables are not empty, do not insert data
|
||||||
|
if Country.objects.exists() or Region.objects.exists():
|
||||||
|
self.stdout.write(self.style.NOTICE(
|
||||||
|
'Countries or regions already exist in the database!'))
|
||||||
|
return
|
||||||
|
|
||||||
|
countries = [
|
||||||
|
('United States', 'us', 'NA'),
|
||||||
|
('Canada', 'ca', 'NA'),
|
||||||
|
('Mexico', 'mx', 'NA'),
|
||||||
|
('Brazil', 'br', 'SA'),
|
||||||
|
('Argentina', 'ar', 'SA'),
|
||||||
|
('United Kingdom', 'gb', 'EU'),
|
||||||
|
('Germany', 'de', 'EU'),
|
||||||
|
('France', 'fr', 'EU'),
|
||||||
|
('Japan', 'jp', 'AS'),
|
||||||
|
('China', 'cn', 'AS'),
|
||||||
|
('India', 'in', 'AS'),
|
||||||
|
('Australia', 'au', 'OC'),
|
||||||
|
('New Zealand', 'nz', 'OC'),
|
||||||
|
('South Africa', 'za', 'AF'),
|
||||||
|
('Egypt', 'eg', 'AF'),
|
||||||
|
('Sweden', 'se', 'EU'),
|
||||||
|
('Ireland', 'ie', 'EU'),
|
||||||
|
('Spain', 'es', 'EU'),
|
||||||
|
('Switzerland', 'ch', 'EU'),
|
||||||
|
('Italy', 'it', 'EU'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, country_code, continent in countries:
|
||||||
|
country, created = Country.objects.get_or_create(
|
||||||
|
name=name,
|
||||||
|
country_code=country_code,
|
||||||
|
defaults={'continent': continent}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
print(f'Inserted {name} into worldtravel countries')
|
||||||
|
else:
|
||||||
|
print(f'{name} already exists in worldtravel countries')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
'Successfully inserted worldtravel countries!'
|
||||||
|
))
|
||||||
|
|
||||||
|
regions = [
|
||||||
|
('US-AL', 'Alabama', 'us'),
|
||||||
|
('US-AK', 'Alaska', 'us'),
|
||||||
|
('US-AZ', 'Arizona', 'us'),
|
||||||
|
('US-AR', 'Arkansas', 'us'),
|
||||||
|
('US-CA', 'California', 'us'),
|
||||||
|
('US-CO', 'Colorado', 'us'),
|
||||||
|
('US-CT', 'Connecticut', 'us'),
|
||||||
|
('US-DE', 'Delaware', 'us'),
|
||||||
|
('US-FL', 'Florida', 'us'),
|
||||||
|
('US-GA', 'Georgia', 'us'),
|
||||||
|
('US-HI', 'Hawaii', 'us'),
|
||||||
|
('US-ID', 'Idaho', 'us'),
|
||||||
|
('US-IL', 'Illinois', 'us'),
|
||||||
|
('US-IN', 'Indiana', 'us'),
|
||||||
|
('US-IA', 'Iowa', 'us'),
|
||||||
|
('US-KS', 'Kansas', 'us'),
|
||||||
|
('US-KY', 'Kentucky', 'us'),
|
||||||
|
('US-LA', 'Louisiana', 'us'),
|
||||||
|
('US-ME', 'Maine', 'us'),
|
||||||
|
('US-MD', 'Maryland', 'us'),
|
||||||
|
('US-MA', 'Massachusetts', 'us'),
|
||||||
|
('US-MI', 'Michigan', 'us'),
|
||||||
|
('US-MN', 'Minnesota', 'us'),
|
||||||
|
('US-MS', 'Mississippi', 'us'),
|
||||||
|
('US-MO', 'Missouri', 'us'),
|
||||||
|
('US-MT', 'Montana', 'us'),
|
||||||
|
('US-NE', 'Nebraska', 'us'),
|
||||||
|
('US-NV', 'Nevada', 'us'),
|
||||||
|
('US-NH', 'New Hampshire', 'us'),
|
||||||
|
('US-NJ', 'New Jersey', 'us'),
|
||||||
|
('US-NM', 'New Mexico', 'us'),
|
||||||
|
('US-NY', 'New York', 'us'),
|
||||||
|
('US-NC', 'North Carolina', 'us'),
|
||||||
|
('US-ND', 'North Dakota', 'us'),
|
||||||
|
('US-OH', 'Ohio', 'us'),
|
||||||
|
('US-OK', 'Oklahoma', 'us'),
|
||||||
|
('US-OR', 'Oregon', 'us'),
|
||||||
|
('US-PA', 'Pennsylvania', 'us'),
|
||||||
|
('US-RI', 'Rhode Island', 'us'),
|
||||||
|
('US-SC', 'South Carolina', 'us'),
|
||||||
|
('US-SD', 'South Dakota', 'us'),
|
||||||
|
('US-TN', 'Tennessee', 'us'),
|
||||||
|
('US-TX', 'Texas', 'us'),
|
||||||
|
('US-UT', 'Utah', 'us'),
|
||||||
|
('US-VT', 'Vermont', 'us'),
|
||||||
|
('US-VA', 'Virginia', 'us'),
|
||||||
|
('US-WA', 'Washington', 'us'),
|
||||||
|
('US-WV', 'West Virginia', 'us'),
|
||||||
|
('US-WI', 'Wisconsin', 'us'),
|
||||||
|
('US-WY', 'Wyoming', 'us'),
|
||||||
|
('CA-AB', 'Alberta', 'ca'),
|
||||||
|
('CA-BC', 'British Columbia', 'ca'),
|
||||||
|
('CA-MB', 'Manitoba', 'ca'),
|
||||||
|
('CA-NB', 'New Brunswick', 'ca'),
|
||||||
|
('CA-NL', 'Newfoundland and Labrador', 'ca'),
|
||||||
|
('CA-NS', 'Nova Scotia', 'ca'),
|
||||||
|
('CA-ON', 'Ontario', 'ca'),
|
||||||
|
('CA-PE', 'Prince Edward Island', 'ca'),
|
||||||
|
('CA-QC', 'Quebec', 'ca'),
|
||||||
|
('CA-SK', 'Saskatchewan', 'ca'),
|
||||||
|
('CA-NT', 'Northwest Territories', 'ca'),
|
||||||
|
('CA-NU', 'Nunavut', 'ca'),
|
||||||
|
('CA-YT', 'Yukon', 'ca'),
|
||||||
|
('DE-BW', 'Baden-Württemberg', 'de'),
|
||||||
|
('DE-BY', 'Bavaria', 'de'),
|
||||||
|
('DE-BE', 'Berlin', 'de'),
|
||||||
|
('DE-BB', 'Brandenburg', 'de'),
|
||||||
|
('DE-HB', 'Bremen', 'de'),
|
||||||
|
('DE-HH', 'Hamburg', 'de'),
|
||||||
|
('DE-HE', 'Hesse', 'de'),
|
||||||
|
('DE-NI', 'Lower Saxony', 'de'),
|
||||||
|
('DE-MV', 'Mecklenburg-Vorpommern', 'de'),
|
||||||
|
('DE-NW', 'North Rhine-Westphalia', 'de'),
|
||||||
|
('DE-RP', 'Rhineland-Palatinate', 'de'),
|
||||||
|
('DE-SL', 'Saarland', 'de'),
|
||||||
|
('DE-SN', 'Saxony', 'de'),
|
||||||
|
('DE-ST', 'Saxony-Anhalt', 'de'),
|
||||||
|
('DE-SH', 'Schleswig-Holstein', 'de'),
|
||||||
|
('DE-TH', 'Thuringia', 'de'),
|
||||||
|
('FR-ARA', 'Auvergne-Rhône-Alpes', 'fr'),
|
||||||
|
('FR-BFC', 'Bourgogne-Franche-Comté', 'fr'),
|
||||||
|
('FR-BRE', 'Brittany', 'fr'),
|
||||||
|
('FR-CVL', 'Centre-Val de Loire', 'fr'),
|
||||||
|
('FR-GES', 'Grand Est', 'fr'),
|
||||||
|
('FR-HDF', 'Hauts-de-France', 'fr'),
|
||||||
|
('FR-IDF', 'Île-de-France', 'fr'),
|
||||||
|
('FR-NOR', 'Normandy', 'fr'),
|
||||||
|
('FR-NAQ', 'Nouvelle-Aquitaine', 'fr'),
|
||||||
|
('FR-OCC', 'Occitanie', 'fr'),
|
||||||
|
('FR-PDL', 'Pays de la Loire', 'fr'),
|
||||||
|
('FR-PAC', 'Provence-Alpes-Côte d''Azur', 'fr'),
|
||||||
|
('FR-COR', 'Corsica', 'fr'),
|
||||||
|
('FR-MQ', 'Martinique', 'fr'),
|
||||||
|
('FR-GF', 'French Guiana', 'fr'),
|
||||||
|
('FR-RÉ', 'Réunion', 'fr'),
|
||||||
|
('FR-YT', 'Mayotte', 'fr'),
|
||||||
|
('FR-GP', 'Guadeloupe', 'fr'),
|
||||||
|
('GB-ENG', 'England', 'gb'),
|
||||||
|
('GB-NIR', 'Northern Ireland', 'gb'),
|
||||||
|
('GB-SCT', 'Scotland', 'gb'),
|
||||||
|
('GB-WLS', 'Wales', 'gb'),
|
||||||
|
('AR-C', 'Ciudad Autónoma de Buenos Aires', 'ar'),
|
||||||
|
('AR-B', 'Buenos Aires', 'ar'),
|
||||||
|
('AR-K', 'Catamarca', 'ar'),
|
||||||
|
('AR-H', 'Chaco', 'ar'),
|
||||||
|
('AR-U', 'Chubut', 'ar'),
|
||||||
|
('AR-W', 'Córdoba', 'ar'),
|
||||||
|
('AR-X', 'Corrientes', 'ar'),
|
||||||
|
('AR-E', 'Entre Ríos', 'ar'),
|
||||||
|
('AR-P', 'Formosa', 'ar'),
|
||||||
|
('AR-Y', 'Jujuy', 'ar'),
|
||||||
|
('AR-L', 'La Pampa', 'ar'),
|
||||||
|
('AR-F', 'La Rioja', 'ar'),
|
||||||
|
('AR-M', 'Mendoza', 'ar'),
|
||||||
|
('AR-N', 'Misiones', 'ar'),
|
||||||
|
('AR-Q', 'Neuquén', 'ar'),
|
||||||
|
('AR-R', 'Río Negro', 'ar'),
|
||||||
|
('AR-A', 'Salta', 'ar'),
|
||||||
|
('AR-J', 'San Juan', 'ar'),
|
||||||
|
('AR-D', 'San Luis', 'ar'),
|
||||||
|
('AR-Z', 'Santa Cruz', 'ar'),
|
||||||
|
('AR-S', 'Santa Fe', 'ar'),
|
||||||
|
('AR-G', 'Santiago del Estero', 'ar'),
|
||||||
|
('AR-V', 'Tierra del Fuego', 'ar'),
|
||||||
|
('AR-T', 'Tucumán', 'ar'),
|
||||||
|
('MX-AGU', 'Aguascalientes', 'mx'),
|
||||||
|
('MX-BCN', 'Baja California', 'mx'),
|
||||||
|
('MX-BCS', 'Baja California Sur', 'mx'),
|
||||||
|
('MX-CAM', 'Campeche', 'mx'),
|
||||||
|
('MX-CHP', 'Chiapas', 'mx'),
|
||||||
|
('MX-CHH', 'Chihuahua', 'mx'),
|
||||||
|
('MX-COA', 'Coahuila', 'mx'),
|
||||||
|
('MX-COL', 'Colima', 'mx'),
|
||||||
|
('MX-DUR', 'Durango', 'mx'),
|
||||||
|
('MX-GUA', 'Guanajuato', 'mx'),
|
||||||
|
('MX-GRO', 'Guerrero', 'mx'),
|
||||||
|
('MX-HID', 'Hidalgo', 'mx'),
|
||||||
|
('MX-JAL', 'Jalisco', 'mx'),
|
||||||
|
('MX-MEX', 'State of Mexico', 'mx'),
|
||||||
|
('MX-MIC', 'Michoacán', 'mx'),
|
||||||
|
('MX-MOR', 'Morelos', 'mx'),
|
||||||
|
('MX-NAY', 'Nayarit', 'mx'),
|
||||||
|
('MX-NLE', 'Nuevo León', 'mx'),
|
||||||
|
('MX-OAX', 'Oaxaca', 'mx'),
|
||||||
|
('MX-PUE', 'Puebla', 'mx'),
|
||||||
|
('MX-QUE', 'Querétaro', 'mx'),
|
||||||
|
('MX-ROO', 'Quintana Roo', 'mx'),
|
||||||
|
('MX-SLP', 'San Luis Potosí', 'mx'),
|
||||||
|
('MX-SIN', 'Sinaloa', 'mx'),
|
||||||
|
('MX-SON', 'Sonora', 'mx'),
|
||||||
|
('MX-TAB', 'Tabasco', 'mx'),
|
||||||
|
('MX-TAM', 'Tamaulipas', 'mx'),
|
||||||
|
('MX-TLA', 'Tlaxcala', 'mx'),
|
||||||
|
('MX-VER', 'Veracruz', 'mx'),
|
||||||
|
('MX-YUC', 'Yucatán', 'mx'),
|
||||||
|
('MX-ZAC', 'Zacatecas', 'mx'),
|
||||||
|
('JP-01', 'Hokkaido', 'jp'),
|
||||||
|
('JP-02', 'Aomori', 'jp'),
|
||||||
|
('JP-03', 'Iwate', 'jp'),
|
||||||
|
('JP-04', 'Miyagi', 'jp'),
|
||||||
|
('JP-05', 'Akita', 'jp'),
|
||||||
|
('JP-06', 'Yamagata', 'jp'),
|
||||||
|
('JP-07', 'Fukushima', 'jp'),
|
||||||
|
('JP-08', 'Ibaraki', 'jp'),
|
||||||
|
('JP-09', 'Tochigi', 'jp'),
|
||||||
|
('JP-10', 'Gunma', 'jp'),
|
||||||
|
('JP-11', 'Saitama', 'jp'),
|
||||||
|
('JP-12', 'Chiba', 'jp'),
|
||||||
|
('JP-13', 'Tokyo', 'jp'),
|
||||||
|
('JP-14', 'Kanagawa', 'jp'),
|
||||||
|
('JP-15', 'Niigata', 'jp'),
|
||||||
|
('JP-16', 'Toyama', 'jp'),
|
||||||
|
('JP-17', 'Ishikawa', 'jp'),
|
||||||
|
('JP-18', 'Fukui', 'jp'),
|
||||||
|
('JP-19', 'Yamanashi', 'jp'),
|
||||||
|
('JP-20', 'Nagano', 'jp'),
|
||||||
|
('JP-21', 'Gifu', 'jp'),
|
||||||
|
('JP-22', 'Shizuoka', 'jp'),
|
||||||
|
('JP-23', 'Aichi', 'jp'),
|
||||||
|
('JP-24', 'Mie', 'jp'),
|
||||||
|
('JP-25', 'Shiga', 'jp'),
|
||||||
|
('JP-26', 'Kyoto', 'jp'),
|
||||||
|
('JP-27', 'Osaka', 'jp'),
|
||||||
|
('JP-28', 'Hyogo', 'jp'),
|
||||||
|
('JP-29', 'Nara', 'jp'),
|
||||||
|
('JP-30', 'Wakayama', 'jp'),
|
||||||
|
('JP-31', 'Tottori', 'jp'),
|
||||||
|
('JP-32', 'Shimane', 'jp'),
|
||||||
|
('JP-33', 'Okayama', 'jp'),
|
||||||
|
('JP-34', 'Hiroshima', 'jp'),
|
||||||
|
('JP-35', 'Yamaguchi', 'jp'),
|
||||||
|
('JP-36', 'Tokushima', 'jp'),
|
||||||
|
('JP-37', 'Kagawa', 'jp'),
|
||||||
|
('JP-38', 'Ehime', 'jp'),
|
||||||
|
('JP-39', 'Kochi', 'jp'),
|
||||||
|
('JP-40', 'Fukuoka', 'jp'),
|
||||||
|
('JP-41', 'Saga', 'jp'),
|
||||||
|
('JP-42', 'Nagasaki', 'jp'),
|
||||||
|
('JP-43', 'Kumamoto', 'jp'),
|
||||||
|
('JP-44', 'Oita', 'jp'),
|
||||||
|
('JP-45', 'Miyazaki', 'jp'),
|
||||||
|
('JP-46', 'Kagoshima', 'jp'),
|
||||||
|
('JP-47', 'Okinawa', 'jp'),
|
||||||
|
('CN-BJ', 'Beijing', 'cn'),
|
||||||
|
('CN-TJ', 'Tianjin', 'cn'),
|
||||||
|
('CN-HE', 'Hebei', 'cn'),
|
||||||
|
('CN-SX', 'Shanxi', 'cn'),
|
||||||
|
('CN-NM', 'Inner Mongolia', 'cn'),
|
||||||
|
('CN-LN', 'Liaoning', 'cn'),
|
||||||
|
('CN-JL', 'Jilin', 'cn'),
|
||||||
|
('CN-HL', 'Heilongjiang', 'cn'),
|
||||||
|
('CN-SH', 'Shanghai', 'cn'),
|
||||||
|
('CN-JS', 'Jiangsu', 'cn'),
|
||||||
|
('CN-ZJ', 'Zhejiang', 'cn'),
|
||||||
|
('CN-AH', 'Anhui', 'cn'),
|
||||||
|
('CN-FJ', 'Fujian', 'cn'),
|
||||||
|
('CN-JX', 'Jiangxi', 'cn'),
|
||||||
|
('CN-SD', 'Shandong', 'cn'),
|
||||||
|
('CN-HA', 'Henan', 'cn'),
|
||||||
|
('CN-HB', 'Hubei', 'cn'),
|
||||||
|
('CN-HN', 'Hunan', 'cn'),
|
||||||
|
('CN-GD', 'Guangdong', 'cn'),
|
||||||
|
('CN-GX', 'Guangxi', 'cn'),
|
||||||
|
('CN-HI', 'Hainan', 'cn'),
|
||||||
|
('CN-CQ', 'Chongqing', 'cn'),
|
||||||
|
('CN-SC', 'Sichuan', 'cn'),
|
||||||
|
('CN-GZ', 'Guizhou', 'cn'),
|
||||||
|
('CN-YN', 'Yunnan', 'cn'),
|
||||||
|
('CN-XZ', 'Tibet', 'cn'),
|
||||||
|
('CN-SA', 'Shaanxi', 'cn'),
|
||||||
|
('CN-GS', 'Gansu', 'cn'),
|
||||||
|
('CN-QH', 'Qinghai', 'cn'),
|
||||||
|
('CN-NX', 'Ningxia', 'cn'),
|
||||||
|
('CN-XJ', 'Xinjiang', 'cn'),
|
||||||
|
('IN-AN', 'Andaman and Nicobar Islands', 'in'),
|
||||||
|
('IN-AP', 'Andhra Pradesh', 'in'),
|
||||||
|
('IN-AR', 'Arunachal Pradesh', 'in'),
|
||||||
|
('IN-AS', 'Assam', 'in'),
|
||||||
|
('IN-BR', 'Bihar', 'in'),
|
||||||
|
('IN-CH', 'Chandigarh', 'in'),
|
||||||
|
('IN-CT', 'Chhattisgarh', 'in'),
|
||||||
|
('IN-DN', 'Dadra and Nagar Haveli and Daman and Diu', 'in'),
|
||||||
|
('IN-DD', 'Daman and Diu', 'in'),
|
||||||
|
('IN-DL', 'Delhi', 'in'),
|
||||||
|
('IN-GA', 'Goa', 'in'),
|
||||||
|
('IN-GJ', 'Gujarat', 'in'),
|
||||||
|
('IN-HR', 'Haryana', 'in'),
|
||||||
|
('IN-HP', 'Himachal Pradesh', 'in'),
|
||||||
|
('IN-JH', 'Jharkhand', 'in'),
|
||||||
|
('IN-KA', 'Karnataka', 'in'),
|
||||||
|
('IN-KL', 'Kerala', 'in'),
|
||||||
|
('IN-LD', 'Lakshadweep', 'in'),
|
||||||
|
('IN-MP', 'Madhya Pradesh', 'in'),
|
||||||
|
('IN-MH', 'Maharashtra', 'in'),
|
||||||
|
('IN-MN', 'Manipur', 'in'),
|
||||||
|
('IN-ML', 'Meghalaya', 'in'),
|
||||||
|
('IN-MZ', 'Mizoram', 'in'),
|
||||||
|
('IN-NL', 'Nagaland', 'in'),
|
||||||
|
('IN-OR', 'Odisha', 'in'),
|
||||||
|
('IN-PY', 'Puducherry', 'in'),
|
||||||
|
('IN-PB', 'Punjab', 'in'),
|
||||||
|
('IN-RJ', 'Rajasthan', 'in'),
|
||||||
|
('IN-SK', 'Sikkim', 'in'),
|
||||||
|
('IN-TN', 'Tamil Nadu', 'in'),
|
||||||
|
('IN-TG', 'Telangana', 'in'),
|
||||||
|
('IN-TR', 'Tripura', 'in'),
|
||||||
|
('IN-UP', 'Uttar Pradesh', 'in'),
|
||||||
|
('IN-UT', 'Uttarakhand', 'in'),
|
||||||
|
('IN-WB', 'West Bengal', 'in'),
|
||||||
|
('AU-NSW', 'New South Wales', 'au'),
|
||||||
|
('AU-VIC', 'Victoria', 'au'),
|
||||||
|
('AU-QLD', 'Queensland', 'au'),
|
||||||
|
('AU-SA', 'South Australia', 'au'),
|
||||||
|
('AU-WA', 'Western Australia', 'au'),
|
||||||
|
('AU-TAS', 'Tasmania', 'au'),
|
||||||
|
('AU-NT', 'Northern Territory', 'au'),
|
||||||
|
('AU-ACT', 'Australian Capital Territory', 'au'),
|
||||||
|
('NZ-N', 'Northland', 'nz'),
|
||||||
|
('NZ-AUK', 'Auckland', 'nz'),
|
||||||
|
('NZ-WKO', 'Waikato', 'nz'),
|
||||||
|
('NZ-BOP', 'Bay of Plenty', 'nz'),
|
||||||
|
('NZ-GIS', 'Gisborne', 'nz'),
|
||||||
|
('NZ-HKB', 'Hawke''s Bay', 'nz'),
|
||||||
|
('NZ-TKI', 'Taranaki', 'nz'),
|
||||||
|
('NZ-MWT', 'Manawatū-Whanganui', 'nz'),
|
||||||
|
('NZ-WGN', 'Wellington', 'nz'),
|
||||||
|
('NZ-TAS', 'Tasman', 'nz'),
|
||||||
|
('NZ-NEL', 'Nelson', 'nz'),
|
||||||
|
('NZ-MBH', 'Marlborough', 'nz'),
|
||||||
|
('NZ-WTC', 'West Coast', 'nz'),
|
||||||
|
('NZ-CAN', 'Canterbury', 'nz'),
|
||||||
|
('NZ-OTA', 'Otago', 'nz'),
|
||||||
|
('NZ-STL', 'Southland', 'nz'),
|
||||||
|
('ZA-EC', 'Eastern Cape', 'za'),
|
||||||
|
('ZA-FS', 'Free State', 'za'),
|
||||||
|
('ZA-GP', 'Gauteng', 'za'),
|
||||||
|
('ZA-KZN', 'KwaZulu-Natal', 'za'),
|
||||||
|
('ZA-LP', 'Limpopo', 'za'),
|
||||||
|
('ZA-MP', 'Mpumalanga', 'za'),
|
||||||
|
('ZA-NW', 'North West', 'za'),
|
||||||
|
('ZA-NC', 'Northern Cape', 'za'),
|
||||||
|
('ZA-WC', 'Western Cape', 'za'),
|
||||||
|
('EG-ALX', 'Alexandria', 'eg'),
|
||||||
|
('EG-ASN', 'Aswan', 'eg'),
|
||||||
|
('EG-ASY', 'Asyut', 'eg'),
|
||||||
|
('EG-BHR', 'Beheira', 'eg'),
|
||||||
|
('EG-BNS', 'Beni Suef', 'eg'),
|
||||||
|
('EG-C', 'Cairo', 'eg'),
|
||||||
|
('EG-DK', 'Dakahlia', 'eg'),
|
||||||
|
('EG-DAM', 'Damietta', 'eg'),
|
||||||
|
('EG-FYM', 'Faiyum', 'eg'),
|
||||||
|
('EG-GH', 'Gharbia', 'eg'),
|
||||||
|
('EG-GZ', 'Giza', 'eg'),
|
||||||
|
('EG-IS', 'Ismailia', 'eg'),
|
||||||
|
('EG-KB', 'Kafr El Sheikh', 'eg'),
|
||||||
|
('EG-LX', 'Luxor', 'eg'),
|
||||||
|
('EG-MN', 'Minya', 'eg'),
|
||||||
|
('EG-MT', 'Matrouh', 'eg'),
|
||||||
|
('EG-QH', 'Qalyubia', 'eg'),
|
||||||
|
('EG-KFS', 'Qena', 'eg'),
|
||||||
|
('EG-SHG', 'Sohag', 'eg'),
|
||||||
|
('EG-SHR', 'Sharqia', 'eg'),
|
||||||
|
('EG-SIN', 'South Sinai', 'eg'),
|
||||||
|
('EG-SW', 'Suez', 'eg'),
|
||||||
|
('EG-WAD', 'New Valley', 'eg'),
|
||||||
|
('EG-ASD', 'North Sinai', 'eg'),
|
||||||
|
('EG-PTS', 'Port Said', 'eg'),
|
||||||
|
('EG-SKB', 'Suez', 'eg'),
|
||||||
|
('EG-ESI', 'Ismailia', 'eg'),
|
||||||
|
('BR-AC', 'Acre', 'br'),
|
||||||
|
('BR-AL', 'Alagoas', 'br'),
|
||||||
|
('BR-AP', 'Amapá', 'br'),
|
||||||
|
('BR-AM', 'Amazonas', 'br'),
|
||||||
|
('BR-BA', 'Bahia', 'br'),
|
||||||
|
('BR-CE', 'Ceará', 'br'),
|
||||||
|
('BR-DF', 'Federal District', 'br'),
|
||||||
|
('BR-ES', 'Espírito Santo', 'br'),
|
||||||
|
('BR-GO', 'Goiás', 'br'),
|
||||||
|
('BR-MA', 'Maranhão', 'br'),
|
||||||
|
('BR-MT', 'Mato Grosso', 'br'),
|
||||||
|
('BR-MS', 'Mato Grosso do Sul', 'br'),
|
||||||
|
('BR-MG', 'Minas Gerais', 'br'),
|
||||||
|
('BR-PA', 'Pará', 'br'),
|
||||||
|
('BR-PB', 'Paraíba', 'br'),
|
||||||
|
('BR-PR', 'Paraná', 'br'),
|
||||||
|
('BR-PE', 'Pernambuco', 'br'),
|
||||||
|
('BR-PI', 'Piauí', 'br'),
|
||||||
|
('BR-RJ', 'Rio de Janeiro', 'br'),
|
||||||
|
('BR-RN', 'Rio Grande do Norte', 'br'),
|
||||||
|
('BR-RS', 'Rio Grande do Sul', 'br'),
|
||||||
|
('BR-RO', 'Rondônia', 'br'),
|
||||||
|
('BR-RR', 'Roraima', 'br'),
|
||||||
|
('BR-SC', 'Santa Catarina', 'br'),
|
||||||
|
('BR-SP', 'São Paulo', 'br'),
|
||||||
|
('BR-SE', 'Sergipe', 'br'),
|
||||||
|
('BR-TO', 'Tocantins', 'br'),
|
||||||
|
('SE-AB', 'Stockholm', 'se'),
|
||||||
|
('SE-AC', 'Västerbotten', 'se'),
|
||||||
|
('SE-BD', 'Norrbotten', 'se'),
|
||||||
|
('SE-C', 'Uppsala', 'se'),
|
||||||
|
('SE-D', 'Södermanland', 'se'),
|
||||||
|
('SE-E', 'Östergötland', 'se'),
|
||||||
|
('SE-F', 'Jönköping', 'se'),
|
||||||
|
('SE-G', 'Kronoberg', 'se'),
|
||||||
|
('SE-H', 'Kalmar', 'se'),
|
||||||
|
('SE-I', 'Gotland', 'se'),
|
||||||
|
('SE-K', 'Blekinge', 'se'),
|
||||||
|
('SE-M', 'Skåne', 'se'),
|
||||||
|
('SE-N', 'Halland', 'se'),
|
||||||
|
('SE-O', 'Västra Götaland', 'se'),
|
||||||
|
('SE-S', 'Värmland', 'se'),
|
||||||
|
('SE-T', 'Örebro', 'se'),
|
||||||
|
('SE-U', 'Västmanland', 'se'),
|
||||||
|
('SE-W', 'Dalarna', 'se'),
|
||||||
|
('SE-X', 'Gävleborg', 'se'),
|
||||||
|
('SE-Y', 'Västernorrland', 'se'),
|
||||||
|
('SE-Z', 'Jämtland', 'se'),
|
||||||
|
('IE-C', 'Connacht', 'ie'),
|
||||||
|
('IE-L', 'Leinster', 'ie'),
|
||||||
|
('IE-M', 'Munster', 'ie'),
|
||||||
|
('IE-U', 'Ulster', 'ie'),
|
||||||
|
('ES-AN', 'Andalucía', 'es'),
|
||||||
|
('ES-AR', 'Aragón', 'es'),
|
||||||
|
('ES-AS', 'Asturias', 'es'),
|
||||||
|
('ES-CB', 'Cantabria', 'es'),
|
||||||
|
('ES-CL', 'Castilla y León', 'es'),
|
||||||
|
('ES-CM', 'Castilla-La Mancha', 'es'),
|
||||||
|
('ES-CN', 'Canarias', 'es'),
|
||||||
|
('ES-CT', 'Cataluña', 'es'),
|
||||||
|
('ES-EX', 'Extremadura', 'es'),
|
||||||
|
('ES-GA', 'Galicia', 'es'),
|
||||||
|
('ES-IB', 'Islas Baleares', 'es'),
|
||||||
|
('ES-MD', 'Madrid', 'es'),
|
||||||
|
('ES-MC', 'Murcia', 'es'),
|
||||||
|
('ES-NC', 'Navarra', 'es'),
|
||||||
|
('ES-PV', 'País Vasco', 'es'),
|
||||||
|
('ES-RI', 'La Rioja', 'es'),
|
||||||
|
('ES-VC', 'Comunidad Valenciana', 'es'),
|
||||||
|
('CH-AG', 'Aargau', 'ch'),
|
||||||
|
('CH-AR', 'Appenzell Ausserrhoden', 'ch'),
|
||||||
|
('CH-AI', 'Appenzell Innerrhoden', 'ch'),
|
||||||
|
('CH-BL', 'Basel-Landschaft', 'ch'),
|
||||||
|
('CH-BS', 'Basel-Stadt', 'ch'),
|
||||||
|
('CH-BE', 'Bern', 'ch'),
|
||||||
|
('CH-FR', 'Fribourg', 'ch'),
|
||||||
|
('CH-GE', 'Genève', 'ch'),
|
||||||
|
('CH-GL', 'Glarus', 'ch'),
|
||||||
|
('CH-GR', 'Graubünden', 'ch'),
|
||||||
|
('CH-JU', 'Jura', 'ch'),
|
||||||
|
('CH-LU', 'Luzern', 'ch'),
|
||||||
|
('CH-NE', 'Neuchâtel', 'ch'),
|
||||||
|
('CH-NW', 'Nidwalden', 'ch'),
|
||||||
|
('CH-OW', 'Obwalden', 'ch'),
|
||||||
|
('CH-SH', 'Schaffhausen', 'ch'),
|
||||||
|
('CH-SZ', 'Schwyz', 'ch'),
|
||||||
|
('CH-SO', 'Solothurn', 'ch'),
|
||||||
|
('CH-SG', 'St. Gallen', 'ch'),
|
||||||
|
('CH-TG', 'Thurgau', 'ch'),
|
||||||
|
('CH-TI', 'Ticino', 'ch'),
|
||||||
|
('CH-UR', 'Uri', 'ch'),
|
||||||
|
('CH-VS', 'Valais', 'ch'),
|
||||||
|
('CH-VD', 'Vaud', 'ch'),
|
||||||
|
('CH-ZG', 'Zug', 'ch'),
|
||||||
|
('CH-ZH', 'Zürich', 'ch'),
|
||||||
|
('IT-65', 'Abruzzo', 'it'),
|
||||||
|
('IT-77', 'Basilicata', 'it'),
|
||||||
|
('IT-78', 'Calabria', 'it'),
|
||||||
|
('IT-72', 'Campania', 'it'),
|
||||||
|
('IT-45', 'Emilia-Romagna', 'it'),
|
||||||
|
('IT-36', 'Friuli Venezia Giulia', 'it'),
|
||||||
|
('IT-62', 'Lazio', 'it'),
|
||||||
|
('IT-42', 'Liguria', 'it'),
|
||||||
|
('IT-25', 'Lombardia', 'it'),
|
||||||
|
('IT-57', 'Marche', 'it'),
|
||||||
|
('IT-67', 'Molise', 'it'),
|
||||||
|
('IT-21', 'Piemonte', 'it'),
|
||||||
|
('IT-75', 'Puglia', 'it'),
|
||||||
|
('IT-88', 'Sardegna', 'it'),
|
||||||
|
('IT-82', 'Sicilia', 'it'),
|
||||||
|
('IT-52', 'Toscana', 'it'),
|
||||||
|
('IT-32', 'Trentino-Alto Adige', 'it'),
|
||||||
|
('IT-55', 'Umbria', 'it'),
|
||||||
|
('IT-23', 'Valle d''Aosta', 'it'),
|
||||||
|
('IT-34', 'Veneto', 'it'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for code, name, country_code in regions:
|
||||||
|
country = Country.objects.get(country_code=country_code)
|
||||||
|
region, created = Region.objects.get_or_create(
|
||||||
|
id=code,
|
||||||
|
defaults={'name': name, 'country': country}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
print(f'Inserted region: {name} ({code})')
|
||||||
|
else:
|
||||||
|
print(f'Region already exists: {name} ({code})')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
'Successfully inserted worldtravel regions!'
|
||||||
|
))
|
46
backend/server/worldtravel/migrations/0001_initial.py
Normal file
46
backend/server/worldtravel/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-06-28 01:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Country',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('country_code', models.CharField(max_length=2)),
|
||||||
|
('continent', models.CharField(choices=[('AF', 'Africa'), ('AN', 'Antarctica'), ('AS', 'Asia'), ('EU', 'Europe'), ('NA', 'North America'), ('OC', 'Oceania'), ('SA', 'South America')], default='AF', max_length=2)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Country',
|
||||||
|
'verbose_name_plural': 'Countries',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Region',
|
||||||
|
fields=[
|
||||||
|
('id', models.CharField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=100)),
|
||||||
|
('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.country')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VisitedRegion',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.region')),
|
||||||
|
('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
backend/server/worldtravel/migrations/__init__.py
Normal file
0
backend/server/worldtravel/migrations/__init__.py
Normal file
59
backend/server/worldtravel/models.py
Normal file
59
backend/server/worldtravel/models.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
default_user_id = 1 # Replace with an actual user ID
|
||||||
|
|
||||||
|
class Country(models.Model):
|
||||||
|
AFRICA = 'AF'
|
||||||
|
ANTARCTICA = 'AN'
|
||||||
|
ASIA = 'AS'
|
||||||
|
EUROPE = 'EU'
|
||||||
|
NORTH_AMERICA = 'NA'
|
||||||
|
OCEANIA = 'OC'
|
||||||
|
SOUTH_AMERICA = 'SA'
|
||||||
|
|
||||||
|
CONTINENT_CHOICES = [
|
||||||
|
(AFRICA, 'Africa'),
|
||||||
|
(ANTARCTICA, 'Antarctica'),
|
||||||
|
(ASIA, 'Asia'),
|
||||||
|
(EUROPE, 'Europe'),
|
||||||
|
(NORTH_AMERICA, 'North America'),
|
||||||
|
(OCEANIA, 'Oceania'),
|
||||||
|
(SOUTH_AMERICA, 'South America'),
|
||||||
|
]
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
country_code = models.CharField(max_length=2)
|
||||||
|
continent = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=CONTINENT_CHOICES,
|
||||||
|
default=AFRICA
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Country"
|
||||||
|
verbose_name_plural = "Countries"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Region(models.Model):
|
||||||
|
id = models.CharField(primary_key=True)
|
||||||
|
name = models.CharField(max_length=100)
|
||||||
|
country = models.ForeignKey(Country, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class VisitedRegion(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
user_id = models.ForeignKey(
|
||||||
|
User, on_delete=models.CASCADE, default=default_user_id)
|
||||||
|
region = models.ForeignKey(Region, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.region.name} ({self.region.country.country_code}) visited by: {self.user_id.username}'
|
18
backend/server/worldtravel/serializers.py
Normal file
18
backend/server/worldtravel/serializers.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from .models import Country, Region, VisitedRegion
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class CountrySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Country
|
||||||
|
fields = '__all__' # Serialize all fields of the Adventure model
|
||||||
|
|
||||||
|
class RegionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Region
|
||||||
|
fields = '__all__' # Serialize all fields of the Adventure model
|
||||||
|
|
||||||
|
class VisitedRegionSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = VisitedRegion
|
||||||
|
fields = '__all__' # Serialize all fields of the Adventure model
|
3
backend/server/worldtravel/tests.py
Normal file
3
backend/server/worldtravel/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
16
backend/server/worldtravel/urls.py
Normal file
16
backend/server/worldtravel/urls.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# travel/urls.py
|
||||||
|
|
||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework.routers import DefaultRouter
|
||||||
|
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
router.register(r'countries', CountryViewSet)
|
||||||
|
router.register(r'regions', RegionViewSet)
|
||||||
|
router.register(r'visitedregion', VisitedRegionViewSet)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls)),
|
||||||
|
path('<str:country_code>/regions/', regions_by_country, name='regions-by-country'),
|
||||||
|
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
|
||||||
|
]
|
43
backend/server/worldtravel/views.py
Normal file
43
backend/server/worldtravel/views.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from .models import Country, Region, VisitedRegion
|
||||||
|
from .serializers import CountrySerializer, RegionSerializer, VisitedRegionSerializer
|
||||||
|
from rest_framework import viewsets
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def regions_by_country(request, country_code):
|
||||||
|
# require authentication
|
||||||
|
country = get_object_or_404(Country, country_code=country_code)
|
||||||
|
regions = Region.objects.filter(country=country)
|
||||||
|
serializer = RegionSerializer(regions, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def visits_by_country(request, country_code):
|
||||||
|
country = get_object_or_404(Country, country_code=country_code)
|
||||||
|
visits = VisitedRegion.objects.filter(region__country=country)
|
||||||
|
|
||||||
|
serializer = VisitedRegionSerializer(visits, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
queryset = Country.objects.all()
|
||||||
|
serializer_class = CountrySerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
class RegionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
queryset = Region.objects.all()
|
||||||
|
serializer_class = RegionSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
class VisitedRegionViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = VisitedRegion.objects.all()
|
||||||
|
serializer_class = VisitedRegionSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
Binary file not shown.
Before Width: | Height: | Size: 127 KiB |
Binary file not shown.
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
Before Width: | Height: | Size: 160 KiB |
Binary file not shown.
Before Width: | Height: | Size: 158 KiB |
|
@ -1,12 +0,0 @@
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- DATABASE_URL=
|
|
||||||
# ORIGIN is only necessary when not using a reverse proxy or hosting that includes https
|
|
||||||
- ORIGIN=http://localhost:3000
|
|
||||||
- SKIP_DB_WAIT=true
|
|
||||||
# Only necessary for externaly hosted databases such as NeonDB
|
|
||||||
# docker compose -f ./compose-dev.yml up --build
|
|
|
@ -1,43 +1,46 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
build: .
|
build:
|
||||||
ports:
|
context: ./frontend/
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgres://adventurelog:PO24VjITwGgk@db:5432/adventurelog
|
- PUBLIC_SERVER_URL=http://server:8000
|
||||||
# ORIGIN is only necessary when not using a reverse proxy or hosting that includes https
|
- ORIGIN=http://localhost:8080
|
||||||
- ORIGIN=http://localhost:3000
|
- BODY_SIZE_LIMIT=Infinity
|
||||||
# SKIP_DB_WAIT: Only necessary for externally hosted databases such as NeonDB which have their own health checks!
|
ports:
|
||||||
- SKIP_DB_WAIT=false
|
- "8080:3000"
|
||||||
- AWS_ACCESS_KEY_ID=minioadmin
|
depends_on:
|
||||||
- AWS_SECRET_ACCESS_KEY=minioadmin
|
- server
|
||||||
- AWS_S3_ENDPOINT=http://minio:9000
|
|
||||||
# MINIO_CLIENT_OVERRIDE: Only necessary if using minio here with this docker compose file. This is becaues the client needs a different endpoint than the server because its not in the docker network.
|
|
||||||
- MINIO_CLIENT_OVERRIDE=http://localhost:9000
|
|
||||||
- BODY_SIZE_LIMIT=Infinity # change this to a smaller value if you want to limit the size of uploaded files!
|
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: database
|
||||||
|
POSTGRES_USER: adventure
|
||||||
|
POSTGRES_PASSWORD: changeme123
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
|
||||||
|
server:
|
||||||
|
build: ./backend/
|
||||||
|
environment:
|
||||||
|
- PGHOST=db
|
||||||
|
- PGDATABASE=database
|
||||||
|
- PGUSER=adventure
|
||||||
|
- PGPASSWORD=changeme123
|
||||||
|
- SECRET_KEY=changeme123
|
||||||
|
- DJANGO_ADMIN_USERNAME=admin
|
||||||
|
- DJANGO_ADMIN_PASSWORD=admin
|
||||||
|
- DJANGO_ADMIN_EMAIL=admin@example.com
|
||||||
|
- PUBLIC_URL='http://127.0.0.1:8000'
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- minio
|
|
||||||
db:
|
|
||||||
image: postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: adventurelog
|
|
||||||
POSTGRES_PASSWORD: PO24VjITwGgk
|
|
||||||
POSTGRES_DB: adventurelog
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
minio:
|
|
||||||
image: quay.io/minio/minio
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=minioadmin
|
|
||||||
- MINIO_ROOT_PASSWORD=minioadmin
|
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- adventurelog_media:/code/media/
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
minio_data:
|
postgres_data:
|
||||||
|
adventurelog_media:
|
||||||
|
|
20
documentation/.gitignore
vendored
Normal file
20
documentation/.gitignore
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
41
documentation/README.md
Normal file
41
documentation/README.md
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
3
documentation/babel.config.js
Normal file
3
documentation/babel.config.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
};
|
12
documentation/blog/2019-05-28-first-blog-post.md
Normal file
12
documentation/blog/2019-05-28-first-blog-post.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
slug: first-blog-post
|
||||||
|
title: First Blog Post
|
||||||
|
authors:
|
||||||
|
name: Gao Wei
|
||||||
|
title: Docusaurus Core Team
|
||||||
|
url: https://github.com/wgao19
|
||||||
|
image_url: https://github.com/wgao19.png
|
||||||
|
tags: [hola, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
44
documentation/blog/2019-05-29-long-blog-post.md
Normal file
44
documentation/blog/2019-05-29-long-blog-post.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
slug: long-blog-post
|
||||||
|
title: Long Blog Post
|
||||||
|
authors: endi
|
||||||
|
tags: [hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the summary of a very long blog post,
|
||||||
|
|
||||||
|
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||||
|
|
||||||
|
<!--truncate-->
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
20
documentation/blog/2021-08-01-mdx-blog-post.mdx
Normal file
20
documentation/blog/2021-08-01-mdx-blog-post.mdx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
slug: mdx-blog-post
|
||||||
|
title: MDX Blog Post
|
||||||
|
authors: [slorber]
|
||||||
|
tags: [docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
Use the power of React to create interactive blog posts.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||||
|
|
||||||
|
:::
|
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
25
documentation/blog/2021-08-26-welcome/index.md
Normal file
25
documentation/blog/2021-08-26-welcome/index.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
slug: welcome
|
||||||
|
title: Welcome
|
||||||
|
authors: [slorber, yangshun]
|
||||||
|
tags: [facebook, hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||||
|
|
||||||
|
Simply add Markdown files (or folders) to the `blog` directory.
|
||||||
|
|
||||||
|
Regular blog authors can be added to `authors.yml`.
|
||||||
|
|
||||||
|
The blog post date can be extracted from filenames, such as:
|
||||||
|
|
||||||
|
- `2019-05-30-welcome.md`
|
||||||
|
- `2019-05-30-welcome/index.md`
|
||||||
|
|
||||||
|
A blog post folder can be convenient to co-locate blog post images:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The blog supports tags as well!
|
||||||
|
|
||||||
|
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
17
documentation/blog/authors.yml
Normal file
17
documentation/blog/authors.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
endi:
|
||||||
|
name: Endilie Yacop Sucipto
|
||||||
|
title: Maintainer of Docusaurus
|
||||||
|
url: https://github.com/endiliey
|
||||||
|
image_url: https://github.com/endiliey.png
|
||||||
|
|
||||||
|
yangshun:
|
||||||
|
name: Yangshun Tay
|
||||||
|
title: Front End Engineer @ Facebook
|
||||||
|
url: https://github.com/yangshun
|
||||||
|
image_url: https://github.com/yangshun.png
|
||||||
|
|
||||||
|
slorber:
|
||||||
|
name: Sébastien Lorber
|
||||||
|
title: Docusaurus maintainer
|
||||||
|
url: https://sebastienlorber.com
|
||||||
|
image_url: https://github.com/slorber.png
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue