mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-18 12:29:37 +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,26 +1,27 @@
|
|||
// 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
|
||||
{
|
||||
"name": "AdventureLog",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
}
|
||||
"name": "Ubuntu",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/base:jammy",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {},
|
||||
"ghcr.io/devcontainers/features/python:1": {}
|
||||
}
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// 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": "uname -a",
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "uname -a",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
// Configure tool-specific properties.
|
||||
// "customizations": {},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -1,18 +1,18 @@
|
|||
# Contributing to AdventureLog
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
|
||||
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. Please make sure you create an issue first for your change so you can link any pull requests to this issue. There should be a clear relationship between pull requests and issues.
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||
variables, exposed ports, useful file locations and container parameters.
|
||||
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
|
||||
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
|
||||
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
|
||||
do not have permission to do that, you may request the second reviewer to merge it for you.
|
||||
|
||||
## Code of Conduct
|
||||
|
@ -31,21 +31,21 @@ orientation.
|
|||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
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
|
||||
|
||||
### Our Responsibilities
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
|
|||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -11,7 +11,7 @@ _**⚠️ AdventureLog is in early development and is not recommended for produc
|
|||
### Docker 🐋 (Recomended)
|
||||
|
||||
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
|
||||
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.
|
||||
|
@ -20,7 +20,7 @@ _**⚠️ AdventureLog is in early development and is not recommended for produc
|
|||
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
## Screenshots 🖼️
|
||||
<!-- ## Screenshots 🖼️
|
||||
|
||||

|
||||

|
||||
|
@ -43,4 +43,4 @@ AdventureLog is licensed under the GNU General Public License v3.0.
|
|||
- Improved mobile device support
|
||||
- Password reset functionality
|
||||
- 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:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
build:
|
||||
context: ./frontend/
|
||||
environment:
|
||||
- DATABASE_URL=postgres://adventurelog:PO24VjITwGgk@db:5432/adventurelog
|
||||
# ORIGIN is only necessary when not using a reverse proxy or hosting that includes https
|
||||
- ORIGIN=http://localhost:3000
|
||||
# SKIP_DB_WAIT: Only necessary for externally hosted databases such as NeonDB which have their own health checks!
|
||||
- SKIP_DB_WAIT=false
|
||||
- AWS_ACCESS_KEY_ID=minioadmin
|
||||
- AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
- 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!
|
||||
- PUBLIC_SERVER_URL=http://server:8000
|
||||
- ORIGIN=http://localhost:8080
|
||||
- BODY_SIZE_LIMIT=Infinity
|
||||
ports:
|
||||
- "8080:3000"
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
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:
|
||||
- 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:
|
||||
- minio_data:/data
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
- adventurelog_media:/code/media/
|
||||
|
||||
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.
|
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