diff --git a/.gitignore b/.gitignore
index 4c49bd7..bbe9cb2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,112 @@
+# Django #
+*.log
+*.pot
+*.pyc
+__pycache__
+db.sqlite3
+
+# Backup files #
+*.bak
+
+# just going to ignore the whole .idea folder
+.idea/
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Python #
+*.py[cod]
+*$py.class
+
+# Distribution / packaging
+.Python build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+.pytest_cache/
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery
+celerybeat-schedule.*
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# Sublime Text #
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+*.sublime-workspace
+*.sublime-project
+
+# sftp configuration file
+sftp-config.json
+
+# Package control specific files Package
+Control.last-run
+Control.ca-list
+Control.ca-bundle
+Control.system-ca-bundle
+GitHub.sublime-settings
+
+# Visual Studio Code #
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index a55e7a1..0000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index 5cb71ef..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/palguybuddydude.iml b/.idea/palguybuddydude.iml
deleted file mode 100644
index 0e9dd5a..0000000
--- a/.idea/palguybuddydude.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/django-app/.dockerignore b/django-app/.dockerignore
new file mode 100644
index 0000000..4ea7deb
--- /dev/null
+++ b/django-app/.dockerignore
@@ -0,0 +1,6 @@
+# these can cause issues with the test runner when it is trying to collect the tests
+*.pyc
+**/__pycache__
+
+# allows installing pipenv dependencies locally in a virtual environment
+.venv
diff --git a/django-app/.env.template b/django-app/.env.template
new file mode 100644
index 0000000..24af7c7
--- /dev/null
+++ b/django-app/.env.template
@@ -0,0 +1,16 @@
+# debug
+DEBUG=True
+DEBUG_EXTENSIONS=True
+
+# list of allowed hosts
+ALLOWED_HOSTS=127.0.0.1,localhost,0.0.0.0
+
+# path to postgres server when running locally. This value overwritten in docker-compose.yml
+POSTGRES_HOST=0.0.0.0
+POSTGRES_PASSWORD=postgres
+POSTGRES_DB=postgres
+POSTGRES_USER=postgres
+POSTGRES_PORT=5432
+
+# django stuff
+DJANGO_SECRET_KEY=SECRET_KEY_GOES_HERE
diff --git a/django-app/.gitignore b/django-app/.gitignore
new file mode 100644
index 0000000..0492696
--- /dev/null
+++ b/django-app/.gitignore
@@ -0,0 +1,8 @@
+# data for management commands
+**/management/commands/data/
+
+# media files location
+app/media/
+
+# static files location
+app/static/
diff --git a/django-app/Dockerfile b/django-app/Dockerfile
new file mode 100644
index 0000000..14ca9d4
--- /dev/null
+++ b/django-app/Dockerfile
@@ -0,0 +1,25 @@
+FROM python:3.11-slim as base
+ENV PYTHONUNBUFFERED 1
+
+# install pipenv and compilation dependencies
+# https://cryptography.io/en/latest/installation/
+RUN apt-get update && apt-get install -y build-essential libssl-dev libffi-dev python3-dev cargo
+RUN pip install --upgrade pip pipenv
+
+# install python dependencies
+RUN mkdir /code
+WORKDIR /code
+COPY Pipfile /code
+COPY Pipfile.lock /code
+RUN pipenv install --dev --deploy --system
+
+# install application into container
+COPY . /code
+
+# run application
+EXPOSE 8000
+
+WORKDIR /code/app
+
+# this CMD is overridden by the command in docker-compose.yml for local development
+CMD /code/bin/wait-for-it.sh db:5432 -- python /code/app/manage.py runserver 0.0.0.0:8000
diff --git a/django-app/Pipfile b/django-app/Pipfile
new file mode 100644
index 0000000..d1b65e0
--- /dev/null
+++ b/django-app/Pipfile
@@ -0,0 +1,19 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+django = "==5.0.2"
+django-extensions = "==3.2.3"
+psycopg2-binary = "==2.9.9"
+stringcase = "==1.2.0"
+
+[dev-packages]
+pytest = "==8.0.0"
+pytest-cov = "==4.1.0"
+pytest-django = "==4.8.0"
+factory-boy = "==3.3.0"
+
+[requires]
+python_version = "3.11.4"
diff --git a/django-app/Pipfile.lock b/django-app/Pipfile.lock
new file mode 100644
index 0000000..b45a224
--- /dev/null
+++ b/django-app/Pipfile.lock
@@ -0,0 +1,287 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "1308de52680d1b4eef7392a11288a0c9b72a45d5aa824f755e83fdafe94441fd"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.11.4"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {
+ "asgiref": {
+ "hashes": [
+ "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e",
+ "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==3.7.2"
+ },
+ "django": {
+ "hashes": [
+ "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4",
+ "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.10'",
+ "version": "==5.0.2"
+ },
+ "django-extensions": {
+ "hashes": [
+ "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a",
+ "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.6'",
+ "version": "==3.2.3"
+ },
+ "psycopg2-binary": {
+ "hashes": [
+ "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9",
+ "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77",
+ "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e",
+ "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84",
+ "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3",
+ "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2",
+ "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67",
+ "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876",
+ "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152",
+ "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f",
+ "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a",
+ "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6",
+ "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503",
+ "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f",
+ "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493",
+ "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996",
+ "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f",
+ "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e",
+ "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59",
+ "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94",
+ "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7",
+ "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682",
+ "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420",
+ "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae",
+ "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291",
+ "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe",
+ "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980",
+ "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93",
+ "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692",
+ "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119",
+ "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716",
+ "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472",
+ "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b",
+ "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2",
+ "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc",
+ "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c",
+ "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5",
+ "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab",
+ "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984",
+ "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9",
+ "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf",
+ "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0",
+ "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f",
+ "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212",
+ "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb",
+ "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be",
+ "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90",
+ "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041",
+ "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7",
+ "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860",
+ "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d",
+ "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245",
+ "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27",
+ "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417",
+ "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359",
+ "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202",
+ "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0",
+ "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7",
+ "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba",
+ "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1",
+ "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd",
+ "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07",
+ "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98",
+ "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55",
+ "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d",
+ "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972",
+ "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f",
+ "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e",
+ "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26",
+ "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957",
+ "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53",
+ "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==2.9.9"
+ },
+ "sqlparse": {
+ "hashes": [
+ "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3",
+ "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==0.4.4"
+ },
+ "stringcase": {
+ "hashes": [
+ "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"
+ ],
+ "index": "pypi",
+ "version": "==1.2.0"
+ }
+ },
+ "develop": {
+ "coverage": {
+ "extras": [
+ "toml"
+ ],
+ "hashes": [
+ "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61",
+ "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1",
+ "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7",
+ "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7",
+ "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75",
+ "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd",
+ "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35",
+ "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04",
+ "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6",
+ "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042",
+ "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166",
+ "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1",
+ "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d",
+ "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c",
+ "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66",
+ "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70",
+ "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1",
+ "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676",
+ "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630",
+ "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a",
+ "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74",
+ "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad",
+ "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19",
+ "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6",
+ "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448",
+ "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018",
+ "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218",
+ "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756",
+ "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54",
+ "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45",
+ "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628",
+ "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968",
+ "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d",
+ "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25",
+ "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60",
+ "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950",
+ "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06",
+ "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295",
+ "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b",
+ "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c",
+ "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc",
+ "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74",
+ "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1",
+ "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee",
+ "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011",
+ "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156",
+ "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766",
+ "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5",
+ "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581",
+ "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016",
+ "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c",
+ "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==7.4.1"
+ },
+ "factory-boy": {
+ "hashes": [
+ "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c",
+ "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==3.3.0"
+ },
+ "faker": {
+ "hashes": [
+ "sha256:0520a6b97e07c658b2798d7140971c1d5bc4bcd5013e7937fe075fd054aa320c",
+ "sha256:f07b64d27f67b62c7f0536a72f47813015b3b51cd4664918454011094321e464"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==23.2.1"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+ "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.0"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
+ "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==23.2"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981",
+ "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.4.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c",
+ "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==8.0.0"
+ },
+ "pytest-cov": {
+ "hashes": [
+ "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
+ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.7'",
+ "version": "==4.1.0"
+ },
+ "pytest-django": {
+ "hashes": [
+ "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90",
+ "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==4.8.0"
+ },
+ "python-dateutil": {
+ "hashes": [
+ "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.8.2"
+ },
+ "six": {
+ "hashes": [
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.16.0"
+ }
+ }
+}
diff --git a/django-app/README.md b/django-app/README.md
new file mode 100644
index 0000000..efd53b7
--- /dev/null
+++ b/django-app/README.md
@@ -0,0 +1,42 @@
+# Django w/ Postgres starter
+
+* git clone the repo to your machine
+* find and replace instances of `yourproject` with the name of your project
+* `$ cd packages/django-app`
+* `python -m venv .venv`
+ * not technically necessary, but useful for installing locally to add pip packages and update the requirements.txt file
+ * `$ source .venv/bin/activate`
+ * `$ pip install pipenv`
+ * `$ PIPENV_VENV_IN_PROJECT=1 pipenv install --dev --deploy`
+ * `$ pipenv install some_package`
+* `$ cp .env.template .env`
+* `$ docker-compose build`
+* `$ ./utils/create-docker-volumes.sh`
+* `$ ./bin/dcp-generate-secret-key.sh`
+ * copy and paste the output from this command into `.env` replacing `SECRET_KEY_GOES_HERE`
+* `$ ./bin/dcp-django-admin.sh migrate`
+
+* now, you have two options
+ * create your own superuser
+ * `$ ./bin/dcp-django-admin.sh createsuperuser`
+ * load db w/ user admin@email:password
+ * `$ ./utils/reload-docker-db.sh --data=dev_data.json`
+
+* `$ docker-compose up web`
+* you can now login with your superuser at 0.0.0.0:8000/admin
+
+## helpful scripts
+* `$ ./utils/dcp-run-tests.sh`
+ * runs all tests, except those decorated with `@pytest.mark.integration`
+ * tests.py test_*.py *_test.py *_tests.py
+* `$ ./bin/dcp-django-admin.sh`
+ * runs `manage.py` in the docker container with argument passthrough
+ * `$ ./bin/dcp-django-admin.sh makemigrations`
+ * `$ ./bin/dcp-django-admin.sh migrate`
+ * `$ ./bin/dcp-django-admin.sh startapp payments`
+* `$ ./utils/reload-docker-db.sh`
+ * reloads `dev_data.json` by default
+ * `$ ./utils/reload-docker-db.sh --data=fixture_filename.json`
+* `$ ./utils/dump-data.sh`
+ * `$ ./utils/dump-data.sh > app/core/fixtures/dump-2021-10-08.json`
+ * you can then reload these files with `./utils/reload-docker-db.sh`
diff --git a/go.sum b/django-app/app/app/__init__.py
similarity index 100%
rename from go.sum
rename to django-app/app/app/__init__.py
diff --git a/django-app/app/app/asgi.py b/django-app/app/app/asgi.py
new file mode 100644
index 0000000..3163a3a
--- /dev/null
+++ b/django-app/app/app/asgi.py
@@ -0,0 +1,16 @@
+"""
+ASGI config for app project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
+
+application = get_asgi_application()
diff --git a/django-app/app/app/settings.py b/django-app/app/app/settings.py
new file mode 100644
index 0000000..776b3ca
--- /dev/null
+++ b/django-app/app/app/settings.py
@@ -0,0 +1,153 @@
+"""
+Django settings for app project.
+
+Generated by 'django-admin startproject' using Django 4.0.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/4.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/4.0/ref/settings/
+"""
+
+import os
+from pathlib import Path
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = os.environ['DJANGO_SECRET_KEY']
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = os.environ['DEBUG']
+
+try:
+ ALLOWED_HOSTS = os.environ['ALLOWED_HOSTS'].split(',')
+except KeyError:
+ ALLOWED_HOSTS = []
+
+# auth
+AUTH_USER_MODEL = 'core.User'
+# authentication for django admin
+AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+]
+
+# Application definition
+
+INSTALLED_APPS = [
+ # django
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ # contrib
+ 'django_extensions',
+
+ # own
+ 'core',
+ 'palworld_core',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ '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',
+]
+
+ROOT_URLCONF = 'app.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ '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',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'app.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': os.environ['POSTGRES_DB'],
+ 'USER': os.environ['POSTGRES_USER'],
+ 'PASSWORD': os.environ['POSTGRES_PASSWORD'],
+ 'HOST': os.environ['POSTGRES_HOST'],
+ 'PORT': os.environ['POSTGRES_PORT'],
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/4.0/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/4.0/howto/static-files/
+
+STATIC_URL = os.environ.get('STATIC_URL', '/static/')
+
+# path to static directory in docker container.
+# this is where files are created when running `collectstatic`
+STATIC_ROOT = os.environ.get('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
+
+# https://docs.djangoproject.com/en/4.0/topics/files/
+MEDIA_URL = os.environ.get('MEDIA_URL', '/media/')
+MEDIA_ROOT = os.environ.get('MEDIA_ROOT', os.path.join(BASE_DIR, 'media'))
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+SITE_URL = os.environ.get('SITE_URL', '0.0.0.0')
diff --git a/django-app/app/app/test_settings.py b/django-app/app/app/test_settings.py
new file mode 100644
index 0000000..70eecde
--- /dev/null
+++ b/django-app/app/app/test_settings.py
@@ -0,0 +1,6 @@
+from .settings import *
+
+"""
+Adds `common` app to installed apps so that test models are only created for tests.
+"""
+INSTALLED_APPS += ['common']
diff --git a/django-app/app/app/urls.py b/django-app/app/app/urls.py
new file mode 100644
index 0000000..1e0fd9a
--- /dev/null
+++ b/django-app/app/app/urls.py
@@ -0,0 +1,21 @@
+"""app URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/4.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+]
diff --git a/django-app/app/app/wsgi.py b/django-app/app/app/wsgi.py
new file mode 100644
index 0000000..0efb709
--- /dev/null
+++ b/django-app/app/app/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for app 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/4.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
+
+application = get_wsgi_application()
diff --git a/django-app/app/common/commands/abstract_base_command.py b/django-app/app/common/commands/abstract_base_command.py
new file mode 100644
index 0000000..bd83713
--- /dev/null
+++ b/django-app/app/common/commands/abstract_base_command.py
@@ -0,0 +1,18 @@
+from abc import ABC, abstractmethod
+
+from django.core.exceptions import ValidationError
+
+from common.forms.base_form import BaseForm
+
+
+class AbstractBaseCommand(ABC):
+ """
+ The Command interface declares a method for executing a command.
+ """
+
+ form: BaseForm
+
+ @abstractmethod
+ def execute(self) -> None:
+ if not self.form.is_valid():
+ raise ValidationError(self.form.errors.as_json())
diff --git a/django-app/app/common/forms/base_form.py b/django-app/app/common/forms/base_form.py
new file mode 100644
index 0000000..1e4e5aa
--- /dev/null
+++ b/django-app/app/common/forms/base_form.py
@@ -0,0 +1,39 @@
+from django import forms
+from stringcase import snakecase
+
+
+def snake_case_and_rename_id(key):
+ """
+ snake cases key and renames to 'pk' if 'id', because 'id' shadows built in
+ """
+ new_key = snakecase(key)
+ if new_key == 'id':
+ new_key = 'pk'
+ return new_key
+
+
+class BaseForm(forms.Form):
+ def __init__(self, data):
+ """
+ Snake cases all form keys.
+ Ex: `createdBy` -> `created_by`
+ """
+ transformed_data = {
+ snake_case_and_rename_id(key): val for key, val in data.items()
+ }
+ super().__init__(transformed_data)
+
+ def clean(self, *args, **kwargs):
+ cleaned_data = super().clean()
+
+ # NOTE - Django forms will set non required fields to None or to an empty string if
+ # the form field is not passed into the form.
+
+ # Filter for fields that are passed through the form and remove fields that where
+ # not passed into the form.
+ cleaned_data = {
+ form_field: cleaned_data[form_field]
+ for form_field in self.data if form_field in cleaned_data
+ }
+
+ return cleaned_data
diff --git a/django-app/app/common/forms/created_by_form_mixin.py b/django-app/app/common/forms/created_by_form_mixin.py
new file mode 100644
index 0000000..d30b715
--- /dev/null
+++ b/django-app/app/common/forms/created_by_form_mixin.py
@@ -0,0 +1,16 @@
+from django import forms
+
+from common.forms.base_form import BaseForm
+from core.repositories import UserRepository, AccountRepository
+
+
+class CreatedByFormMixin(BaseForm):
+ created_by = forms.ModelChoiceField(queryset=UserRepository.get_queryset())
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # created_by is not required when updating
+ form_name = type(self).__name__
+ if form_name.startswith('Update'):
+ self.fields['created_by'].required = False
diff --git a/django-app/app/common/management/commands/base_management_command.py b/django-app/app/common/management/commands/base_management_command.py
new file mode 100644
index 0000000..c54ddec
--- /dev/null
+++ b/django-app/app/common/management/commands/base_management_command.py
@@ -0,0 +1,27 @@
+from django.core.management.base import BaseCommand
+
+
+class BaseManagementCommand(BaseCommand):
+ """
+ This class if for containing logic that
+ we want shared across all management commands.
+ """
+
+ def add_arguments(self, parser):
+ """
+ Add dry_run argument for every management command by default.
+ """
+ parser.add_argument(
+ '--dry_run',
+ help='Run script with out saving changes to database.',
+ action='store_true'
+ )
+
+ def handle(self, *args, **options):
+ """
+ The actual logic of the command. Subclasses must implement
+ this method.
+
+ Copied directly from Django's code from django/core/management/base.py
+ """
+ raise NotImplementedError('subclasses of BaseManagementCommand must provide a handle() method')
diff --git a/django-app/app/common/management/helpers.py b/django-app/app/common/management/helpers.py
new file mode 100644
index 0000000..cd46aa2
--- /dev/null
+++ b/django-app/app/common/management/helpers.py
@@ -0,0 +1,28 @@
+def get_file_contents(path):
+ """
+ Read file at path and return contents.
+ Tries multiple encodings.
+ """
+ encodings = ['utf-8', 'windows-1250', 'windows-1252'] # add more
+ file_reader = None
+ file_contents = None
+ for e in encodings:
+ try:
+ file_reader = open(path, 'r', encoding=e)
+ file_contents = file_reader.read()
+ except UnicodeDecodeError:
+ print(f'got unicode error with {e}, trying different encoding')
+ finally:
+ if file_reader:
+ file_reader.close()
+
+ return file_contents
+
+
+def write_file_contents(path, contents):
+ """
+ Write file contents at path.
+ """
+ f = open(path, 'w')
+ f.write(contents)
+ f.close()
diff --git a/django-app/app/common/managers/soft_delete_model_manager.py b/django-app/app/common/managers/soft_delete_model_manager.py
new file mode 100644
index 0000000..c5c11fa
--- /dev/null
+++ b/django-app/app/common/managers/soft_delete_model_manager.py
@@ -0,0 +1,21 @@
+from django.db import models
+from django.utils.timezone import now
+
+
+class SoftDeleteQuerySet(models.query.QuerySet):
+ def delete(self, *args, **kwargs):
+ if kwargs.get('force_delete', None):
+ return super().delete()
+
+ return super().update(is_active=False, deleted_at=now())
+
+ def undelete(self, *args, **kwargs):
+ return super().update(is_active=True, deleted_at=None)
+
+
+class SoftDeleteModelManager(models.Manager):
+ """
+ Custom manager for models that use SoftDeleteTimestampMixin.
+ """
+ def get_queryset(self):
+ return SoftDeleteQuerySet(self.model)
diff --git a/django-app/app/common/models/created_by_mixin.py b/django-app/app/common/models/created_by_mixin.py
new file mode 100644
index 0000000..84c9d8a
--- /dev/null
+++ b/django-app/app/common/models/created_by_mixin.py
@@ -0,0 +1,15 @@
+from django.db import models
+
+
+class CreatedByMixin(models.Model):
+ """
+ Adds `created_by` field.
+ """
+ created_by = models.ForeignKey(
+ 'core.User',
+ db_index=True,
+ related_name='%(class)ss_created', # eg user.documents_created
+ on_delete=models.CASCADE)
+
+ class Meta:
+ abstract = True
diff --git a/django-app/app/common/models/crud_timestamps_mixin.py b/django-app/app/common/models/crud_timestamps_mixin.py
new file mode 100644
index 0000000..6b35f90
--- /dev/null
+++ b/django-app/app/common/models/crud_timestamps_mixin.py
@@ -0,0 +1,13 @@
+from django.db import models
+
+
+class CRUDTimestampsMixin(models.Model):
+ """
+ `created_at` will be set on creation.
+ `modified_at` will be updated on saves.
+ """
+ created_at = models.DateTimeField(auto_now_add=True, db_index=True)
+ modified_at = models.DateTimeField(auto_now=True, db_index=True)
+
+ class Meta:
+ abstract = True
diff --git a/django-app/app/common/models/soft_delete_timestamp_mixin.py b/django-app/app/common/models/soft_delete_timestamp_mixin.py
new file mode 100644
index 0000000..dca40cc
--- /dev/null
+++ b/django-app/app/common/models/soft_delete_timestamp_mixin.py
@@ -0,0 +1,40 @@
+from django.db import models
+from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
+
+from common.managers.soft_delete_model_manager import SoftDeleteModelManager
+
+is_active_help_text = _(
+ 'Designates whether this object should be treated as active. '
+ 'Unselect this instead of deleting objects.'
+)
+
+
+class SoftDeleteTimestampMixin(models.Model):
+ """
+ Set `is_active` to False instead of deleting rows in the database.
+ `deleted_at` will be set to the current time at time of deletion.
+ """
+ deleted_at = models.DateTimeField(db_index=True, null=True)
+ is_active = models.BooleanField(_('active'),
+ db_index=True,
+ default=True,
+ help_text=is_active_help_text)
+
+ objects = SoftDeleteModelManager()
+
+ def delete(self, *args, **kwargs):
+ if kwargs.pop('force_delete', None):
+ super().delete(*args, **kwargs)
+ else:
+ self.deleted_at = timezone.now()
+ self.is_active = False
+ super().save()
+
+ def undelete(self, *args, **kwargs):
+ self.is_active = True
+ self.deleted_at = None
+ super().save()
+
+ class Meta:
+ abstract = True
diff --git a/django-app/app/common/models/uuid_mixin.py b/django-app/app/common/models/uuid_mixin.py
new file mode 100644
index 0000000..cd6f1b6
--- /dev/null
+++ b/django-app/app/common/models/uuid_mixin.py
@@ -0,0 +1,17 @@
+import uuid
+from django.db import models
+from django.template.defaultfilters import truncatechars
+
+
+class UUIDModelMixin(models.Model):
+ """
+ `uuid` field will be auto set with uuid4 values
+ """
+ uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
+
+ class Meta:
+ abstract = True
+
+ @property
+ def short_uuid(self):
+ return truncatechars(self.uuid, 8)
diff --git a/django-app/app/common/repositories/base_repository.py b/django-app/app/common/repositories/base_repository.py
new file mode 100644
index 0000000..b9fac7f
--- /dev/null
+++ b/django-app/app/common/repositories/base_repository.py
@@ -0,0 +1,32 @@
+from common.models.soft_delete_timestamp_mixin import SoftDeleteTimestampMixin
+
+NOT_IMPLEMENTED_ERROR_MESSAGE = 'You must define a `model` on the inheriting Repository'
+
+
+class BaseRepository:
+ model = None
+
+ @classmethod
+ def get(cls, *, pk):
+ if not cls.model:
+ raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MESSAGE)
+
+ try:
+ instance = cls.model.objects.get(pk=pk)
+ except cls.model.DoesNotExist:
+ return None
+
+ return instance
+
+ @classmethod
+ def get_queryset(cls, queryset=None):
+ if queryset is None:
+ if cls.model is None:
+ raise NotImplementedError(NOT_IMPLEMENTED_ERROR_MESSAGE)
+
+ if issubclass(cls.model, SoftDeleteTimestampMixin):
+ return cls.model.objects.filter(is_active=True)
+
+ return cls.model.objects.all()
+
+ return queryset
diff --git a/django-app/app/core/__init__.py b/django-app/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/core/admin.py b/django-app/app/core/admin.py
new file mode 100644
index 0000000..6711a6f
--- /dev/null
+++ b/django-app/app/core/admin.py
@@ -0,0 +1,142 @@
+# Register your models here.
+from core.models import User
+from django import forms
+from django.contrib import admin
+from django.contrib.auth import password_validation
+from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
+from django.contrib.auth.forms import UserChangeForm
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
+
+
+class UserCreationForm(forms.ModelForm):
+ """
+ A form that creates a user, with no privileges, from the given username and
+ password.
+ """
+ error_messages = {
+ 'password_mismatch': _("The two password fields didn't match."),
+ }
+ password1 = forms.CharField(
+ label=_("Password"),
+ strip=False,
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
+ help_text=password_validation.password_validators_help_text_html(),
+ required=False,
+ )
+ password2 = forms.CharField(
+ label=_("Password confirmation"),
+ widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
+ strip=False,
+ help_text=_("Enter the same password as before, for verification."),
+ required=False,
+ )
+
+ class Meta:
+ model = User
+ fields = ("email",)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if self._meta.model.USERNAME_FIELD in self.fields:
+ self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True
+
+ def clean_password2(self):
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise ValidationError(
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
+ return password2
+
+ def _post_clean(self):
+ super()._post_clean()
+ # Validate the password after self.instance is updated with form data
+ # by super().
+ password = self.cleaned_data.get('password2')
+ if password:
+ try:
+ password_validation.validate_password(password, self.instance)
+ except ValidationError as error:
+ self.add_error('password2', error)
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+
+ if self.cleaned_data['password1']:
+ user.set_password(self.cleaned_data["password1"])
+
+ if commit:
+ user.save()
+ return user
+
+
+class UserEditForm(UserChangeForm):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # NOTE - add fields that are not required here
+ # self.fields['activation_code'].required = False
+
+ class Meta:
+ model = User
+ fields = '__all__'
+
+
+@admin.register(User)
+class UserAdmin(DjangoUserAdmin):
+ form = UserEditForm
+ add_form = UserCreationForm
+ add_fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': (
+ 'email',
+ 'password1',
+ 'password2',
+ 'is_superuser',
+ 'is_staff',
+ 'is_active',
+ )
+ }),)
+
+ fieldsets = (
+ (None, {'fields': ('email',
+ 'password',)}),
+
+ ('Permissions', {'fields': ('is_staff',
+ 'is_superuser',
+ 'groups',
+ 'user_permissions')}),
+
+ ('Important', {'fields': ('is_active',
+ 'imported_at',
+ 'created_at',
+ 'modified_at',
+ 'deleted_at')}),
+ )
+
+ list_display = [
+ 'id',
+ 'is_active',
+ 'is_staff',
+ 'is_superuser',
+ 'email',
+ ]
+
+ list_display_links = ['id', 'email']
+
+ list_filter = [
+ 'is_staff',
+ 'is_superuser',
+ 'is_active',
+ ]
+
+ ordering = ['id']
+
+ readonly_fields = ['created_at', 'deleted_at', 'modified_at']
+
+ search_fields = (
+ 'email',
+ )
diff --git a/django-app/app/core/apps.py b/django-app/app/core/apps.py
new file mode 100644
index 0000000..8115ae6
--- /dev/null
+++ b/django-app/app/core/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'core'
diff --git a/django-app/app/core/fixtures/dev_data.json b/django-app/app/core/fixtures/dev_data.json
new file mode 100644
index 0000000..cf112f3
--- /dev/null
+++ b/django-app/app/core/fixtures/dev_data.json
@@ -0,0 +1,280 @@
+[
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "admin",
+ "model": "logentry"
+ }
+},
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "auth",
+ "model": "permission"
+ }
+},
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "auth",
+ "model": "group"
+ }
+},
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "contenttypes",
+ "model": "contenttype"
+ }
+},
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "sessions",
+ "model": "session"
+ }
+},
+{
+ "model": "contenttypes.contenttype",
+ "fields": {
+ "app_label": "core",
+ "model": "user"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can add log entry",
+ "content_type": [
+ "admin",
+ "logentry"
+ ],
+ "codename": "add_logentry"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can change log entry",
+ "content_type": [
+ "admin",
+ "logentry"
+ ],
+ "codename": "change_logentry"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can delete log entry",
+ "content_type": [
+ "admin",
+ "logentry"
+ ],
+ "codename": "delete_logentry"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can view log entry",
+ "content_type": [
+ "admin",
+ "logentry"
+ ],
+ "codename": "view_logentry"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can add permission",
+ "content_type": [
+ "auth",
+ "permission"
+ ],
+ "codename": "add_permission"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can change permission",
+ "content_type": [
+ "auth",
+ "permission"
+ ],
+ "codename": "change_permission"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can delete permission",
+ "content_type": [
+ "auth",
+ "permission"
+ ],
+ "codename": "delete_permission"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can view permission",
+ "content_type": [
+ "auth",
+ "permission"
+ ],
+ "codename": "view_permission"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can add group",
+ "content_type": [
+ "auth",
+ "group"
+ ],
+ "codename": "add_group"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can change group",
+ "content_type": [
+ "auth",
+ "group"
+ ],
+ "codename": "change_group"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can delete group",
+ "content_type": [
+ "auth",
+ "group"
+ ],
+ "codename": "delete_group"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can view group",
+ "content_type": [
+ "auth",
+ "group"
+ ],
+ "codename": "view_group"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can add content type",
+ "content_type": [
+ "contenttypes",
+ "contenttype"
+ ],
+ "codename": "add_contenttype"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can change content type",
+ "content_type": [
+ "contenttypes",
+ "contenttype"
+ ],
+ "codename": "change_contenttype"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can delete content type",
+ "content_type": [
+ "contenttypes",
+ "contenttype"
+ ],
+ "codename": "delete_contenttype"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can view content type",
+ "content_type": [
+ "contenttypes",
+ "contenttype"
+ ],
+ "codename": "view_contenttype"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can add session",
+ "content_type": [
+ "sessions",
+ "session"
+ ],
+ "codename": "add_session"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can change session",
+ "content_type": [
+ "sessions",
+ "session"
+ ],
+ "codename": "change_session"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can delete session",
+ "content_type": [
+ "sessions",
+ "session"
+ ],
+ "codename": "delete_session"
+ }
+},
+{
+ "model": "auth.permission",
+ "fields": {
+ "name": "Can view session",
+ "content_type": [
+ "sessions",
+ "session"
+ ],
+ "codename": "view_session"
+ }
+},
+{
+ "model": "core.user",
+ "fields": {
+ "password": "pbkdf2_sha256$320000$0Qwlf7cl4AOYvOu8cWne6p$JW9Nhv9/bZkxJv5jUNLMwYAZmwgf6lU25/hmdEEuDtI=",
+ "last_login": "2021-12-10T05:52:39.842Z",
+ "is_superuser": true,
+ "created_at": "2021-12-10T05:51:01.230Z",
+ "modified_at": "2021-12-10T05:51:01.230Z",
+ "deleted_at": null,
+ "is_active": true,
+ "email": "admin@email.com",
+ "is_staff": true,
+ "groups": [],
+ "user_permissions": []
+ }
+}
+]
diff --git a/django-app/app/core/helpers.py b/django-app/app/core/helpers.py
new file mode 100644
index 0000000..f332e7e
--- /dev/null
+++ b/django-app/app/core/helpers.py
@@ -0,0 +1,46 @@
+import functools
+import random
+import secrets
+import string
+
+
+def generate_membership_token():
+ return secrets.token_urlsafe()
+
+
+def generate_email_activation_code():
+ """
+ Generate Email Activation Token to be sent for mobile devices
+ example: ZSDF123
+ """
+ token = ''.join(random.choice(string.ascii_uppercase) for i in range(4))
+ return token + str(random.randint(111, 999))
+
+
+def get_random_password():
+ letters = string.ascii_letters + string.punctuation
+ result_str = 'A1!' + ''.join(random.choice(letters) for i in range(10))
+ return result_str
+
+
+def generate_signup_key():
+ letters = string.ascii_uppercase + string.digits
+ res = ''.join(random.choice(letters) for i in range(10))
+ return res
+
+
+def rgetattr(obj, attr, *args):
+ """
+ Recursive get attribute.
+ Get attr from obj. attr can be nested.
+ Returns None if attribute does not exist.
+
+ Ex: val = rgetattr(obj, 'some.nested.property')
+ """
+
+ def _getattr(obj, attr):
+ if hasattr(obj, attr):
+ return getattr(obj, attr, *args)
+ return None
+
+ return functools.reduce(_getattr, [obj] + attr.split('.'))
diff --git a/django-app/app/core/managers/__init__.py b/django-app/app/core/managers/__init__.py
new file mode 100644
index 0000000..8227174
--- /dev/null
+++ b/django-app/app/core/managers/__init__.py
@@ -0,0 +1 @@
+from core.managers.user_manager import UserManager
diff --git a/django-app/app/core/managers/user_manager.py b/django-app/app/core/managers/user_manager.py
new file mode 100644
index 0000000..d099ec1
--- /dev/null
+++ b/django-app/app/core/managers/user_manager.py
@@ -0,0 +1,58 @@
+from django.contrib.auth.base_user import BaseUserManager
+
+from core.helpers import generate_signup_key
+
+
+class UserManager(BaseUserManager):
+ """
+ Manager used for creating users.
+ """
+ use_in_migrations = True
+
+ def _create_user(self, email, password, **extra_fields):
+ # email = self.normalize_email(email)
+ if not email:
+ raise ValueError('The email must be set')
+ user = self.model(email=email, **extra_fields)
+ user.set_password(password)
+ user.save(using=self._db)
+ return user
+
+ def create_user(self, email, password=None, **extra_fields):
+ """
+ Creates and saves a :class:`User`
+ with the given email and password.
+
+ :param email: :class:`python:str` or :func:`python:unicode`
+ Email
+ :param password: :class:`python:str` or :func:`python:unicode`
+ Password
+ :param extra_fields: :class:`python:dict`
+ Additional pairs of attribute with value to be set on
+ :class:`User` instance.
+ :return: Instance of created :class:`User`
+ :rtype: :class:`core.models.User`
+ """
+ extra_fields.setdefault('is_superuser', False)
+ return self._create_user(email, password, **extra_fields)
+
+ def create_superuser(self, email, password, **extra_fields):
+ """
+ Creates and saves a :class:`User`
+ with the given email, password and superuser privileges.
+
+ :param email: :class:`python:str`
+ Nickname
+ :param password: :class:`python:str`
+ Password
+ :param extra_fields: :class:`python:dict`
+ Additional pairs of attribute with value to be set on
+ :class:`User` instance.
+ :return: Instance of created :class:`User`
+ :rtype: :class:`core.models.User`
+ """
+ extra_fields.setdefault('is_superuser', True)
+ extra_fields.setdefault('is_active', True)
+ extra_fields.setdefault('is_staff', True)
+
+ return self._create_user(email, password, **extra_fields)
diff --git a/django-app/app/core/migrations/0001_initial.py b/django-app/app/core/migrations/0001_initial.py
new file mode 100644
index 0000000..3dff95b
--- /dev/null
+++ b/django-app/app/core/migrations/0001_initial.py
@@ -0,0 +1,41 @@
+# Generated by Django 4.0 on 2021-12-10 05:27
+
+import core.managers.user_manager
+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='User',
+ 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')),
+ ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
+ ('modified_at', models.DateTimeField(auto_now=True, db_index=True)),
+ ('deleted_at', models.DateTimeField(db_index=True, null=True)),
+ ('is_active', models.BooleanField(db_index=True, default=True, help_text='Designates whether this object should be treated as active. Unselect this instead of deleting objects.', verbose_name='active')),
+ ('email', models.EmailField(max_length=255, unique=True, verbose_name='email')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('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={
+ 'db_table': 'users',
+ 'ordering': ('id',),
+ 'default_permissions': (),
+ },
+ managers=[
+ ('objects', core.managers.user_manager.UserManager()),
+ ],
+ ),
+ ]
diff --git a/django-app/app/core/migrations/__init__.py b/django-app/app/core/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/core/models/__init__.py b/django-app/app/core/models/__init__.py
new file mode 100644
index 0000000..2219a54
--- /dev/null
+++ b/django-app/app/core/models/__init__.py
@@ -0,0 +1 @@
+from core.models.user import User
diff --git a/django-app/app/core/models/user.py b/django-app/app/core/models/user.py
new file mode 100644
index 0000000..83bdf06
--- /dev/null
+++ b/django-app/app/core/models/user.py
@@ -0,0 +1,35 @@
+from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from common.models.crud_timestamps_mixin import CRUDTimestampsMixin
+from common.models.soft_delete_timestamp_mixin import SoftDeleteTimestampMixin
+from core.managers import UserManager
+
+
+class User(CRUDTimestampsMixin,
+ SoftDeleteTimestampMixin,
+ AbstractBaseUser,
+ PermissionsMixin):
+ USERNAME_FIELD = 'email'
+ objects = UserManager()
+
+ email = models.EmailField(
+ verbose_name='email',
+ max_length=255,
+ unique=True,
+ )
+
+ is_staff = models.BooleanField(
+ _('staff status'),
+ default=False,
+ help_text=_('Designates whether the user can log into this admin site.'))
+
+ def __str__(self):
+ return self.email
+
+ class Meta:
+ db_table = 'users'
+ default_permissions = ()
+ unique_together = []
+ ordering = ('id',)
diff --git a/django-app/app/core/repositories/user_repository.py b/django-app/app/core/repositories/user_repository.py
new file mode 100644
index 0000000..4fa9248
--- /dev/null
+++ b/django-app/app/core/repositories/user_repository.py
@@ -0,0 +1,29 @@
+from common.repositories.base_repository import BaseRepository
+from core.models import User
+
+
+class UserRepository(BaseRepository):
+ model = User
+
+ @classmethod
+ def get_by_filter(cls, filter_input: dict = None):
+ if filter_input:
+ objects = cls.get_queryset().filter(**filter_input)
+ else:
+ objects = cls.get_queryset().all()
+ return objects
+
+ @classmethod
+ def create(cls, data: dict) -> 'User':
+ user = cls.model.objects.create(**data)
+ return user
+
+ @classmethod
+ def update(cls, *, pk=None, obj: 'User' = None, data: dict) -> 'User':
+ user = obj or cls.get(pk=pk)
+
+ if data.get('is_active'):
+ user.is_active = data['is_active']
+
+ user.save()
+ return user
diff --git a/django-app/app/core/tests.py b/django-app/app/core/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/django-app/app/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/django-app/app/core/views.py b/django-app/app/core/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/django-app/app/core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/django-app/app/manage.py b/django-app/app/manage.py
new file mode 100755
index 0000000..4931389
--- /dev/null
+++ b/django-app/app/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/django-app/app/palworld_analytics/__init__.py b/django-app/app/palworld_analytics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/palworld_analytics/admin.py b/django-app/app/palworld_analytics/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/django-app/app/palworld_analytics/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/django-app/app/palworld_analytics/apps.py b/django-app/app/palworld_analytics/apps.py
new file mode 100644
index 0000000..1aa9590
--- /dev/null
+++ b/django-app/app/palworld_analytics/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PalworldAnalyticsConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'palworld_analytics'
diff --git a/django-app/app/palworld_analytics/migrations/__init__.py b/django-app/app/palworld_analytics/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/palworld_analytics/models.py b/django-app/app/palworld_analytics/models.py
new file mode 100644
index 0000000..a652f5c
--- /dev/null
+++ b/django-app/app/palworld_analytics/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+
+
+class PalworldPlayerCheckIn(models.Model):
+ player = models.ForeignKey('PalworldPlayer', on_delete=models.CASCADE)
+ check_in_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ db_table = 'palworld_player_check_ins'
+ verbose_name = 'Palworld Player Check In'
+ verbose_name_plural = 'Palworld Player Check Ins'
diff --git a/django-app/app/palworld_analytics/tests.py b/django-app/app/palworld_analytics/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/django-app/app/palworld_analytics/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/django-app/app/palworld_analytics/views.py b/django-app/app/palworld_analytics/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/django-app/app/palworld_analytics/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/django-app/app/palworld_core/__init__.py b/django-app/app/palworld_core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/palworld_core/admin.py b/django-app/app/palworld_core/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/django-app/app/palworld_core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/django-app/app/palworld_core/apps.py b/django-app/app/palworld_core/apps.py
new file mode 100644
index 0000000..68421c8
--- /dev/null
+++ b/django-app/app/palworld_core/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class PalworldCoreConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'palworld_core'
diff --git a/django-app/app/palworld_core/migrations/0001_initial.py b/django-app/app/palworld_core/migrations/0001_initial.py
new file mode 100644
index 0000000..7dde062
--- /dev/null
+++ b/django-app/app/palworld_core/migrations/0001_initial.py
@@ -0,0 +1,36 @@
+# Generated by Django 5.0.2 on 2024-02-17 18:55
+
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PalworldPlayer',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
+ ('modified_at', models.DateTimeField(auto_now=True, db_index=True)),
+ ('deleted_at', models.DateTimeField(db_index=True, null=True)),
+ ('is_active', models.BooleanField(db_index=True, default=True, help_text='Designates whether this object should be treated as active. Unselect this instead of deleting objects.', verbose_name='active')),
+ ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
+ ('player_uid', models.CharField(max_length=255, unique=True)),
+ ('steam_id', models.CharField(max_length=255, unique=True)),
+ ('display_name', models.CharField(max_length=255, unique=True)),
+ ('first_seen_at', models.DateTimeField(auto_now_add=True)),
+ ('last_seen_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'verbose_name': 'Palworld Player',
+ 'verbose_name_plural': 'Palworld Players',
+ 'db_table': 'palworld_players',
+ },
+ ),
+ ]
diff --git a/django-app/app/palworld_core/migrations/__init__.py b/django-app/app/palworld_core/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/django-app/app/palworld_core/models.py b/django-app/app/palworld_core/models.py
new file mode 100644
index 0000000..d8bc8be
--- /dev/null
+++ b/django-app/app/palworld_core/models.py
@@ -0,0 +1,24 @@
+from django.db import models
+
+from common.models.crud_timestamps_mixin import CRUDTimestampsMixin
+from common.models.soft_delete_timestamp_mixin import SoftDeleteTimestampMixin
+from common.models.uuid_mixin import UUIDModelMixin
+
+
+class PalworldPlayer(UUIDModelMixin, CRUDTimestampsMixin, SoftDeleteTimestampMixin):
+ player_uid = models.CharField(max_length=255, unique=True)
+ steam_id = models.CharField(max_length=255, unique=True)
+ display_name = models.CharField(max_length=255, unique=True)
+
+ first_seen_at = models.DateTimeField(auto_now_add=True)
+ last_seen_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ db_table = 'palworld_players'
+ verbose_name = 'Palworld Player'
+ verbose_name_plural = 'Palworld Players'
+
+ @property
+ def is_online(self):
+ # FIXME - this is just a placeholder for now; logic not correct.
+ return self.last_seen_at > self.first_seen_at
diff --git a/django-app/app/palworld_core/test_helpers.py b/django-app/app/palworld_core/test_helpers.py
new file mode 100644
index 0000000..252038b
--- /dev/null
+++ b/django-app/app/palworld_core/test_helpers.py
@@ -0,0 +1,22 @@
+import factory
+
+from factory.django import DjangoModelFactory
+
+from palworld_core.models import PalworldPlayer
+
+
+class PalworldPlayerFactory(DjangoModelFactory):
+ class Meta:
+ model = PalworldPlayer
+
+ player_uid = factory.Faker('uuid4')
+ steam_id = factory.Faker('uuid4')
+ display_name = factory.Faker('name')
+ first_seen_at = factory.Faker('date_time')
+ last_seen_at = factory.Faker('date_time')
+
+ uuid = factory.Faker('uuid4')
+ created_at = factory.Faker('date_time')
+ modified_at = factory.Faker('date_time')
+ deleted_at = factory.Faker('date_time')
+ is_active = factory.Faker('boolean')
diff --git a/django-app/app/palworld_core/tests.py b/django-app/app/palworld_core/tests.py
new file mode 100644
index 0000000..ceb0645
--- /dev/null
+++ b/django-app/app/palworld_core/tests.py
@@ -0,0 +1,15 @@
+from django.test import TestCase
+
+from palworld_core.test_helpers import PalworldPlayerFactory
+
+
+class TestPalworldPlayerModel(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.player = PalworldPlayerFactory(last_seen_at='2021-01-02 00:00:00', first_seen_at='2021-01-01 00:00:00')
+
+ def test_player_is_online(self):
+ self.assertTrue(self.player.is_online)
+
+ def test_player_is_offline(self):
+ self.player = PalworldPlayerFactory(last_seen_at='2021-01-01 00:00:00', first_seen_at='2021-01-01 00:00:00')
diff --git a/django-app/app/palworld_core/views.py b/django-app/app/palworld_core/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/django-app/app/palworld_core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/django-app/bin/dcp-django-admin.sh b/django-app/bin/dcp-django-admin.sh
new file mode 100755
index 0000000..fd901cd
--- /dev/null
+++ b/django-app/bin/dcp-django-admin.sh
@@ -0,0 +1,35 @@
+#! /bin/bash
+
+# proxy to execute `manage.py` (django-admin) commands in web container
+
+function checkenv() {
+ ##############################################################
+ # check user's confidence if we are not using local database #
+ ##############################################################
+
+ # get db host envar from docker container
+ DB_HOST_ENVAR=$(docker-compose run --rm -w /code/app web env | grep POSTGRES_HOST)
+ DB_HOST=$(cut -d "=" -f2 <<< "$DB_HOST_ENVAR")
+ # bashism to trim newline
+ DB_HOST=${DB_HOST//[$'\t\r\n']}
+ if [ "$DB_HOST" != 'db' ] && [ "$DB_HOST" != '0.0.0.0' ] && [ "$DB_HOST" != 'localhost' ]
+ then
+ echo "You are running this command against the database at ${DB_HOST}!"
+ checkconfidence
+ else
+ echo "Running command against database at $DB_HOST..."
+ fi
+}
+
+function checkconfidence() {
+ read -r -p "Are you sure you want to continue? [y/N] " response
+ if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]
+ then
+ return
+ else
+ exit
+ fi
+}
+
+checkenv
+docker-compose run --rm -w /code/app web /code/app/manage.py "$@"
diff --git a/django-app/bin/dcp-exec.sh b/django-app/bin/dcp-exec.sh
new file mode 100755
index 0000000..0454a16
--- /dev/null
+++ b/django-app/bin/dcp-exec.sh
@@ -0,0 +1,5 @@
+#! /bin/bash
+
+# proxy to execute shell commands in running web container
+
+docker-compose exec web "$@"
diff --git a/django-app/bin/dcp-run-tests.sh b/django-app/bin/dcp-run-tests.sh
new file mode 100755
index 0000000..0f97f7e
--- /dev/null
+++ b/django-app/bin/dcp-run-tests.sh
@@ -0,0 +1,5 @@
+#! /bin/bash
+
+# proxy to execute pytest in web container
+
+docker-compose run --rm -w /code/app web pytest -m "not integration" --cov=. --verbose
diff --git a/django-app/bin/dcp-run.sh b/django-app/bin/dcp-run.sh
new file mode 100755
index 0000000..b087390
--- /dev/null
+++ b/django-app/bin/dcp-run.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+# proxy to run shell commands in web container.
+# `run --rm` starts and runs container, then removes itself to cleanup
+
+docker-compose run --rm web "$@"
diff --git a/django-app/bin/wait-for-it.sh b/django-app/bin/wait-for-it.sh
new file mode 100755
index 0000000..d990e0d
--- /dev/null
+++ b/django-app/bin/wait-for-it.sh
@@ -0,0 +1,182 @@
+#!/usr/bin/env bash
+# Use this script to test if a given TCP host/port are available
+
+WAITFORIT_cmdname=${0##*/}
+
+echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
+
+usage()
+{
+ cat << USAGE >&2
+Usage:
+ $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
+ -h HOST | --host=HOST Host or IP under test
+ -p PORT | --port=PORT TCP port under test
+ Alternatively, you specify the host and port as host:port
+ -s | --strict Only execute subcommand if the test succeeds
+ -q | --quiet Don't output any status messages
+ -t TIMEOUT | --timeout=TIMEOUT
+ Timeout in seconds, zero for no timeout
+ -- COMMAND ARGS Execute command with args after the test finishes
+USAGE
+ exit 1
+}
+
+wait_for()
+{
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ else
+ echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
+ fi
+ WAITFORIT_start_ts=$(date +%s)
+ while :
+ do
+ if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
+ nc -z $WAITFORIT_HOST $WAITFORIT_PORT
+ WAITFORIT_result=$?
+ else
+ (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
+ WAITFORIT_result=$?
+ fi
+ if [[ $WAITFORIT_result -eq 0 ]]; then
+ WAITFORIT_end_ts=$(date +%s)
+ echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
+ break
+ fi
+ sleep 1
+ done
+ return $WAITFORIT_result
+}
+
+wait_for_wrapper()
+{
+ # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
+ if [[ $WAITFORIT_QUIET -eq 1 ]]; then
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ else
+ timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
+ fi
+ WAITFORIT_PID=$!
+ trap "kill -INT -$WAITFORIT_PID" INT
+ wait $WAITFORIT_PID
+ WAITFORIT_RESULT=$?
+ if [[ $WAITFORIT_RESULT -ne 0 ]]; then
+ echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
+ fi
+ return $WAITFORIT_RESULT
+}
+
+# process arguments
+while [[ $# -gt 0 ]]
+do
+ case "$1" in
+ *:* )
+ WAITFORIT_hostport=(${1//:/ })
+ WAITFORIT_HOST=${WAITFORIT_hostport[0]}
+ WAITFORIT_PORT=${WAITFORIT_hostport[1]}
+ shift 1
+ ;;
+ --child)
+ WAITFORIT_CHILD=1
+ shift 1
+ ;;
+ -q | --quiet)
+ WAITFORIT_QUIET=1
+ shift 1
+ ;;
+ -s | --strict)
+ WAITFORIT_STRICT=1
+ shift 1
+ ;;
+ -h)
+ WAITFORIT_HOST="$2"
+ if [[ $WAITFORIT_HOST == "" ]]; then break; fi
+ shift 2
+ ;;
+ --host=*)
+ WAITFORIT_HOST="${1#*=}"
+ shift 1
+ ;;
+ -p)
+ WAITFORIT_PORT="$2"
+ if [[ $WAITFORIT_PORT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --port=*)
+ WAITFORIT_PORT="${1#*=}"
+ shift 1
+ ;;
+ -t)
+ WAITFORIT_TIMEOUT="$2"
+ if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
+ shift 2
+ ;;
+ --timeout=*)
+ WAITFORIT_TIMEOUT="${1#*=}"
+ shift 1
+ ;;
+ --)
+ shift
+ WAITFORIT_CLI=("$@")
+ break
+ ;;
+ --help)
+ usage
+ ;;
+ *)
+ echoerr "Unknown argument: $1"
+ usage
+ ;;
+ esac
+done
+
+if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
+ echoerr "Error: you need to provide a host and port to test."
+ usage
+fi
+
+WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
+WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
+WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
+WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
+
+# Check to see if timeout is from busybox?
+WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
+WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
+
+WAITFORIT_BUSYTIMEFLAG=""
+if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
+ WAITFORIT_ISBUSY=1
+ # Check if busybox timeout uses -t flag
+ # (recent Alpine versions don't support -t anymore)
+ if timeout &>/dev/stdout | grep -q -e '-t '; then
+ WAITFORIT_BUSYTIMEFLAG="-t"
+ fi
+else
+ WAITFORIT_ISBUSY=0
+fi
+
+if [[ $WAITFORIT_CHILD -gt 0 ]]; then
+ wait_for
+ WAITFORIT_RESULT=$?
+ exit $WAITFORIT_RESULT
+else
+ if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
+ wait_for_wrapper
+ WAITFORIT_RESULT=$?
+ else
+ wait_for
+ WAITFORIT_RESULT=$?
+ fi
+fi
+
+if [[ $WAITFORIT_CLI != "" ]]; then
+ if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
+ echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
+ exit $WAITFORIT_RESULT
+ fi
+ exec "${WAITFORIT_CLI[@]}"
+else
+ exit $WAITFORIT_RESULT
+fi
diff --git a/django-app/docker-compose.yml b/django-app/docker-compose.yml
new file mode 100644
index 0000000..74a3c50
--- /dev/null
+++ b/django-app/docker-compose.yml
@@ -0,0 +1,39 @@
+version: '3.8'
+
+volumes:
+ pgbd_postgres:
+ external: true
+
+services:
+ db:
+ image: postgres:15.4
+ env_file:
+ - ./.env
+ ports:
+ - "5432:5432"
+ volumes:
+ - pgbd_postgres:/var/lib/postgresql/data/
+ web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ env_file:
+ - ./.env
+ environment:
+ - POSTGRES_HOST=db
+ - STATIC_ROOT=/code/app/static/
+ - MEDIA_ROOT=/code/app/media/
+ command:
+ - '/code/bin/wait-for-it.sh'
+ - 'db:5432'
+ - '--'
+ - 'python'
+ - '/code/app/manage.py'
+ - 'runserver'
+ - '0.0.0.0:8000'
+ volumes:
+ - .:/code
+ ports:
+ - "8000:8000"
+ depends_on:
+ - db
diff --git a/django-app/justfile b/django-app/justfile
new file mode 100644
index 0000000..a7c3483
--- /dev/null
+++ b/django-app/justfile
@@ -0,0 +1,48 @@
+default:
+ @just --list
+
+set dotenv-load
+set fallback
+
+default_port := "8000"
+
+# django admin
+dj +commands:
+ cd app && DJANGO_SETTINGS_MODULE=app.settings \
+ ../.venv/bin/python manage.py {{commands}}
+
+# run dev server
+run port=default_port:
+ cd app && DJANGO_SETTINGS_MODULE=app.settings \
+ ../.venv/bin/python manage.py runserver {{port}}
+
+# test
+test:
+ cd app && DJANGO_SETTINGS_MODULE=app.settings \
+ ../.venv/bin/pytest -m "not integration" --cov=.
+
+# generate a secret key for django
+generate-secret-key:
+ cd app && DJANGO_SETTINGS_MODULE=app.settings \
+ ../.venv/bin/python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
+
+
+##################
+# Docker Compose #
+##################
+
+# django-admin via dcp container
+dcp-django-admin +commands:
+ docker-compose run --rm -w /code/app web /code/app/manage.py {{commands}}
+
+# bring up web app container
+dcp-run:
+ docker-compose up web
+
+# test in dcp container
+dcp-test:
+ docker-compose run --rm -w /code/app web pytest -m "not integration" --cov=. --verbose
+
+# generate a secret key using dcp container
+dcp-generate-secret-key:
+ docker-compose run --rm -w /code/app web python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
diff --git a/django-app/pytest.ini b/django-app/pytest.ini
new file mode 100644
index 0000000..d4b83e6
--- /dev/null
+++ b/django-app/pytest.ini
@@ -0,0 +1,12 @@
+[pytest]
+DJANGO_SETTINGS_MODULE = app.test_settings
+# -- recommended but optional:
+python_files = tests.py test_*.py *_test.py *_tests.py
+
+; dynamodb_host = '0.0.0.0'
+; dynamodb_port = '9000'
+
+addopts = --reuse-db
+
+markers =
+ integration
diff --git a/django-app/utils/create-docker-volumes.sh b/django-app/utils/create-docker-volumes.sh
new file mode 100755
index 0000000..b237c97
--- /dev/null
+++ b/django-app/utils/create-docker-volumes.sh
@@ -0,0 +1,4 @@
+#! /bin/bash
+
+# creates named volume
+docker volume create --name=pgbd_postgres
diff --git a/django-app/utils/dump-data.sh b/django-app/utils/dump-data.sh
new file mode 100755
index 0000000..e5eeeb8
--- /dev/null
+++ b/django-app/utils/dump-data.sh
@@ -0,0 +1,10 @@
+#! /bin/bash
+
+# script that dumps data from postgres
+
+docker-compose run --rm -w /code/app web /code/app/manage.py dumpdata \
+ --natural-primary \
+ --natural-foreign \
+ --exclude=admin.logentry \
+ --exclude=sessions.session \
+ --indent 4
diff --git a/django-app/utils/reload-docker-db.sh b/django-app/utils/reload-docker-db.sh
new file mode 100755
index 0000000..b084f11
--- /dev/null
+++ b/django-app/utils/reload-docker-db.sh
@@ -0,0 +1,107 @@
+#! /bin/bash
+
+data_to_load="dev_data.json"
+
+function usage() {
+ echo "Use this script to recreate the Docker volume and container for maya_postgres."
+ echo "It also runs migrations and loads initial data according to \`--data\`,"
+ echo "or \`dev_data.json\` by default if no \`--data\` parameter specified."
+ echo ""
+ echo "Usage:"
+ echo "./utils/reload-docker-db.sh --help"
+ echo "./utils/reload-docker-db.sh"
+ echo "./utils/reload-docker-db.sh --data=demo_data.json"
+ echo ""
+}
+
+function checkenv() {
+ ##############################################################
+ # check user's confidence if we are not using local database #
+ ##############################################################
+
+ # get db host envar from docker container
+ DB_HOST_ENVAR=$(docker-compose run --rm -w /code/app web env | grep POSTGRES_HOST)
+ DB_HOST=$(cut -d "=" -f2 <<< "$DB_HOST_ENVAR")
+ # bashism to trim newline
+ DB_HOST=${DB_HOST//[$'\t\r\n']}
+ if [ "$DB_HOST" != 'db' ] && [ "$DB_HOST" != '0.0.0.0' ] && [ "$DB_HOST" != 'localhost' ]
+ then
+ echo "You are trying to reload the database at ${DB_HOST}!"
+ checkconfidence
+ else
+ echo "Reloading database at $DB_HOST"
+ fi
+}
+
+function checkconfidence() {
+ read -r -p "Are you sure you want to continue? [y/N] " response
+ if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]
+ then
+ return
+ else
+ exit
+ fi
+}
+
+function recreate() {
+ # make sure db container is stopped
+ docker-compose stop db
+
+ # deletes postgres docker container
+ docker-compose rm -f db
+
+ # deletes postgres volume
+ docker volume rm pgbd_postgres
+
+ # recreates named volume
+ docker volume create --name=pgbd_postgres
+
+ # bring container back up and sleep for 5 seconds to ensure db is up
+ docker-compose up -d db
+ sleep 5
+}
+
+function migrate() {
+ # run migrations
+ docker-compose run --rm -w /code/app web /code/app/manage.py migrate
+}
+
+function loaddata() {
+ # load data from fixture
+ docker-compose run --rm -w /code/app web /code/app/manage.py loaddata $data_to_load
+}
+
+while [ "$1" != "" ]; do
+ PARAM=$(echo $1 | awk -F= '{print $1}')
+ VALUE=$(echo $1 | awk -F= '{print $2}')
+ case $PARAM in
+ -h | --help)
+ usage
+ exit
+ ;;
+ --data)
+ data_to_load=$VALUE
+ ;;
+ *)
+ echo "ERROR: unknown parameter \"$PARAM\""
+ usage
+ exit 1
+ ;;
+ esac
+ shift
+done
+
+
+checkenv
+echo "starting script ..."
+echo "Reloading $DB_HOST w/ $data_to_load ..."
+echo ""
+
+echo "recreating volume and container ..."
+recreate
+
+echo "running migrations ..."
+migrate
+
+echo "loading data from ${data_to_load} ..."
+loaddata
diff --git a/docker-compose.yaml b/docker-compose.yaml
index d23bc32..1c2540b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -2,20 +2,21 @@ services:
palguybuddydude:
depends_on:
- palworld-rcon-buddy
- build: .
+ restart: unless-stopped
+ build: ./palguybuddydude/.
container_name: palguybuddydude
env_file:
- - ./.env
+ - palguybuddydude/.env
palworld-rcon-buddy:
- image: "valamidev/palworld-rcon-buddy:latest"
- container_name: palworld-rcon-buddy
- environment:
- PALWORLD_RCON_PORT: "25575"
- INFO_CACHE_DURATION_MS: 5000 # By Default /info end-point is cached for 5 seconds
- BEARER_TOKEN: "" # we're not using an endpoint that requires auth, so doesn't matter right now
- PORT: 3000 # RCON-BUDDY port
- env_file:
- - ./.env
- ports:
- - "3000:3000"
+ restart: unless-stopped
+ image: "valamidev/palworld-rcon-buddy:latest"
+ container_name: palworld-rcon-buddy
+ environment:
+ INFO_CACHE_DURATION_MS: 5000 # By Default /info end-point is cached for 5 seconds
+ BEARER_TOKEN: "" # we're not using an endpoint that requires auth, so doesn't matter right now
+ PORT: 3000 # RCON-BUDDY port
+ env_file:
+ - palguybuddydude/.env
+ ports:
+ - "3000:3000"
diff --git a/.env.example b/palguybuddydude/.env.example
similarity index 75%
rename from .env.example
rename to palguybuddydude/.env.example
index c82cc79..21b0f69 100644
--- a/.env.example
+++ b/palguybuddydude/.env.example
@@ -2,7 +2,8 @@
RCON_BUDDY_HOST=172.17.0.1:3000
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234/abcd1234
-# This is how you can refference the palworld container
+# This is how you can reference the palworld container
PALWORLD_SERVER_IP_ADDRESS=127.0.0.1
# RCON Password
PALWORLD_RCON_PASSWORD=yourrconpassword
+PALWORLD_RCON_PORT=25575
diff --git a/Dockerfile b/palguybuddydude/Dockerfile
similarity index 92%
rename from Dockerfile
rename to palguybuddydude/Dockerfile
index a10e12f..491d88a 100644
--- a/Dockerfile
+++ b/palguybuddydude/Dockerfile
@@ -6,7 +6,7 @@ COPY go.mod go.sum ./
RUN go mod download
-COPY . .
+COPY .. .
RUN go build main.go
diff --git a/go.mod b/palguybuddydude/go.mod
similarity index 100%
rename from go.mod
rename to palguybuddydude/go.mod
diff --git a/palguybuddydude/go.sum b/palguybuddydude/go.sum
new file mode 100644
index 0000000..e69de29
diff --git a/justfile b/palguybuddydude/justfile
similarity index 86%
rename from justfile
rename to palguybuddydude/justfile
index 530d047..de0648a 100644
--- a/justfile
+++ b/palguybuddydude/justfile
@@ -2,6 +2,7 @@ default:
@just --list
set dotenv-load
+set fallback
# run go binary
run:
@@ -19,4 +20,4 @@ refresh:
docker-compose up -d
logs:
- docker-compose logs -f
\ No newline at end of file
+ docker-compose logs -f
diff --git a/main.go b/palguybuddydude/main.go
similarity index 98%
rename from main.go
rename to palguybuddydude/main.go
index 5aee816..f17fe9b 100644
--- a/main.go
+++ b/palguybuddydude/main.go
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"log"
"net/http"
"os"
@@ -31,7 +31,7 @@ func fetchServerInfo(apiURL string) (ServerInfo, error) {
}
defer resp.Body.Close()
- body, err := ioutil.ReadAll(resp.Body)
+ body, err := io.ReadAll(resp.Body)
if err != nil {
return ServerInfo{}, err
}