mirror of
https://github.com/DarkflameUniverse/NexusDashboard.git
synced 2025-04-25 07:46:20 +00:00
Compare commits
102 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
823ec2008f | ||
![]() |
f3e2254330 | ||
![]() |
d6b0a91e4d | ||
![]() |
d698e650ad | ||
![]() |
cde585fad8 | ||
![]() |
8b70f259c0 | ||
![]() |
de50bc7278 | ||
![]() |
2e4bd04d09 | ||
![]() |
ccc793a129 | ||
![]() |
69823be5c8 | ||
![]() |
3027534b16 | ||
![]() |
09096fe1c4 | ||
![]() |
9bfa55ac8e | ||
![]() |
1dee96c04f | ||
![]() |
d005b497e6 | ||
![]() |
259efc81fd | ||
![]() |
b9fc039a7e | ||
![]() |
abc8af89c5 | ||
![]() |
5ae2769ad2 | ||
![]() |
b7e48bb656 | ||
![]() |
7633053490 | ||
![]() |
e6c452d000 | ||
![]() |
13376d0c1f | ||
![]() |
bb63cfb8f5 | ||
![]() |
a10b8d7975 | ||
![]() |
1c9ee91b78 | ||
![]() |
5c2721cd65 | ||
![]() |
8ec3803786 | ||
![]() |
a578f8a53c | ||
![]() |
7ca62fe478 | ||
![]() |
01e304a041 | ||
![]() |
368c0819bd | ||
![]() |
ab8119c5b8 | ||
![]() |
c9ad415f13 | ||
![]() |
a7a68d2fe1 | ||
![]() |
b17928b050 | ||
![]() |
ee65f67fe3 | ||
![]() |
5d1b79334a | ||
![]() |
e726f59114 | ||
![]() |
8826a34ebc | ||
![]() |
a3d492df91 | ||
![]() |
4a58e963a5 | ||
![]() |
8012780eba | ||
![]() |
f403d7dcb0 | ||
![]() |
ceed592342 | ||
![]() |
bf7fb3d159 | ||
![]() |
ef55b8f9f2 | ||
![]() |
3d47b265c9 | ||
![]() |
3f7a382dbc | ||
![]() |
bc6bbdfaa7 | ||
![]() |
9cda62cef7 | ||
![]() |
99087eb30a | ||
![]() |
760936a01f | ||
![]() |
c96174fcbe | ||
![]() |
bec8233aad | ||
![]() |
ae46e6d382 | ||
![]() |
98c61bcaf1 | ||
![]() |
b8bd7c6cba | ||
![]() |
e44872e523 | ||
![]() |
535a07425b | ||
![]() |
785357475d | ||
![]() |
0fea032938 | ||
![]() |
3b5b478815 | ||
![]() |
708fbfb9db | ||
![]() |
53dda2fd8a | ||
![]() |
24fc6a0826 | ||
![]() |
5e59d3b43c | ||
![]() |
19f38b379e | ||
![]() |
4aff169967 | ||
![]() |
4d007d66ac | ||
![]() |
8652d6dc13 | ||
![]() |
ba157a3715 | ||
![]() |
e69d25594a | ||
![]() |
cc4adfcbfe | ||
![]() |
77acd7615a | ||
![]() |
f643f428ea | ||
![]() |
37af644078 | ||
![]() |
64ccb29972 | ||
![]() |
000a8c47bf | ||
![]() |
3fa8bd4651 | ||
![]() |
b87481e803 | ||
![]() |
eb7a820b54 | ||
![]() |
a9c53254f2 | ||
![]() |
d06bad4641 | ||
![]() |
1f2673d7fc | ||
![]() |
dce4466487 | ||
![]() |
f54e9bf9b4 | ||
![]() |
3a034de45a | ||
![]() |
a5f7024211 | ||
![]() |
a7419679d0 | ||
![]() |
e2ca21136e | ||
![]() |
d7333490d1 | ||
![]() |
21b6932f48 | ||
![]() |
6ff2caf039 | ||
![]() |
c1307af49c | ||
![]() |
b561bcb60d | ||
![]() |
34302006a9 | ||
![]() |
a5ea052027 | ||
![]() |
ad237b121b | ||
![]() |
4966d6b029 | ||
![]() |
6c3c0c4888 | ||
![]() |
ae0847aba9 |
54
.github/workflows/ci.yml
vendored
Normal file
54
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,7 +4,7 @@ resources.py
|
||||
__pycache__/
|
||||
venv/
|
||||
static/policy/
|
||||
app/static/site.css
|
||||
app/static/css/site.css
|
||||
app/static/.webassets-cache/**/*
|
||||
app/static/brickdb/*
|
||||
locale.json
|
||||
@ -17,3 +17,5 @@ property_files/*
|
||||
*.log
|
||||
app/settings.py
|
||||
*.exe
|
||||
*.csv
|
||||
*.sql
|
||||
|
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
FROM python:3.11-slim-bookworm
|
||||
|
||||
RUN apt update
|
||||
RUN apt -y install zip
|
||||
|
218
README.md
218
README.md
@ -65,28 +65,30 @@
|
||||
* Reports how much currency that characters posses
|
||||
* U-Score:
|
||||
* Reports how much U-Score that characters posses
|
||||
* Analytics:
|
||||
* Provide reporting to Developers to help better solve issues
|
||||
* Disabled by default. Set `ALLOW_ANALYTICS` to true to enable.
|
||||
|
||||
# Deployment
|
||||
|
||||
> **NOTE: This tutorial assumes you have a working DLU server instance and**
|
||||
> **some knowledge of Linux**
|
||||
> **some knowledge of command line interfaces on your chosen platform**
|
||||
|
||||
|
||||
**It is highly recommended to setup a reverse proxy via Nginx or some other tool and use SSL to secure your Nexus Dashboard instance if you are going to be opening it up to any non-LANs**
|
||||
* [How to setup Nginx](https://www.digitalocean.com/community/tutorials/how-to-configure-nginx-as-a-reverse-proxy-on-ubuntu-22-04)
|
||||
* [How to use certbot for SSL](https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-22-04)
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
|
||||
docker run -d \
|
||||
-e APP_SECRET_KEY='<secret_key>' \
|
||||
-e APP_DATABASE_URI='mysql+pymysql://<username>:<password>@<host>:<port>/<database>' \
|
||||
# you can include other optional Environment Variables from below like this
|
||||
-e REQUIRE_PLAY_KEY=True
|
||||
-p 8000:8000/tcp
|
||||
-v /path/to/unpacked/client:/app/luclient:rw \
|
||||
-e REQUIRE_PLAY_KEY=True \
|
||||
-p 8000:8000/tcp \
|
||||
-v /path/to/logs:/logs:rw /
|
||||
-v /path/to/unpacked/client:/app/luclient:ro \
|
||||
-v /path/to/cachedir:/app/cache:rw \
|
||||
aronwk/nexus-dashboard:latest
|
||||
|
||||
ghcr.io/darkflameuniverse/nexusdashboard:latest
|
||||
```
|
||||
|
||||
* `/app/luclient` must be mapped to the location of an unpacked client
|
||||
@ -96,43 +98,48 @@ docker run -d \
|
||||
|
||||
### Environmental Variables
|
||||
|
||||
Please Reference `app/settings_exmaple.py` to see all the variables
|
||||
Please Reference `app/settings_example.py` to see all the variables
|
||||
|
||||
* Required:
|
||||
* APP_SECRET_KEY (Must be provided)
|
||||
* APP_DATABASE_URI (Must be provided)
|
||||
* Everything else is optional and has defaults
|
||||
|
||||
## Manual
|
||||
## Manual Linux Installation
|
||||
|
||||
Thanks to [HailStorm32](https://github.com/HailStorm32) for this manual install guide!
|
||||
|
||||
### Setting Up The Environment
|
||||
First you will want to install the following packages by executing the following commands
|
||||
First you will want to install the following packages by executing the following commands presuming you are on a Debian based system.
|
||||
|
||||
`sudo apt-get update`
|
||||
|
||||
`sudo apt-get install -y python3 python3-pip sqlite3 git unzip libmagickwand-dev`
|
||||
|
||||
> *Note: If you are having issues with installing `sqlite3`, change it to `sqlite`*
|
||||
|
||||
<br>
|
||||
Next we will clone the repository. You can clone it anywhere, but for the purpose of this tutorial, we will be cloning it to the home directory.
|
||||
Next you will want to clone the repository. You can clone it anywhere, but for the purpose of this tutorial, we will be cloning it to the home directory.'
|
||||
<br></br>
|
||||
|
||||
`cd` *make sure you are in the home directory*
|
||||
Run `cd ~` to ensure that you are currently in the home directory then run the following command to clone the repository into our home directory
|
||||
`git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
|
||||
|
||||
You should now have a directory called `NexusDashboard`
|
||||
You should now have a directory called `NexusDashboard` present in your home directory
|
||||
|
||||
### Setting up
|
||||
|
||||
Rename the example settings file
|
||||
`cp ~/NexusDashboard/app/settings_example.py ~/NexusDashboard/app/settings.py`
|
||||
|
||||
Now let's open the settings file we just created and configure some of the settings
|
||||
`vim ~/NexusDashboard/app/settings.py`
|
||||
>*Feel free to use any text editor you are more comfortable with instead of vim*
|
||||
Now let's open the settings file we just created and configure some of the settings with nano as it is a simple text editor that is easy to use
|
||||
`nano ~/NexusDashboard/app/settings.py`
|
||||
>*Obviously you can replace this with a text editor of your choice, nano is just the most simple to use out of the ones available by default on most Linux distros*
|
||||
|
||||
<br>
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial I will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like
|
||||
>*Note: Enabling the email option will require further setup that is outside the scope of this tutorial*
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial we will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like to fit your needs.
|
||||
|
||||
>*Note: There are options in here that are related to email registration and password recovery among other features however those require extra setup not covered by this tutorial*
|
||||
|
||||
The two important settings to configure are `APP_SECRET_KEY` and `APP_DATABASE_URI`
|
||||
|
||||
@ -162,65 +169,166 @@ Once you are done making the changes, save and close the file
|
||||
|
||||
We will need the following folders from the client
|
||||
```
|
||||
locale (all of the files inside)
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
|_BrickModels
|
||||
|_brickprimitives
|
||||
|_textures
|
||||
|_ui
|
||||
|_brickdb.zip
|
||||
├───BrickModels
|
||||
├───brickprimitives
|
||||
├───textures
|
||||
├───ui
|
||||
├───brickdb.zip
|
||||
```
|
||||
Put the two folders in `~/NexusDashboard/app/luclient`
|
||||
|
||||
Unzip the `brickdb.zip` in place
|
||||
`unzip brickdb.zip`
|
||||
|
||||
Remove the `.zip` after you have unzipped it
|
||||
Remove the `.zip` file after you have unzipped it, you can do that with
|
||||
`rm brickdb.zip`
|
||||
|
||||
In the `luclient` directory you should now have a file structure that looks like this
|
||||
```
|
||||
local
|
||||
|_locale.xml
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
|_BrickModels
|
||||
|_...
|
||||
|_brickprimitives
|
||||
|_...
|
||||
|_textures
|
||||
|_...
|
||||
|_ui
|
||||
|_...
|
||||
|_Assemblies
|
||||
|_...
|
||||
|_Primitives
|
||||
|_...
|
||||
|_Materials.xml
|
||||
|_info.xml
|
||||
├───BrickModels
|
||||
│ └─── ...
|
||||
├───brickprimitives
|
||||
│ └─── ...
|
||||
├───textures
|
||||
│ └─── ...
|
||||
├───ui
|
||||
│ └─── ...
|
||||
├───Assemblies
|
||||
│ └─── ...
|
||||
├───Primitives
|
||||
│ └─── ...
|
||||
├───Materials.xml
|
||||
└───info.xml
|
||||
```
|
||||
|
||||
We will also need to copy the `CDServer.sqlite` database file from the server to the `~/NexusDashboard/app/luclient/res` folder
|
||||
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`
|
||||
`mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite`
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`, this can be done with the following command
|
||||
```bash
|
||||
mv ~/NexusDashboard/app/luclient/res/CDServer.sqlite ~/NexusDashboard/app/luclient/res/cdclient.sqlite
|
||||
```
|
||||
|
||||
|
||||
##### Remaining Setup
|
||||
Run the following commands one at a time
|
||||
To finish this, we will need to install the python dependencies and run the database migrations, simply run the following commands one at a time
|
||||
```bash
|
||||
cd ~/NexusDashboard
|
||||
pip install -r requirements.txt
|
||||
pip install gunicorn
|
||||
flask db upgrade
|
||||
```
|
||||
##### Running the site
|
||||
Once all of the above is complete, you can run the site with the command
|
||||
`gunicorn -b :8000 -w 4 wsgi:app`
|
||||
|
||||
`cd ~/NexusDashboard`
|
||||
`pip install -r requirements.txt`
|
||||
`pip install gunicorn`
|
||||
`flask db upgrade`
|
||||
## Manual Windows Setup
|
||||
|
||||
While a lot of the setup on Windows is the same a lot of it can be completed with GUI interfaces and requires installing things from websites instead of the command line.
|
||||
|
||||
### Setting Up The Environment
|
||||
You need to install the following prerequisites:
|
||||
|
||||
* [Python 3.8](https://www.python.org/downloads/release/python-380/)
|
||||
* [Git](https://git-scm.com/downloads)
|
||||
* [ImageMagick](https://docs.wand-py.org/en/latest/guide/install.html#install-imagemagick-on-windows)
|
||||
* [7-Zip](https://www.7-zip.org/download.html)
|
||||
|
||||
Next you will need to clone the repository. You can clone it anywhere, but for the purpose of this tutorial, you will want to clone it to your desktop just for simplicity, it can be moved after.
|
||||
|
||||
Open a command prompt and run `cd Desktop` (The command line should place you in your Home directory be default) to ensure that you are currently in the desktop directory then run the following command to clone the repository into our desktop directory
|
||||
|
||||
Run the following command to clone the repository `git clone https://github.com/DarkflameUniverse/NexusDashboard.git`
|
||||
|
||||
You should now have a directory called `NexusDashboard` present on your desktop.
|
||||
|
||||
### Setting up
|
||||
Now that we have the repository cloned you need to rename the example settings file, you can perform this manually in the GUI or you can use the command line, to do the latter run the following commands
|
||||
* `cd NexusDashboard\app`
|
||||
* `copy settings_example.py settings.py`
|
||||
|
||||
Now let's open the settings file we just created and configure some of the settings with the Windows default notepad.
|
||||
* `notepad settings.py`
|
||||
|
||||
Inside this file is where you can change certain settings like user registration, email support and other things. In this tutorial we will only be focusing on the bare minimum to get up and running, but feel free to adjust what you would like to fit your needs.
|
||||
|
||||
> *Note: There are options in here that are related to email registration and password recovery among other features however those require extra setup not covered by this tutorial*
|
||||
|
||||
The two important settings to configure are `APP_SECRET_KEY` and `APP_DATABASE_URI`
|
||||
|
||||
For `APP_SECRET_KEY` you can just fill in any random 32 character string and for `APP_DATABASE_URI` you will need to fill in a connection string to your database. The connection string will look similar to this. You will need to fill in your own information for the username, password, host, port and database name.
|
||||
```
|
||||
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
|
||||
```
|
||||
and the rest of the file can be left at the default values other than the `APP_SECRET_KEY` which you will need to fill in with random characters.
|
||||
|
||||
Once you are done making the changes, save and close the file
|
||||
|
||||
##### Client related files
|
||||
We will need the following folders from the client
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
├───brickprimitives
|
||||
├───textures
|
||||
├───ui
|
||||
└───brickdb.zip
|
||||
```
|
||||
Put the two folders in `Desktop/NexusDashboard/app/luclient`
|
||||
|
||||
Unzip the `brickdb.zip` in place using 7-Zip, you can do this by right clicking the file and selecting `7-Zip > Extract Here`.
|
||||
|
||||
After doing this you can remove the `.zip`, simply delete the file.
|
||||
|
||||
In the `luclient` directory you should now have a file structure that looks like this
|
||||
```
|
||||
locale
|
||||
└───locale.xml
|
||||
|
||||
res
|
||||
├───BrickModels
|
||||
│ └─── ...
|
||||
├───brickprimitives
|
||||
│ └─── ...
|
||||
├───textures
|
||||
│ └─── ...
|
||||
├───ui
|
||||
│ └─── ...
|
||||
├───Assemblies
|
||||
│ └─── ...
|
||||
├───Primitives
|
||||
│ └─── ...
|
||||
├───Materials.xml
|
||||
└───info.xml
|
||||
```
|
||||
|
||||
We will also need to copy the `CDServer.sqlite` database file from the server to the `Desktop/NexusDashboard/app/luclient/res` folder
|
||||
|
||||
Once the file is moved over, you will need to rename it to `cdclient.sqlite`, this can be done by right clicking the file and selecting `Rename` and then changing the name to `cdclient.sqlite`
|
||||
|
||||
##### Remaining Setup
|
||||
To finish this, we will need to install the python dependencies and run the database migrations, simply run the following commands one at a time in the root directory of the site, if you are not in the root directory you can run `cd Desktop/NexusDashboard` to get there (assuming you have opened a new terminal window)
|
||||
```bat
|
||||
pip install -r requirements.txt
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
##### Running the site
|
||||
You can run the site with
|
||||
`gunicorn -b :8000 -w 4 wsgi:app`
|
||||
Once all of the above is complete, you can run the site with the command
|
||||
`flask run` however bare in mind that this is a development version of the site, at the moment running a production version of the site on Windows is not supported.
|
||||
|
||||
# Development
|
||||
|
||||
Please use [Editor Config](https://editorconfig.org/)
|
||||
Please use [Editor Config](https://editorconfig.org/) to maintain a consistent coding style between different editors and different contributors.
|
||||
|
||||
* `flask run` to run local dev server
|
||||
* `python3 -m flask run` to run a local dev server
|
||||
|
127
app/__init__.py
127
app/__init__.py
@ -5,12 +5,12 @@ from flask_assets import Environment
|
||||
from webassets import Bundle
|
||||
import time
|
||||
from app.models import db, migrate, PlayKey
|
||||
from app.schemas import ma
|
||||
from app.forms import CustomUserManager
|
||||
from flask_user import user_registered, current_user, user_logged_in
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from flask_apscheduler import APScheduler
|
||||
from app.luclient import register_luclient_jinja_helpers
|
||||
import pathlib
|
||||
|
||||
from app.commands import (
|
||||
init_db,
|
||||
@ -18,7 +18,9 @@ from app.commands import (
|
||||
load_property,
|
||||
gen_image_cache,
|
||||
gen_model_cache,
|
||||
fix_clone_ids
|
||||
fix_clone_ids,
|
||||
remove_buffs,
|
||||
find_missing_commendation_items
|
||||
)
|
||||
from app.models import Account, AccountInvitation, AuditLog
|
||||
|
||||
@ -82,6 +84,11 @@ def create_app():
|
||||
if cdclient is not None:
|
||||
cdclient.close()
|
||||
|
||||
@app.template_filter()
|
||||
def numberFormat(value):
|
||||
return format(int(value), ',d')
|
||||
|
||||
|
||||
# add the commands to flask cli
|
||||
app.cli.add_command(init_db)
|
||||
app.cli.add_command(init_accounts)
|
||||
@ -89,6 +96,8 @@ def create_app():
|
||||
app.cli.add_command(gen_image_cache)
|
||||
app.cli.add_command(gen_model_cache)
|
||||
app.cli.add_command(fix_clone_ids)
|
||||
app.cli.add_command(remove_buffs)
|
||||
app.cli.add_command(find_missing_commendation_items)
|
||||
|
||||
register_logging(app)
|
||||
register_settings(app)
|
||||
@ -96,6 +105,19 @@ def create_app():
|
||||
register_blueprints(app)
|
||||
register_luclient_jinja_helpers(app)
|
||||
|
||||
# Extract the brickdb if it's not already extracted
|
||||
materials = pathlib.Path(f'{app.config["CACHE_LOCATION"]}Materials.xml')
|
||||
if not materials.is_file():
|
||||
# unzip the brickdb, and remove the import after
|
||||
from zipfile import ZipFile
|
||||
with ZipFile(f"{app.config['CLIENT_LOCATION']}res/brickdb.zip","r") as zip_ref:
|
||||
zip_ref.extractall(app.config["CACHE_LOCATION"])
|
||||
del ZipFile
|
||||
# copy over the brick primitives, and remove the import after
|
||||
from shutil import copytree
|
||||
copytree(f"{app.config['CLIENT_LOCATION']}res/brickprimitives", f"{app.config['CACHE_LOCATION']}brickprimitives")
|
||||
del copytree
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@ -107,8 +129,6 @@ def register_extensions(app):
|
||||
"""
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
ma.init_app(app)
|
||||
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
@ -120,7 +140,7 @@ def register_extensions(app):
|
||||
|
||||
assets = Environment(app)
|
||||
assets.url = app.static_url_path
|
||||
scss = Bundle('scss/site.scss', filters='libsass', output='site.css')
|
||||
scss = Bundle('scss/site.scss', filters='libsass', output='css/site.css')
|
||||
assets.register('scss_all', scss)
|
||||
|
||||
|
||||
@ -159,7 +179,7 @@ def register_blueprints(app):
|
||||
|
||||
def register_logging(app):
|
||||
# file logger
|
||||
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler = RotatingFileHandler('logs/nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
@ -183,19 +203,23 @@ def register_settings(app):
|
||||
# Load environment specific settings
|
||||
app.config['TESTING'] = False
|
||||
app.config['DEBUG'] = False
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
"pool_pre_ping": True,
|
||||
"pool_size": 10,
|
||||
"max_overflow": 2,
|
||||
"pool_recycle": 300,
|
||||
"pool_pre_ping": True,
|
||||
"pool_use_lifo": True
|
||||
}
|
||||
|
||||
# always pull these two from the env
|
||||
app.config['SECRET_KEY'] = os.getenv(
|
||||
'APP_SECRET_KEY',
|
||||
app.config['APP_SECRET_KEY']
|
||||
|
||||
)
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
|
||||
'APP_DATABASE_URI',
|
||||
app.config['APP_DATABASE_URI']
|
||||
)
|
||||
|
||||
# try to get overides, otherwise just use what we have already
|
||||
app.config['USER_ENABLE_REGISTER'] = os.getenv(
|
||||
'USER_ENABLE_REGISTER',
|
||||
app.config['USER_ENABLE_REGISTER']
|
||||
@ -220,25 +244,15 @@ def register_settings(app):
|
||||
'USER_REQUIRE_INVITATION',
|
||||
app.config['USER_REQUIRE_INVITATION']
|
||||
)
|
||||
app.config['ALLOW_ANALYTICS'] = os.getenv(
|
||||
'ALLOW_ANALYTICS',
|
||||
app.config['ALLOW_ANALYTICS']
|
||||
)
|
||||
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
|
||||
"pool_pre_ping": True,
|
||||
"pool_size": 10,
|
||||
"max_overflow": 2,
|
||||
"pool_recycle": 300,
|
||||
"pool_pre_ping": True,
|
||||
"pool_use_lifo": True
|
||||
}
|
||||
app.config['MAIL_SERVER'] = os.getenv(
|
||||
'MAIL_SERVER',
|
||||
app.config['MAIL_SERVER']
|
||||
)
|
||||
app.config['MAIL_PORT'] = os.getenv(
|
||||
'MAIL_USE_SSL',
|
||||
app.config['MAIL_PORT'] = int(
|
||||
os.getenv(
|
||||
'MAIL_PORT',
|
||||
app.config['MAIL_PORT']
|
||||
)
|
||||
)
|
||||
app.config['MAIL_USE_SSL'] = os.getenv(
|
||||
'MAIL_USE_SSL',
|
||||
@ -265,6 +279,71 @@ def register_settings(app):
|
||||
app.config['USER_EMAIL_SENDER_EMAIL']
|
||||
)
|
||||
|
||||
if "ENABLE_CHAR_XML_UPLOAD" not in app.config:
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD'] = False
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD'] = os.getenv(
|
||||
'ENABLE_CHAR_XML_UPLOAD',
|
||||
app.config['ENABLE_CHAR_XML_UPLOAD']
|
||||
)
|
||||
|
||||
if "CLIENT_LOCATION" not in app.config:
|
||||
app.config['CLIENT_LOCATION'] = 'app/luclient/'
|
||||
app.config['CLIENT_LOCATION'] = os.getenv(
|
||||
'CLIENT_LOCATION',
|
||||
app.config['CLIENT_LOCATION']
|
||||
)
|
||||
|
||||
if "CD_SQLITE_LOCATION" not in app.config:
|
||||
app.config['CD_SQLITE_LOCATION'] = 'app/luclient/res/'
|
||||
app.config['CD_SQLITE_LOCATION'] = os.getenv(
|
||||
'CD_SQLITE_LOCATION',
|
||||
app.config['CD_SQLITE_LOCATION']
|
||||
)
|
||||
|
||||
if "CACHE_LOCATION" not in app.config:
|
||||
app.config['CACHE_LOCATION'] = 'app/cache/'
|
||||
app.config['CACHE_LOCATION'] = os.getenv(
|
||||
'CACHE_LOCATION',
|
||||
app.config['CACHE_LOCATION']
|
||||
)
|
||||
|
||||
# Recaptcha settings
|
||||
if "RECAPTCHA_ENABLE" not in app.config:
|
||||
app.config['RECAPTCHA_ENABLE'] = False
|
||||
app.config['RECAPTCHA_ENABLE'] = os.getenv(
|
||||
'RECAPTCHA_ENABLE',
|
||||
app.config['RECAPTCHA_ENABLE']
|
||||
)
|
||||
if "RECAPTCHA_PUBLIC_KEY" not in app.config:
|
||||
app.config['RECAPTCHA_PUBLIC_KEY'] = ''
|
||||
app.config['RECAPTCHA_PUBLIC_KEY'] = os.getenv(
|
||||
'RECAPTCHA_PUBLIC_KEY',
|
||||
app.config['RECAPTCHA_PUBLIC_KEY']
|
||||
)
|
||||
if "RECAPTCHA_PRIVATE_KEY" not in app.config:
|
||||
app.config['RECAPTCHA_PRIVATE_KEY'] = ''
|
||||
app.config['RECAPTCHA_PRIVATE_KEY'] = os.getenv(
|
||||
'RECAPTCHA_PRIVATE_KEY',
|
||||
app.config['RECAPTCHA_PRIVATE_KEY']
|
||||
)
|
||||
# Optional
|
||||
if "RECAPTCHA_API_SERVER" in app.config:
|
||||
app.config['RECAPTCHA_API_SERVER'] = os.getenv(
|
||||
'RECAPTCHA_API_SERVER',
|
||||
app.config['RECAPTCHA_API_SERVER']
|
||||
)
|
||||
if "RECAPTCHA_PARAMETERS" in app.config:
|
||||
app.config['RECAPTCHA_PARAMETERS'] = os.getenv(
|
||||
'RECAPTCHA_PARAMETERS',
|
||||
app.config['RECAPTCHA_PARAMETERS']
|
||||
)
|
||||
if "RECAPTCHA_DATA_ATTRS" in app.config:
|
||||
app.config['RECAPTCHA_DATA_ATTRS'] = os.getenv(
|
||||
'RECAPTCHA_DATA_ATTRS',
|
||||
app.config['RECAPTCHA_DATA_ATTRS']
|
||||
)
|
||||
|
||||
|
||||
|
||||
def gm_level(gm_level):
|
||||
"""Decorator for handling permissions based on the user's GM Level
|
||||
|
@ -1,7 +1,9 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, current_app, flash
|
||||
from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import bcrypt
|
||||
import datetime
|
||||
import secrets
|
||||
from app.models import (
|
||||
Account,
|
||||
CharacterInfo,
|
||||
@ -14,17 +16,15 @@ from app.models import (
|
||||
AuditLog,
|
||||
BugReport,
|
||||
AccountInvitation,
|
||||
db
|
||||
db,
|
||||
Friends
|
||||
)
|
||||
from app.schemas import AccountSchema
|
||||
from app import gm_level, log_audit
|
||||
from app.forms import EditGMLevelForm, EditEmailForm
|
||||
from sqlalchemy import or_
|
||||
|
||||
accounts_blueprint = Blueprint('accounts', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
|
||||
|
||||
@accounts_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@ -150,10 +150,14 @@ def delete(id):
|
||||
message = f"Deleted Account ({account.id}){account.username}"
|
||||
chars = CharacterInfo.query.filter(CharacterInfo.account_id == id).all()
|
||||
for char in chars:
|
||||
activities = ActivityLog.query.filter(ActivityLog.character_id == char.id).all()
|
||||
activities = ActivityLog.query.filter(
|
||||
ActivityLog.character_id == char.id
|
||||
).all()
|
||||
for activity in activities:
|
||||
activity.delete()
|
||||
lb_entries = Leaderboard.query.filter(Leaderboard.character_id == char.id).all()
|
||||
lb_entries = Leaderboard.query.filter(
|
||||
Leaderboard.character_id == char.id
|
||||
).all()
|
||||
for lb_entry in lb_entries:
|
||||
lb_entry.delete()
|
||||
mails = Mail.query.filter(Mail.receiver_id == char.id).all()
|
||||
@ -161,21 +165,29 @@ def delete(id):
|
||||
mail.delete()
|
||||
props = Property.query.filter(Property.owner_id == char.id).all()
|
||||
for prop in props:
|
||||
prop_contents = PropertyContent.query.filter(PropertyContent.property_id == prop.id).all()
|
||||
prop_contents = PropertyContent.query.filter(
|
||||
PropertyContent.property_id == prop.id
|
||||
).all()
|
||||
for prop_content in prop_contents:
|
||||
if prop_content.lot == "14":
|
||||
UGC.query.filter(UGC.id == prop.ugc_id).first().delete()
|
||||
prop_content.delete()
|
||||
prop.delete()
|
||||
friends = Friends.query.filter(
|
||||
or_(Friends.player_id == char.id, Friends.friend_id == char.id)
|
||||
).all()
|
||||
for friend in friends:
|
||||
friend.delete()
|
||||
char.delete()
|
||||
# This is for GM stuff, it will be permnently delete logs
|
||||
bugs = BugReport.query.filter(BugReport.resolve_by_id == id).all()
|
||||
bugs = BugReport.query.filter(BugReport.resoleved_by_id == id).all()
|
||||
for bug in bugs:
|
||||
bug.delete()
|
||||
audits = AuditLog.query.filter(AuditLog.account_id == id).all()
|
||||
for audit in audits:
|
||||
audit.delete()
|
||||
invites = AccountInvitation.query.filter(AccountInvitation.invited_by_user_id == id).all()
|
||||
invites = AccountInvitation.query.filter(
|
||||
AccountInvitation.invited_by_user_id == id).all()
|
||||
for invite in invites:
|
||||
invite.delete()
|
||||
account.delete()
|
||||
@ -184,6 +196,27 @@ def delete(id):
|
||||
return redirect(url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/pass_reset/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def pass_reset(id):
|
||||
# get the account
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
# make a random pass of length 12 using secrets
|
||||
raw_pass = secrets.token_urlsafe(12)
|
||||
# generate the hash
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(str.encode(raw_pass), salt)
|
||||
# save the has
|
||||
account.password = hashed
|
||||
account.save()
|
||||
# display for the admin to get and log that the action was done
|
||||
flash(f"Set password for account {account.username} to {raw_pass}", "success")
|
||||
log_audit(f"Reset password for {account.username}")
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@ -219,6 +252,9 @@ def get():
|
||||
# Delete
|
||||
# </a>
|
||||
|
||||
if not current_app.config["USER_ENABLE_EMAIL"]:
|
||||
account["2"] = '''N/A'''
|
||||
|
||||
if account["4"]:
|
||||
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
@ -234,20 +270,11 @@ def get():
|
||||
else:
|
||||
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if current_app.config["USER_ENABLE_EMAIL"]:
|
||||
if account["8"]:
|
||||
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
else:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
if not current_app.config["USER_ENABLE_EMAIL"]:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-muted"></h2>'''
|
||||
elif account["8"]:
|
||||
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
else:
|
||||
# shift columns to fill in gap of 2
|
||||
account["2"] = account["3"]
|
||||
account["3"] = account["4"]
|
||||
account["4"] = account["5"]
|
||||
account["5"] = account["6"]
|
||||
account["6"] = account["7"]
|
||||
# remove last two columns
|
||||
del account["7"]
|
||||
del account["8"]
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
|
||||
return data
|
||||
|
@ -1,21 +1,19 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response, current_app
|
||||
from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import CharacterInfo, CharacterXML, Account, db
|
||||
from app.schemas import CharacterInfoSchema
|
||||
from app.forms import RescueForm
|
||||
from app.forms import RescueForm, CharXMLUploadForm
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import translate_from_locale
|
||||
import xmltodict
|
||||
import xml.etree.ElementTree as ET
|
||||
import json
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
character_blueprint = Blueprint('characters', __name__)
|
||||
|
||||
character_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@character_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@ -30,14 +28,20 @@ def approve_name(id, action):
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if action == "approve":
|
||||
log_audit(f"Approved ({character.id}){character.pending_name} from {character.name}")
|
||||
flash(
|
||||
f"Approved ({character.id}){character.pending_name} from {character.name}",
|
||||
"success"
|
||||
)
|
||||
if character.pending_name:
|
||||
character.name = character.pending_name
|
||||
character.pending_name = ""
|
||||
log_audit(f"Approved ({character.id}){character.pending_name} from {character.name}")
|
||||
flash(
|
||||
f"Approved ({character.id}){character.pending_name} from {character.name}",
|
||||
"success"
|
||||
)
|
||||
else:
|
||||
log_audit("Cannot make character name empty")
|
||||
flash(
|
||||
"Cannot make character name empty",
|
||||
"danger"
|
||||
)
|
||||
character.needs_rename = False
|
||||
|
||||
elif action == "rename":
|
||||
@ -72,7 +76,7 @@ def view(id):
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data,
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
@ -85,9 +89,10 @@ def view(id):
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
|
||||
return render_template(
|
||||
'character/view.html.j2',
|
||||
@ -95,6 +100,88 @@ def view(id):
|
||||
character_json=character_json
|
||||
)
|
||||
|
||||
@character_blueprint.route('/chardata/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def chardata(id):
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if character_data.account_id and character_data.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
# print json for reference
|
||||
# with open("errorchar.json", "a") as file:
|
||||
# file.write(
|
||||
# json.dumps(character_json, indent=4)
|
||||
# )
|
||||
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
|
||||
return render_template(
|
||||
'partials/_charxml.html.j2',
|
||||
character_data=character_data,
|
||||
character_json=character_json
|
||||
)
|
||||
|
||||
@character_blueprint.route('/inventory/<id>/<inventory_id>', methods=['GET'])
|
||||
@login_required
|
||||
def inventory(id, inventory_id):
|
||||
|
||||
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if character_data == {}:
|
||||
abort(404)
|
||||
return
|
||||
|
||||
if current_user.gm_level < 3:
|
||||
if character_data.account_id and character_data.account_id != current_user.id:
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt="),
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
|
||||
# print json for reference
|
||||
# with open("errorchar.json", "a") as file:
|
||||
# file.write(
|
||||
# json.dumps(character_json, indent=4)
|
||||
# )
|
||||
|
||||
# stupid fix for jinja parsing
|
||||
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
|
||||
# sort by items slot index
|
||||
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
|
||||
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list:
|
||||
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
|
||||
for inventory in character_json["obj"]["inv"]["holdings"]["in"]:
|
||||
if inventory["attr_t"] == inventory_id:
|
||||
return render_template(
|
||||
'partials/charxml/_inventory.html.j2',
|
||||
inventory=inventory
|
||||
)
|
||||
return "No Items in Inventory", 404
|
||||
|
||||
@character_blueprint.route('/view_xml/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@ -113,7 +200,7 @@ def view_xml(id):
|
||||
|
||||
character_xml = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data
|
||||
).first().xml_data.replace("\"stt=", "\" stt=")
|
||||
|
||||
response = make_response(character_xml)
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
@ -184,7 +271,7 @@ def rescue(id):
|
||||
CharacterXML.id == id
|
||||
).first()
|
||||
|
||||
character_xml = ET.XML(character_data.xml_data)
|
||||
character_xml = ET.XML(character_data.xml_data.replace("\"stt=", "\" stt="))
|
||||
for zone in character_xml.findall('.//r'):
|
||||
if int(zone.attrib["w"]) % 100 == 0:
|
||||
form.save_world.choices.append(
|
||||
@ -210,6 +297,30 @@ def rescue(id):
|
||||
return render_template("character/rescue.html.j2", form=form)
|
||||
|
||||
|
||||
@character_blueprint.route('/upload/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def upload(id):
|
||||
if not current_app.config["ENABLE_CHAR_XML_UPLOAD"]:
|
||||
flash("You must enable this setting to do this", "danger")
|
||||
return redirect(url_for('characters.view', id=id))
|
||||
|
||||
form = CharXMLUploadForm()
|
||||
|
||||
character_data = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first()
|
||||
|
||||
if form.validate_on_submit():
|
||||
character_data.xml_data = form.char_xml.data
|
||||
character_data.save()
|
||||
flash("You accept all consequences from these actions", "danger")
|
||||
log_audit(f"Updated {character_data.id}'s xml data")
|
||||
return redirect(url_for('characters.view', id=id))
|
||||
form.char_xml.data = minidom.parseString(character_data.xml_data).toprettyxml(indent=" ")
|
||||
return render_template("character/upload.html.j2", form=form)
|
||||
|
||||
|
||||
@character_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@ -228,7 +339,7 @@ def get(status):
|
||||
if status == "approved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name == "") & (CharacterInfo.needs_rename == False))
|
||||
elif status == "unapproved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") | (CharacterInfo.needs_rename == True))
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") & (CharacterInfo.needs_rename == False))
|
||||
else:
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account)
|
||||
|
||||
|
@ -4,8 +4,8 @@ import random
|
||||
import string
|
||||
import datetime
|
||||
from flask_user import current_app
|
||||
from app import db
|
||||
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
|
||||
from app import db, luclient
|
||||
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail, CharacterXML
|
||||
import pathlib
|
||||
import zlib
|
||||
from wand import image
|
||||
@ -15,7 +15,7 @@ from multiprocessing import Pool
|
||||
from functools import partial
|
||||
from sqlalchemy import func
|
||||
import time
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@click.command("init_db")
|
||||
@click.argument('drop_tables', nargs=1)
|
||||
@ -180,6 +180,20 @@ def load_property(zone, player):
|
||||
)
|
||||
new_prop_content.save()
|
||||
|
||||
@click.command("remove_buffs")
|
||||
@with_appcontext
|
||||
def remove_buffs():
|
||||
"""Clears all buff from all characters"""
|
||||
chars = CharacterXML.query.all()
|
||||
for char in chars:
|
||||
character_xml = ET.XML(char.xml_data.replace("\"stt=", "\" stt="))
|
||||
dest = character_xml.find(".//dest")
|
||||
if dest:
|
||||
buff = character_xml.find(".//buff")
|
||||
if buff:
|
||||
dest.remove(buff)
|
||||
char.xml_data = ET.tostring(character_xml)
|
||||
char.save()
|
||||
|
||||
@click.command("gen_image_cache")
|
||||
def gen_image_cache():
|
||||
@ -267,3 +281,41 @@ def find_or_create_account(name, email, password, gm_level=9):
|
||||
db.session.add(play_key)
|
||||
db.session.commit()
|
||||
return # account
|
||||
|
||||
@click.command("find_missing_commendation_items")
|
||||
@with_appcontext
|
||||
def find_missing_commendation_items():
|
||||
data = dict()
|
||||
lots = set()
|
||||
reward_items = luclient.query_cdclient("Select reward_item1 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item2 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item3 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
reward_items = luclient.query_cdclient("Select reward_item4 from Missions;")
|
||||
for reward_item in reward_items:
|
||||
lots.add(reward_item[0])
|
||||
lots.remove(0)
|
||||
lots.remove(-1)
|
||||
|
||||
for lot in lots:
|
||||
itemcompid = luclient.query_cdclient(
|
||||
"Select component_id from ComponentsRegistry where component_type = 11 and id = ?;",
|
||||
[lot],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
itemcomp = luclient.query_cdclient(
|
||||
"Select commendationLOT, commendationCost from ItemComponent where id = ?;",
|
||||
[itemcompid],
|
||||
one=True
|
||||
)
|
||||
if itemcomp[0] is None or itemcomp[1] is None:
|
||||
data[lot] = {"name": luclient.get_lot_name(lot)}
|
||||
print(data)
|
||||
|
||||
|
||||
|
82
app/forms.py
82
app/forms.py
@ -1,17 +1,15 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf import FlaskForm, Recaptcha, RecaptchaField
|
||||
from flask import current_app
|
||||
|
||||
from flask_user.forms import (
|
||||
unique_email_validator,
|
||||
password_validator,
|
||||
unique_username_validator
|
||||
LoginForm,
|
||||
RegisterForm
|
||||
)
|
||||
from flask_user import UserManager
|
||||
from wtforms.widgets import TextArea, NumberInput
|
||||
from wtforms import (
|
||||
StringField,
|
||||
HiddenField,
|
||||
PasswordField,
|
||||
BooleanField,
|
||||
SubmitField,
|
||||
validators,
|
||||
@ -24,67 +22,43 @@ from app.models import PlayKey
|
||||
|
||||
|
||||
def validate_play_key(form, field):
|
||||
"""Validates a field for a valid phone number
|
||||
"""Validates a field for a valid play kyey
|
||||
Args:
|
||||
form: REQUIRED, the field's parent form
|
||||
field: REQUIRED, the field with data
|
||||
Returns:
|
||||
None, raises ValidationError if failed
|
||||
"""
|
||||
# jank to get the fireign key that we need back into the field
|
||||
# jank to get the foreign key that we need back into the field
|
||||
if current_app.config["REQUIRE_PLAY_KEY"]:
|
||||
field.data = PlayKey.key_is_valid(key_string=field.data)
|
||||
return
|
||||
return True
|
||||
|
||||
class CustomRecaptcha(Recaptcha):
|
||||
def __call__(self, form, field):
|
||||
if not current_app.config.get("RECAPTCHA_ENABLE", False):
|
||||
return True
|
||||
return super(CustomRecaptcha, self).__call__(form, field)
|
||||
|
||||
|
||||
class CustomUserManager(UserManager):
|
||||
def customize(self, app):
|
||||
self.RegisterFormClass = CustomRegisterForm
|
||||
self.LoginFormClass = CustomLoginForm
|
||||
|
||||
|
||||
class CustomRegisterForm(FlaskForm):
|
||||
"""Registration form"""
|
||||
next = HiddenField()
|
||||
reg_next = HiddenField()
|
||||
|
||||
# Login Info
|
||||
email = StringField(
|
||||
'E-Mail',
|
||||
validators=[
|
||||
Optional(),
|
||||
validators.Email('Invalid email address'),
|
||||
unique_email_validator,
|
||||
]
|
||||
)
|
||||
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
unique_username_validator,
|
||||
]
|
||||
)
|
||||
|
||||
class CustomRegisterForm(RegisterForm):
|
||||
play_key_id = StringField(
|
||||
'Play Key',
|
||||
validators=[
|
||||
Optional(),
|
||||
validate_play_key,
|
||||
]
|
||||
validators=[validate_play_key]
|
||||
)
|
||||
recaptcha = RecaptchaField(
|
||||
validators=[CustomRecaptcha()]
|
||||
)
|
||||
|
||||
password = PasswordField('Password', validators=[
|
||||
DataRequired(),
|
||||
password_validator
|
||||
])
|
||||
retype_password = PasswordField('Retype Password', validators=[
|
||||
validators.EqualTo('password', message='Passwords did not match')
|
||||
])
|
||||
|
||||
invite_token = HiddenField('Token')
|
||||
|
||||
submit = SubmitField('Register')
|
||||
|
||||
class CustomLoginForm(LoginForm):
|
||||
recaptcha = RecaptchaField(
|
||||
validators=[CustomRecaptcha()]
|
||||
)
|
||||
|
||||
class CreatePlayKeyForm(FlaskForm):
|
||||
|
||||
@ -119,7 +93,7 @@ class EditPlayKeyForm(FlaskForm):
|
||||
|
||||
class EditGMLevelForm(FlaskForm):
|
||||
|
||||
email = IntegerField(
|
||||
gm_level = IntegerField(
|
||||
'GM Level',
|
||||
widget=NumberInput(min=0, max=9)
|
||||
)
|
||||
@ -209,3 +183,13 @@ class RejectPropertyForm(FlaskForm):
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class CharXMLUploadForm(FlaskForm):
|
||||
char_xml = StringField(
|
||||
'Paste minified charxml here:',
|
||||
widget=TextArea(),
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
@ -25,7 +25,7 @@ def command():
|
||||
@log_blueprint.route('/system')
|
||||
@gm_level(8)
|
||||
def system():
|
||||
with open('nexus_dashboard.log', 'r') as file:
|
||||
with open('logs/nexus_dashboard.log', 'r') as file:
|
||||
logs = '</br>'.join(file.read().split('\n')[-100:])
|
||||
return render_template('logs/system.html.j2', logs=logs)
|
||||
|
||||
|
@ -5,7 +5,8 @@ from flask import (
|
||||
redirect,
|
||||
url_for,
|
||||
make_response,
|
||||
abort
|
||||
abort,
|
||||
current_app
|
||||
)
|
||||
from flask_user import login_required
|
||||
from app.models import CharacterInfo
|
||||
@ -32,7 +33,7 @@ def get_dds_as_png(filename):
|
||||
cache = f'cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = 'app/luclient/res/'
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
|
||||
path = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
@ -41,7 +42,7 @@ def get_dds_as_png(filename):
|
||||
|
||||
with image.Image(filename=path) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename='app/cache/' + filename.split('.')[0] + '.png')
|
||||
img.save(filename=current_app.config["CACHE_LOCATION"] + filename.split('.')[0] + '.png')
|
||||
|
||||
return send_file(cache)
|
||||
|
||||
@ -52,7 +53,7 @@ def get_dds(filename):
|
||||
if filename.split('.')[-1] != 'dds':
|
||||
return 404
|
||||
|
||||
root = 'app/luclient/res/'
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
|
||||
dds = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
@ -65,12 +66,17 @@ def get_dds(filename):
|
||||
@luclient_blueprint.route('/get_icon_lot/<id>')
|
||||
@login_required
|
||||
def get_icon_lot(id):
|
||||
|
||||
if id is None:
|
||||
redirect(url_for('luclient.unknown'))
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[id],
|
||||
one=True
|
||||
)[0]
|
||||
)
|
||||
if render_component_id is not None:
|
||||
render_component_id = render_component_id[0]
|
||||
else:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient(
|
||||
@ -84,10 +90,10 @@ def get_icon_lot(id):
|
||||
else:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
cache = f'app/cache/{filename.split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = 'app/luclient/res/'
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
@ -111,10 +117,10 @@ def get_icon_iconid(id):
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
|
||||
cache = f'app/cache/{filename.split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = 'app/luclient/res/'
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
@ -132,14 +138,14 @@ def brick_list():
|
||||
brick_list = []
|
||||
if len(brick_list) == 0:
|
||||
suffixes = [".g", ".g1", ".g2", ".g3", ".xml"]
|
||||
res = pathlib.Path('app/luclient/res/')
|
||||
cache = pathlib.Path(f"{current_app.config['CACHE_LOCATION']}")
|
||||
# Load g files
|
||||
for path in res.rglob("*.*"):
|
||||
for path in cache.rglob("*.*"):
|
||||
if str(path.suffix) in suffixes:
|
||||
brick_list.append(
|
||||
{
|
||||
"type": "file",
|
||||
"name": str(path.as_posix()).replace("app/luclient/res/", "")
|
||||
"name": str(path.as_posix()).replace(f"{current_app.config['CACHE_LOCATION']}", "")
|
||||
}
|
||||
)
|
||||
response = make_response(json.dumps(brick_list))
|
||||
@ -151,7 +157,7 @@ def brick_list():
|
||||
@luclient_blueprint.route('/ldddb/<path:req_path>')
|
||||
def dir_listing(req_path):
|
||||
# Joining the base and the requested path
|
||||
rel_path = pathlib.Path(str(pathlib.Path(f'app/luclient/res/{req_path}').resolve()))
|
||||
rel_path = pathlib.Path(str(pathlib.Path(f"{current_app.config['CACHE_LOCATION']}/{req_path}").resolve()))
|
||||
# Return 404 if path doesn't exist
|
||||
if not rel_path.exists():
|
||||
return abort(404)
|
||||
@ -168,10 +174,10 @@ def dir_listing(req_path):
|
||||
def unknown():
|
||||
filename = "textures/ui/inventory/unknown.dds"
|
||||
|
||||
cache = f'app/cache/{filename.split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists(cache):
|
||||
root = 'app/luclient/res/'
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
try:
|
||||
pathlib.Path(os.path.dirname(cache)).resolve().mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
@ -191,7 +197,16 @@ def get_cdclient():
|
||||
"""
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
if cdclient is None:
|
||||
cdclient = g._database = sqlite3.connect('app/luclient/res/cdclient.sqlite')
|
||||
path = pathlib.Path(f"{current_app.config['CD_SQLITE_LOCATION']}cdclient.sqlite")
|
||||
if path.is_file():
|
||||
cdclient = g._database = sqlite3.connect(f"{current_app.config['CD_SQLITE_LOCATION']}cdclient.sqlite")
|
||||
return cdclient
|
||||
|
||||
path = pathlib.Path(f"{current_app.config['CD_SQLITE_LOCATION']}CDServer.sqlite")
|
||||
if path.is_file():
|
||||
cdclient = g._database = sqlite3.connect(f"{current_app.config['CD_SQLITE_LOCATION']}CDServer.sqlite")
|
||||
return cdclient
|
||||
|
||||
return cdclient
|
||||
|
||||
|
||||
@ -223,7 +238,7 @@ def translate_from_locale(trans_string):
|
||||
locale_data = ""
|
||||
|
||||
if not locale:
|
||||
locale_path = "app/luclient/locale/locale.xml"
|
||||
locale_path = f"{current_app.config['CLIENT_LOCATION']}locale/locale.xml"
|
||||
|
||||
with open(locale_path, 'r') as file:
|
||||
locale_data = file.read()
|
||||
@ -281,6 +296,7 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('parse_lzid')
|
||||
def parse_lzid(lzid):
|
||||
if not lzid: return [1000, 1000, 1000]
|
||||
return[
|
||||
(int(lzid) & ((1 << 16) - 1)),
|
||||
((int(lzid) >> 16) & ((1 << 16) - 1)),
|
||||
@ -308,7 +324,9 @@ def register_luclient_jinja_helpers(app):
|
||||
'select component_id from ComponentsRegistry where component_type = 11 and id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)[0]
|
||||
)
|
||||
if render_component_id:
|
||||
render_component_id = render_component_id[0]
|
||||
|
||||
rarity = query_cdclient(
|
||||
'select rarity from ItemComponent where id = ?',
|
||||
|
@ -69,8 +69,7 @@ def send():
|
||||
form.recipient.choices.append((character.id, character.name))
|
||||
|
||||
items = query_cdclient(
|
||||
'Select id, name, displayName from Objects where type = ?',
|
||||
["Loot"]
|
||||
'Select id, name, displayName from Objects where type = "Loot"'
|
||||
)
|
||||
|
||||
for item in items:
|
||||
|
24
app/main.py
24
app/main.py
@ -2,13 +2,12 @@ from flask import render_template, Blueprint, send_from_directory
|
||||
from flask_user import current_user, login_required
|
||||
|
||||
from app.models import Account, CharacterInfo, ActivityLog
|
||||
from app.schemas import AccountSchema, CharacterInfoSchema
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
main_blueprint = Blueprint('main', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
char_info_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@main_blueprint.route('/', methods=['GET'])
|
||||
def index():
|
||||
@ -28,12 +27,16 @@ def index():
|
||||
@login_required
|
||||
def about():
|
||||
"""About Page"""
|
||||
mods = Account.query.filter(Account.gm_level > 0).order_by(Account.gm_level.desc()).all()
|
||||
mods = Account.query.filter(Account.gm_level > 1).order_by(Account.gm_level.desc()).all()
|
||||
online = 0
|
||||
chars = CharacterInfo.query.all()
|
||||
users = []
|
||||
zones = {}
|
||||
twodaysago = time.mktime((datetime.datetime.now() - datetime.timedelta(days=2)).timetuple())
|
||||
chars = CharacterInfo.query.filter(CharacterInfo.last_login >= twodaysago).all()
|
||||
|
||||
for char in chars:
|
||||
last_log = ActivityLog.query.with_entities(
|
||||
ActivityLog.activity
|
||||
ActivityLog.activity, ActivityLog.map_id
|
||||
).filter(
|
||||
ActivityLog.character_id == char.id
|
||||
).order_by(ActivityLog.id.desc()).first()
|
||||
@ -41,8 +44,13 @@ def about():
|
||||
if last_log:
|
||||
if last_log[0] == 0:
|
||||
online += 1
|
||||
if current_user.gm_level >= 8: users.append([char.name, last_log[1]])
|
||||
if str(last_log[1]) not in zones:
|
||||
zones[str(last_log[1])] = 1
|
||||
else:
|
||||
zones[str(last_log[1])] += 1
|
||||
|
||||
return render_template('main/about.html.j2', mods=mods, online=online)
|
||||
return render_template('main/about.html.j2', mods=mods, online=online, users=users, zones=zones)
|
||||
|
||||
|
||||
@main_blueprint.route('/favicon.ico')
|
||||
|
@ -4,10 +4,9 @@ from flask_user import UserMixin
|
||||
from wtforms import ValidationError
|
||||
|
||||
import logging
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.exc import OperationalError, StatementError
|
||||
from sqlalchemy.types import JSON
|
||||
from time import sleep
|
||||
import random
|
||||
import string
|
||||
@ -15,7 +14,7 @@ import string
|
||||
|
||||
# retrying query to work around python trash collector
|
||||
# killing connections of other gunicorn workers
|
||||
class RetryingQuery(BaseQuery):
|
||||
class RetryingQuery(Query):
|
||||
__retry_count__ = 3
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -114,7 +113,7 @@ class PlayKey(db.Model):
|
||||
)
|
||||
db.session.add(new_key)
|
||||
db.session.commit()
|
||||
return key
|
||||
return key
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
@ -1018,7 +1017,7 @@ class Reports(db.Model):
|
||||
__tablename__ = 'reports'
|
||||
|
||||
data = db.Column(
|
||||
JSON(),
|
||||
mysql.MEDIUMBLOB(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
|
@ -11,14 +11,14 @@ play_keys_blueprint = Blueprint('play_keys', __name__)
|
||||
# Key creation page
|
||||
@play_keys_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def index():
|
||||
return render_template('play_keys/index.html.j2')
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/create/<count>/<uses>', methods=['GET'], defaults={'count': 1, 'uses': 1})
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def create(count=1, uses=1):
|
||||
key = PlayKey.create(count=count, uses=uses)
|
||||
log_audit(f"Created {count} Play Key(s) with {uses} uses!")
|
||||
@ -28,7 +28,7 @@ def create(count=1, uses=1):
|
||||
|
||||
@play_keys_blueprint.route('/create/bulk', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def bulk_create():
|
||||
form = CreatePlayKeyForm()
|
||||
if form.validate_on_submit():
|
||||
@ -42,7 +42,7 @@ def bulk_create():
|
||||
|
||||
@play_keys_blueprint.route('/delete/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def delete(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
# associated_accounts = Account.query.filter(Account.play_key_id==id).all()
|
||||
@ -54,7 +54,7 @@ def delete(id):
|
||||
|
||||
@play_keys_blueprint.route('/edit/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def edit(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
form = EditPlayKeyForm()
|
||||
@ -81,7 +81,7 @@ def edit(id):
|
||||
|
||||
@play_keys_blueprint.route('/view/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def view(id):
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
accounts = Account.query.filter(Account.play_key_id == id).all()
|
||||
@ -90,7 +90,7 @@ def view(id):
|
||||
|
||||
@play_keys_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def get():
|
||||
columns = [
|
||||
ColumnDT(PlayKey.id),
|
||||
|
@ -13,7 +13,6 @@ from flask_user import login_required, current_user
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail
|
||||
from app.schemas import PropertySchema
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import query_cdclient
|
||||
from app.forms import RejectPropertyForm
|
||||
@ -24,9 +23,6 @@ import pathlib
|
||||
|
||||
property_blueprint = Blueprint('properties', __name__)
|
||||
|
||||
property_schema = PropertySchema()
|
||||
|
||||
|
||||
@property_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@ -411,25 +407,37 @@ def download_model(id):
|
||||
|
||||
def ugc(content):
|
||||
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
|
||||
uncompressed_lxfml = zlib.decompress(ugc_data.lxfml)
|
||||
uncompressed_lxfml = decompress(ugc_data.lxfml)
|
||||
response = make_response(uncompressed_lxfml)
|
||||
return response, ugc_data.filename
|
||||
|
||||
def decompress(data):
|
||||
assert data[:5] == b"sd0\x01\xff"
|
||||
pos = 5
|
||||
out = b""
|
||||
while pos < len(data):
|
||||
length = int.from_bytes(data[pos:pos+4], "little")
|
||||
pos += 4
|
||||
out += zlib.decompress(data[pos:pos+length])
|
||||
pos += length
|
||||
return out
|
||||
|
||||
def prebuilt(content, file_format, lod):
|
||||
# translate LOT to component id
|
||||
# we need to get a type of 2 because reasons
|
||||
# we need to get a type of 2 for the render component to find the filename
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
[content.lot],
|
||||
one=True
|
||||
)[0]
|
||||
# find the asset from rendercomponent given the component id
|
||||
# find the asset from rendercomponent given the component id
|
||||
filename = query_cdclient(
|
||||
'select render_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
|
||||
# if we have a valie filename, coerce it
|
||||
if filename:
|
||||
filename = filename[0].split("\\\\")[-1].lower().split(".")[0]
|
||||
if "/" in filename:
|
||||
@ -437,25 +445,29 @@ def prebuilt(content, file_format, lod):
|
||||
else:
|
||||
return f"No filename for LOT {content.lot}"
|
||||
|
||||
lxfml = pathlib.Path(f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml')
|
||||
# if we just want the lxfml, fine t and return it
|
||||
lxfml = pathlib.Path(f'{current_app.config["CLIENT_LOCATION"]}res/BrickModels/{filename.split(".")[0]}.lxfml')
|
||||
if file_format == "lxfml":
|
||||
|
||||
with open(lxfml, 'r') as file:
|
||||
lxfml_data = file.read()
|
||||
response = make_response(lxfml_data)
|
||||
|
||||
# else we handle getting the files for lddviewer
|
||||
elif file_format in ["obj", "mtl"]:
|
||||
cache = pathlib.Path(f'app/cache/BrickModels/{filename}.lod{lod}.{file_format}')
|
||||
# check to see if the file exists
|
||||
cache = pathlib.Path(f'{current_app.config["CACHE_LOCATION"]}BrickModels/{filename}.lod{lod}.{file_format}')
|
||||
if not cache.is_file():
|
||||
# if not make it an store it for later
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
ldd.main(str(lxfml.as_posix()), str(cache.with_suffix("").as_posix()), lod) # convert to OBJ
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"ERROR on {cache}:\n {e}")
|
||||
|
||||
# then just read it
|
||||
with open(str(cache.as_posix()), 'r') as file:
|
||||
cache_data = file.read()
|
||||
|
||||
# and serve it
|
||||
response = make_response(cache_data)
|
||||
|
||||
else:
|
||||
|
@ -7,6 +7,7 @@ import math
|
||||
import struct
|
||||
import zipfile
|
||||
from xml.dom import minidom
|
||||
from flask import current_app
|
||||
|
||||
PRIMITIVEPATH = '/Primitives/'
|
||||
GEOMETRIEPATH = PRIMITIVEPATH
|
||||
@ -968,8 +969,8 @@ def main(lxf_filename, obj_filename, lod="2"):
|
||||
GEOMETRIEPATH = GEOMETRIEPATH + f"LOD{lod}/"
|
||||
converter = Converter()
|
||||
# print("Found DB folder. Will use this instead of db.lif!")
|
||||
setDBFolderVars(dbfolderlocation="app/luclient/res/", lod=lod)
|
||||
converter.LoadDBFolder(dbfolderlocation="app/luclient/res/")
|
||||
setDBFolderVars(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}", lod=lod)
|
||||
converter.LoadDBFolder(dbfolderlocation=f"{current_app.config['CACHE_LOCATION']}")
|
||||
converter.LoadScene(filename=lxf_filename)
|
||||
converter.Export(filename=obj_filename)
|
||||
|
||||
|
@ -4,9 +4,7 @@ from app.models import CharacterInfo, Account, CharacterXML, Reports
|
||||
from app.luclient import get_lot_name
|
||||
from app import gm_level, scheduler
|
||||
from sqlalchemy.orm import load_only
|
||||
import datetime
|
||||
import xmltodict
|
||||
import random
|
||||
import xmltodict, gzip, json, datetime
|
||||
|
||||
reports_blueprint = Blueprint('reports', __name__)
|
||||
|
||||
@ -32,7 +30,6 @@ def index():
|
||||
).filter(
|
||||
Reports.report_type == "uscore"
|
||||
).group_by(Reports.date).options(load_only(Reports.date)).all()
|
||||
|
||||
return render_template(
|
||||
'reports/index.html.j2',
|
||||
reports_items=reports_items,
|
||||
@ -46,6 +43,8 @@ def index():
|
||||
@gm_level(3)
|
||||
def items_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/items/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@ -64,25 +63,31 @@ def items_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for key in entry.data:
|
||||
items[key] = get_lot_name(key)
|
||||
# make it
|
||||
for key, value in items.items():
|
||||
data = []
|
||||
for entry in entries:
|
||||
if key in entry.data.keys():
|
||||
data.append(entry.data[key])
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + value.encode("utf-8").hex()[1:7]
|
||||
if max(data) > 10:
|
||||
datasets.append({
|
||||
"label": value,
|
||||
"data": data,
|
||||
"backgroundColor": color,
|
||||
"borderColor": color
|
||||
})
|
||||
if value:
|
||||
data = []
|
||||
for entry in entries:
|
||||
if key in entry.data.keys():
|
||||
if not isinstance(entry.data[key], int):
|
||||
data.append(entry.data[key]["item_count"])
|
||||
else:
|
||||
data.append(entry.data[key])
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + value.encode("utf-8").hex()[1:7]
|
||||
if max(data) > 10:
|
||||
datasets.append({
|
||||
"label": value,
|
||||
"data": data,
|
||||
"backgroundColor": color,
|
||||
"borderColor": color
|
||||
})
|
||||
|
||||
return render_template(
|
||||
'reports/graph.html.j2',
|
||||
@ -102,6 +107,8 @@ def items_graph(start, end):
|
||||
@gm_level(3)
|
||||
def currency_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/currency/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@ -119,6 +126,8 @@ def currency_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
@ -153,6 +162,8 @@ def currency_graph(start, end):
|
||||
@gm_level(3)
|
||||
def uscore_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
|
||||
data = gzip.decompress(data)
|
||||
data = json.loads(data.decode('utf-8'))
|
||||
return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@ -170,6 +181,8 @@ def uscore_graph(start, end):
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
entry.data = gzip.decompress(entry.data)
|
||||
entry.data = json.loads(entry.data.decode('utf-8'))
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
@ -224,6 +237,7 @@ def gen_item_report():
|
||||
report_data = {}
|
||||
|
||||
for char_xml in char_xmls:
|
||||
name = CharacterInfo.query.filter(CharacterInfo.id == char_xml.id).first().name
|
||||
try:
|
||||
character_json = xmltodict.parse(
|
||||
char_xml.xml_data,
|
||||
@ -233,15 +247,31 @@ def gen_item_report():
|
||||
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"]) == 0 or int(inv["attr_t"]) == 1):
|
||||
for item in inv["i"]:
|
||||
if item["attr_l"] in report_data:
|
||||
report_data[item["attr_l"]] = report_data[item["attr_l"]] + int(item["attr_c"])
|
||||
if ("attr_c" in item):
|
||||
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
|
||||
else:
|
||||
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + 1
|
||||
else:
|
||||
report_data[item["attr_l"]] = int(item["attr_c"])
|
||||
if ("attr_c" in item):
|
||||
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
|
||||
else:
|
||||
report_data[item["attr_l"]] = {"item_count": 1, "chars": {}}
|
||||
if name in report_data[item["attr_l"]]["chars"]:
|
||||
if ("attr_c" in item):
|
||||
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + int(item["attr_c"])
|
||||
else:
|
||||
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + 1
|
||||
else:
|
||||
if ("attr_c" in item):
|
||||
report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
|
||||
else:
|
||||
report_data[item["attr_l"]]["chars"][name] = 1
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::ITEMS - ERROR PARSING CHARACTER {char_xml.id}")
|
||||
current_app.logger.error(f"REPORT::ITEMS - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=report_data,
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
report_type="items",
|
||||
date=date
|
||||
)
|
||||
@ -289,7 +319,7 @@ def gen_currency_report():
|
||||
current_app.logger.error(f"REPORT::CURRENCY - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=report_data,
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
report_type="currency",
|
||||
date=date
|
||||
)
|
||||
@ -337,7 +367,7 @@ def gen_uscore_report():
|
||||
current_app.logger.error(f"REPORT::U-SCORE - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=report_data,
|
||||
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
|
||||
report_type="uscore",
|
||||
date=date
|
||||
)
|
||||
|
130
app/schemas.py
130
app/schemas.py
@ -1,130 +0,0 @@
|
||||
from flask_marshmallow import Marshmallow
|
||||
from app.models import (
|
||||
PlayKey,
|
||||
PetNames,
|
||||
Mail,
|
||||
UGC,
|
||||
PropertyContent,
|
||||
Property,
|
||||
CharacterXML,
|
||||
CharacterInfo,
|
||||
Account,
|
||||
AccountInvitation,
|
||||
ActivityLog,
|
||||
CommandLog
|
||||
)
|
||||
ma = Marshmallow()
|
||||
|
||||
|
||||
class PlayKeySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PlayKey
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
|
||||
class PetNamesSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PetNames
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class MailSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Mail
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class UGCSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = UGC
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = PropertyContent
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
ugc = ma.Nested(UGCSchema)
|
||||
|
||||
|
||||
class PropertySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Property
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
properties_contents = ma.Nested(PropertyContentSchema, many=True)
|
||||
|
||||
|
||||
class CharacterXMLSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterXML
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
|
||||
class CharacterInfoSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = CharacterInfo
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
charxml = ma.Nested(CharacterXMLSchema)
|
||||
properties_owner = ma.Nested(PropertySchema, many=True)
|
||||
pets = ma.Nested(PetNamesSchema, many=True)
|
||||
mail = ma.Nested(MailSchema, many=True)
|
||||
|
||||
|
||||
class AccountSchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Account
|
||||
include_relationships = False
|
||||
load_instance = True
|
||||
include_fk = False
|
||||
|
||||
play_key = ma.Nested(PlayKeySchema)
|
||||
charinfo = ma.Nested(CharacterInfoSchema, many=True)
|
||||
|
||||
|
||||
class AccountInvitationSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = AccountInvitation
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
invite_by_user = ma.Nested(AccountSchema)
|
||||
|
||||
|
||||
class ActivityLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = ActivityLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
||||
|
||||
|
||||
class CommandLogSchema(ma.SQLAlchemyAutoSchema): # noqa
|
||||
class Meta:
|
||||
model = CommandLog
|
||||
include_relationships = True
|
||||
load_instance = True
|
||||
include_fk = True
|
||||
|
||||
character = ma.Nested(CharacterInfoSchema())
|
@ -7,8 +7,14 @@ APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
|
||||
APP_SECRET_KEY = ""
|
||||
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
|
||||
|
||||
# Send Analytics for Developers to better fix issues
|
||||
ALLOW_ANALYTICS = False
|
||||
CLIENT_LOCATION = 'app/luclient/'
|
||||
CD_SQLITE_LOCATION = 'app/luclient/res/'
|
||||
CACHE_LOCATION = 'app/cache/'
|
||||
|
||||
CONFIG_LINK = False
|
||||
CONFIG_LINK_TITLE = ""
|
||||
CONFIG_LINK_HREF = ""
|
||||
CONFIG_LINK_TEXT = ""
|
||||
|
||||
# Flask settings
|
||||
CSRF_ENABLED = True
|
||||
@ -52,3 +58,16 @@ USER_PASSLIB_CRYPTCONTEXT_SCHEMES = ['bcrypt'] # bcrypt for password hashing
|
||||
# Flask-User routing settings
|
||||
USER_AFTER_LOGIN_ENDPOINT = "main.index"
|
||||
USER_AFTER_LOGOUT_ENDPOINT = "main.index"
|
||||
|
||||
# Option will be removed once this feature is full implemeted
|
||||
ENABLE_CHAR_XML_UPLOAD = False
|
||||
|
||||
# Recaptcha settings
|
||||
# See: https://flask-wtf.readthedocs.io/en/1.2.x/form/#recaptcha
|
||||
RECAPTCHA_ENABLE = False
|
||||
RECAPTCHA_PUBLIC_KEY = ''
|
||||
RECAPTCHA_PRIVATE_KEY = ''
|
||||
# Optional
|
||||
# RECAPTCHA_API_SERVER = ''
|
||||
# RECAPTCHA_PARAMETERS = ''
|
||||
RECAPTCHA_DATA_ATTRS = {'theme': 'white', 'size': 'invisible'}
|
||||
|
0
app/static/css/.gitkeep
Normal file
0
app/static/css/.gitkeep
Normal file
@ -14,17 +14,13 @@
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Name</th>
|
||||
{% if config.USER_ENABLE_EMAIL %}
|
||||
<th>Email</th>
|
||||
{% endif %}
|
||||
<th>Email</th>
|
||||
<th>GM Level</th>
|
||||
<th>Locked</th>
|
||||
<th>Banned</th>
|
||||
<th>Muted</th>
|
||||
<th>Registered</th>
|
||||
{% if config.USER_ENABLE_EMAIL %}
|
||||
<th>Email Confirmed</th>
|
||||
{% endif %}
|
||||
<th>Email Confirmed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
@ -1,188 +0,0 @@
|
||||
{% extends "bootstrap/base.html" %}
|
||||
{% block title %}Key Creation{% endblock %}
|
||||
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
</ul>
|
||||
<ul class="nav navbar-nav" style="float: right">
|
||||
<li class="active"><a href="#">Welcome {{ current_user.username }}!</a></li>
|
||||
<li><a href="{{ url_for('logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock navbar %}}
|
||||
|
||||
{% block content %}
|
||||
{# LOGO #}
|
||||
<div class="container" style="margin-top: 50px">
|
||||
|
||||
{# Display logo #}
|
||||
<div style="margin-bottom: 50px">
|
||||
<img
|
||||
src="{{ url_for('static', filename=resources.LOGO) }}"
|
||||
class="center-block img-responsive mx-auto d-block"
|
||||
alt="Logo"
|
||||
width="100"
|
||||
height="100"
|
||||
>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{# Key creation #}
|
||||
<div class="container">
|
||||
<div class="text-center">
|
||||
<h3>Key Creation</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
|
||||
{# If the error value is set, display the error in red text #}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# If the message value is set, display the message in green text #}
|
||||
{% if message %}
|
||||
<div class="alert alert-success">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Form which takes in Admin Username, Admin Password, and the amount of keys to create. #}
|
||||
<form action="{{ url_for('dashboard') }}" method="post">
|
||||
{# Key count input #}
|
||||
<div class="form-group">
|
||||
<label for="key_count">Generate keys</label>
|
||||
<input type="number" class="form-control" name="key_count" placeholder="Enter number of keys...">
|
||||
<small class="form-text text-muted">Number of keys to create.</small>
|
||||
</div>
|
||||
|
||||
{# Submit button #}
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Generate Keys</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# If the keys value is set, create a list for each key in keys #}
|
||||
{% if keys %}
|
||||
<div class="alert alert-success">
|
||||
<ul>
|
||||
{% for key in keys %}
|
||||
<li>{{ key }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Activity graphs #}
|
||||
<div class="container">
|
||||
|
||||
<div class="text-center">
|
||||
<h3>Activity</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
|
||||
<canvas id="sessions_graph" width="400" height="400"></canvas>
|
||||
<canvas id="play_time_graph" width="400" height="400"></canvas>
|
||||
<canvas id="zone_play_time_graph" width="400" height="400"></canvas>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Make a get request to the server to load the activity data
|
||||
$.get("{{ url_for('load_activities') }}", function(data) {
|
||||
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "sessions"
|
||||
$.get("{{ url_for('activity_data', name='sessions') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('sessions_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "play_time"
|
||||
$.get("{{ url_for('activity_data', name='play_time') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('play_time_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Make a get request to the server to get the activity data for "zone_play_time"
|
||||
$.get("{{ url_for('activity_data', name='zone_play_time') }}", function(data) {
|
||||
// Load data as a json object
|
||||
data = JSON.parse(data);
|
||||
var ctx = document.getElementById('zone_play_time_graph').getContext('2d');
|
||||
var myChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock scripts %}}
|
@ -90,30 +90,29 @@
|
||||
let target_nav = '#{{request.endpoint}}'.replace('\.', '-');
|
||||
$(target_nav).addClass('active');
|
||||
});
|
||||
// make tooltips with data work
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
// Matomo JS analytics
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.aronwk.com/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
{% endif %}
|
||||
function setInnerHTML(elm, html) {
|
||||
elm.innerHTML = html;
|
||||
$("body").tooltip({ selector: '[data-toggle=tooltip]' });
|
||||
Array.from(elm.querySelectorAll("script"))
|
||||
.forEach( oldScriptEl => {
|
||||
const newScriptEl = document.createElement("script");
|
||||
|
||||
Array.from(oldScriptEl.attributes).forEach( attr => {
|
||||
newScriptEl.setAttribute(attr.name, attr.value)
|
||||
});
|
||||
|
||||
const scriptText = document.createTextNode(oldScriptEl.innerHTML);
|
||||
newScriptEl.appendChild(scriptText);
|
||||
|
||||
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
|
||||
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<!-- Matomo no js analytics -->
|
||||
<noscript><p><img src="https://matomo.aronwk.com/matomo.php?idsite=3&rec=1" style="border:0;" alt="" /></p></noscript>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
34
app/templates/character/upload.html.j2
Normal file
34
app/templates/character/upload.html.j2
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Character XML Upload
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Character XML Upload
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.blink {
|
||||
animation: blinker .5s linear infinite;
|
||||
color: red;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<h3 class="text-center blink">PROCEED WITH CAUTION</h3>
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" >
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.char_xml) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -15,8 +15,9 @@
|
||||
{% include 'partials/_character.html.j2' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
{% include 'partials/_charxml.html.j2'%}
|
||||
<div class="col-sm" id="charxml">
|
||||
Loading Character Data
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@ -32,3 +33,14 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content_after %}
|
||||
|
||||
{% block js %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
fetch({{ url_for("characters.chardata", id=character_data.id)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("charxml"), text);
|
||||
})
|
||||
</script>
|
||||
{% endblock js %}
|
||||
|
@ -60,7 +60,12 @@
|
||||
|
||||
{# Remember me #}
|
||||
{% if user_manager.USER_ENABLE_REMEMBER_ME %}
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ render_field(form.recaptcha, tabindex=250) }}
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
|
@ -29,6 +29,11 @@
|
||||
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ form.recaptcha }}
|
||||
{% endif %}
|
||||
|
||||
{# Submit button #}
|
||||
{{ render_submit_field(login_form.submit, tabindex=180) }}
|
||||
</form>
|
||||
@ -63,6 +68,11 @@
|
||||
{{ render_field(register_form.retype_password, tabindex=240) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ register_form.recaptcha }}
|
||||
{% endif %}
|
||||
|
||||
{{ render_submit_field(register_form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
|
@ -47,6 +47,11 @@
|
||||
{{ render_field(form.retype_password, tabindex=240) }}
|
||||
{% endif %}
|
||||
|
||||
{# recaptcha #}
|
||||
{% if config.RECAPTCHA_ENABLE %}
|
||||
{{ render_field(form.recaptcha, tabindex=250) }}
|
||||
{% endif %}
|
||||
|
||||
{{ render_submit_field(form.submit, tabindex=280) }}
|
||||
</form>
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
||||
<a id='property-index' class='nav-link' href='{{ url_for('properties.index') }}'>Properties</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated and current_user.gm_level == 9 and config.REQUIRE_PLAY_KEY %}
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 5 and config.REQUIRE_PLAY_KEY %}
|
||||
{# Play Keys #}
|
||||
<a id='play_keys-index' class='nav-link' href='{{ url_for('play_keys.index') }}'>Play Keys</a>
|
||||
{% endif %}
|
||||
@ -71,8 +71,8 @@
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 8 %}
|
||||
<hr/>
|
||||
<h3 class="text-center">Logs</h3>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.activity') }}'>Command Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.command') }}'>Activity Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.command') }}'>Command Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.activity') }}'>Activity Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.audit') }}'>Audit Log</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('log.system') }}'>System Log</a>
|
||||
{% endif %}
|
||||
|
@ -35,29 +35,6 @@
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='lddviewer/base64-binary.js') }}"></script>
|
||||
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<script>
|
||||
// Matomo JS analytics
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.aronwk.com/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if config.ALLOW_ANALYTICS %}
|
||||
<!-- Matomo no js analytics -->
|
||||
<noscript><p><img src="https://matomo.aronwk.com/matomo.php?idsite=3&rec=1" style="border:0;" alt="" /></p></noscript>
|
||||
{% endif %}
|
||||
|
||||
<script type='module'>
|
||||
import {MTLLoader} from 'https://cdn.jsdelivr.net/npm/three@0.116.0/examples/jsm/loaders/MTLLoader.js'
|
||||
import {OBJLoader} from 'https://cdn.jsdelivr.net/npm/three@0.116.0/examples/jsm/loaders/OBJLoader.js'
|
||||
|
@ -22,7 +22,9 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account:Character</th>
|
||||
<th>Command</th>
|
||||
<th>Activity</th>
|
||||
<th>Time</th>
|
||||
<th>Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@ -38,7 +40,7 @@
|
||||
"order": [[0, "desc"]],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('log.get_commands') }}",
|
||||
"ajax": "{{ url_for('log.get_activities') }}",
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -22,9 +22,7 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account:Character</th>
|
||||
<th>Activity</th>
|
||||
<th>Time</th>
|
||||
<th>Map</th>
|
||||
<th>Command</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@ -40,7 +38,7 @@
|
||||
"order": [[0, "desc"]],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('log.get_activities') }}",
|
||||
"ajax": "{{ url_for('log.get_commands') }}",
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -4,9 +4,47 @@
|
||||
|
||||
{% block content_before %}
|
||||
Online Players: {{ online }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{# show general zone info to everyone #}
|
||||
{% if zones %}
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
{% for zone, players in zones.items() %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ zone|get_zone_name }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ players }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# only show this info to high level admina #}
|
||||
{% if current_user.gm_level >= 8 and users|length > 0 %}
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
{% for user in users %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ user[0] }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ user[1]|get_zone_name }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
<h4 class="text-center">Staff</h4>
|
||||
@ -24,7 +62,26 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='card mx-auto mt-5 shadow-sm bg-dark border-primary'>
|
||||
<div class="card-body">
|
||||
<h4 class="text-center">Links</h4>
|
||||
|
||||
{% if config.CONFIG_LINK %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ config.CONFIG_LINK_TITLE }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="{{ url_for('static', filename=config.CONFIG_LINK_HREF) }}">
|
||||
{{ config.CONFIG_LINK_TEXT }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Source
|
||||
|
@ -108,6 +108,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif current_user.gm_level == 9 %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-danger btn btn-block" href='{{ url_for('accounts.pass_reset', id= account_data.id) }}'>
|
||||
Reset User's Password
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if account_data.play_key and current_user.gm_level > 3 and config.REQUIRE_PLAY_KEY %}
|
||||
|
@ -41,29 +41,35 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if request.endpoint != "characters.view" %}
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view', id=character.id) }}'>
|
||||
View Character
|
||||
</a>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view', id=character.id) }}'>
|
||||
View Character
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<br/>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view_xml', id=character.id) }}'>
|
||||
View XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.get_xml', id=character.id) }}'>
|
||||
Download XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('accounts.view', id=character.account_id) }}'>
|
||||
View Account: {{character.account.username}}
|
||||
</a>
|
||||
<br/>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.view_xml', id=character.id) }}'>
|
||||
View XML
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.get_xml', id=character.id) }}'>
|
||||
Download XML
|
||||
</a>
|
||||
{% if config.ENABLE_CHAR_XML_UPLOAD and current_user.gm_level >= 8 %}
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.upload', id=character.id) }}'>
|
||||
Upload XML
|
||||
</a>
|
||||
{% endif %}
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('accounts.view', id=character.account_id) }}'>
|
||||
View Account: {{character.account.username}}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.gm_level > 2 %}
|
||||
|
@ -11,9 +11,11 @@
|
||||
<div class="col text-right">
|
||||
U-Score: {{ character_json.obj.char.attr_ls }}
|
||||
</div>
|
||||
<div class="col">
|
||||
Level: {{ character_json.obj.lvl.attr_l }}
|
||||
</div>
|
||||
{% if "lvl" in character_json.obj %}
|
||||
<div class="col">
|
||||
Level: {{ character_json.obj.lvl.attr_l }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
@ -63,9 +65,13 @@
|
||||
Play time:
|
||||
</div>
|
||||
<div class="col">
|
||||
{% if character_json.obj.char.attr_time %}
|
||||
{{ (character_json.obj.char.attr_time|int/60/60/24)|int }} Days
|
||||
{{ (character_json.obj.char.attr_time|int/60/60)|int - ((character_json.obj.char.attr_time|int/60/60/24)|int) * 24}} Hours
|
||||
{{ (character_json.obj.char.attr_time|int/60 - (character_json.obj.char.attr_time|int/60/60)|int*60)|int }} Minutes
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="bg-primary"/>
|
||||
@ -108,63 +114,33 @@
|
||||
<div class="tab-content mt-3" id="nav-invContent">
|
||||
<div class="tab-pane fade show active" id="nav-items" role="tabpanel" aria-labelledby="nav-items-tab">
|
||||
{# Inv ID 0 - Index: 0 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "0" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-vault" role="tabpanel" aria-labelledby="nav-vault-tab">
|
||||
{# Inv ID 1 - Index: 1 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "1" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-vault-models" role="tabpanel" aria-labelledby="nav-vault-models-tab">
|
||||
{# Inv ID 14 - Index: 10 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "14" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-bricks" role="tabpanel" aria-labelledby="nav-bricks-tab">
|
||||
{# Inv ID 2 - Index: 2 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "2" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-models" role="tabpanel" aria-labelledby="nav-models-tab">
|
||||
{# Inv ID 5 - Index: 6 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "5" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="nav-behaviors" role="tabpanel" aria-labelledby="nav-behaviors-tab">
|
||||
{# Inv ID 7 - Index: 8 #}
|
||||
{% for item in character_json.obj.inv.holdings.in %}
|
||||
{% if item.attr_t == "7" %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading Inventory
|
||||
{% include 'partials/_loading.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,10 +163,14 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% for zone in character_json.obj.char.zs.s %}
|
||||
{% include 'partials/charxml/_zone_stats.html.j2' %}
|
||||
{{ '<hr class="bg-primary"/>' if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
{% if character_json.obj.char.zs %}
|
||||
{% for zone in character_json.obj.char.zs.s %}
|
||||
{% include 'partials/charxml/_zone_stats.html.j2' %}
|
||||
{{ '<hr class="bg-primary"/>' if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
No Stats Yet
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
@ -219,3 +199,40 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=0)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-items"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=1)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-vault"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=14)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-vault-models"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=2)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-bricks"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=5)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-models"), text);
|
||||
})
|
||||
|
||||
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=7)|tojson }})
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
setInnerHTML(document.getElementById("nav-behaviors"), text);
|
||||
})
|
||||
</script>
|
@ -14,7 +14,7 @@
|
||||
{% if gm_level==0 %}
|
||||
Player
|
||||
{% elif gm_level==1 %}
|
||||
Key Distributor
|
||||
Elevevated Civilan {# Unused #}
|
||||
{% elif gm_level==2 %}
|
||||
Junior Moderator
|
||||
{% elif gm_level==3 %}
|
||||
|
21
app/templates/partials/_loading.html
Normal file
21
app/templates/partials/_loading.html
Normal file
@ -0,0 +1,21 @@
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s;">
|
||||
<span class="sr-only">Loading 0</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 1s;">
|
||||
<span class="sr-only">Loading 1</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 2s;">
|
||||
<span class="sr-only">Loading 2</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 3s;">
|
||||
<span class="sr-only">Loading 3</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 4s;">
|
||||
<span class="sr-only">Loading 4</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 5s;">
|
||||
<span class="sr-only">Loading 5</span>
|
||||
</div>
|
||||
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 6s;">
|
||||
<span class="sr-only">Loading 6</span>
|
||||
</div>
|
@ -77,6 +77,14 @@
|
||||
{{ property.performance_cost }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Clone ID:
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ property.clone_id }}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.endpoint != "properties.view" %}
|
||||
<br/>
|
||||
<div class="row">
|
||||
|
@ -4,21 +4,29 @@
|
||||
alt="{{ inv_item.attr_l|get_lot_name }}"
|
||||
class="border p-1 border-primary rounded m-1"
|
||||
width="60"
|
||||
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
|
||||
{% if 'attr_eq' in inv_item %}
|
||||
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
|
||||
{% endif %}
|
||||
height="60"
|
||||
data-html="true"
|
||||
data-toggle="tooltip"
|
||||
data-placement="left"
|
||||
title="{% include 'partials/charxml/_item_tooltip.html.j2' %}"
|
||||
>
|
||||
{% if inv_item.attr_c != "1" %}
|
||||
{% if 'attr_c' in inv_item %}
|
||||
<span class="inventory-count text-bold">
|
||||
{{ inv_item.attr_c }}
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
+999
|
||||
{% elif inv_item.attr_c|int > 1 %}
|
||||
{{ inv_item.attr_c }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if inv_item.attr_b == "true" %}
|
||||
<span class="inventory-lock">
|
||||
<i class='fas fa-lock'></i>
|
||||
</span>
|
||||
{% if 'attr_b' in inv_item %}
|
||||
{% if inv_item.attr_b == "true" %}
|
||||
<span class="inventory-lock">
|
||||
<i class='fas fa-lock'></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
9
app/templates/partials/charxml/_inventory.html.j2
Normal file
9
app/templates/partials/charxml/_inventory.html.j2
Normal file
@ -0,0 +1,9 @@
|
||||
{% if inventory.i is iterable and (inventory.i is not string and inventory.i is not mapping) %}
|
||||
{% for inv_item in inventory.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=inventory.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
@ -66,3 +66,8 @@
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if 'attr_c' in inv_item %}
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
<br />Count: {{ inv_item.attr_c|numberFormat }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -20,18 +20,34 @@
|
||||
<th scope="col">
|
||||
Count
|
||||
</th>
|
||||
<th scope="col">
|
||||
Breakdown
|
||||
</th>
|
||||
<th scope="col">
|
||||
Rarity
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for lot, count in data.items() %}
|
||||
{% for lot, details in data.items() %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ lot|get_lot_name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ count }}
|
||||
{% if details.chars %}
|
||||
{{ details.item_count }}
|
||||
{% else %}
|
||||
{{ details }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if details.chars %}
|
||||
{% for char, value in details.chars|dictsort(false, 'value')|reverse %}
|
||||
{{char}}: {{value}}<br/>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Missing
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ lot|get_lot_rarity }}
|
||||
|
2
entrypoint.bat
Normal file
2
entrypoint.bat
Normal file
@ -0,0 +1,2 @@
|
||||
python3 -m flask db upgrade
|
||||
python3 wsgi.py
|
@ -1,8 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# unzip brickdb from client to the right places
|
||||
unzip -n -q /app/luclient/res/brickdb.zip -d app/luclient/res/
|
||||
|
||||
# TODO: preconvert images options
|
||||
# TODO: preconvery models options
|
||||
|
||||
|
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
@ -22,8 +22,8 @@ logger = logging.getLogger('alembic.env')
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||
)
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
|
164
migrations/versions/1164e037907f_compressss_reports.py
Normal file
164
migrations/versions/1164e037907f_compressss_reports.py
Normal file
@ -0,0 +1,164 @@
|
||||
"""compressss reports
|
||||
|
||||
Revision ID: 1164e037907f
|
||||
Revises: a6e42ef03da7
|
||||
Create Date: 2023-11-18 01:38:00.127472
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
from sqlalchemy.orm.session import Session
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.types import JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
import gzip
|
||||
import json
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1164e037907f'
|
||||
down_revision = 'a6e42ef03da7'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
class ReportsUpgradeNew(Base):
|
||||
__tablename__ = 'reports'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
mysql.MEDIUMBLOB(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
def save(self):
|
||||
sa.session.add(self)
|
||||
sa.session.commit()
|
||||
sa.session.refresh(self)
|
||||
|
||||
|
||||
class ReportsUpgradeOld(Base):
|
||||
__tablename__ = 'reports_old'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
class ReportsDowngradeOld(Base):
|
||||
__tablename__ = 'reports'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
mysql.MEDIUMBLOB(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
def save(self):
|
||||
sa.session.add(self)
|
||||
sa.session.commit()
|
||||
sa.session.refresh(self)
|
||||
|
||||
|
||||
class ReportsDowngradeNew(Base):
|
||||
__tablename__ = 'reports_old'
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
data = sa.Column(
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
report_type = sa.Column(
|
||||
sa.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = sa.Column(
|
||||
sa.Date(),
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.rename_table('reports', 'reports_old')
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
reports = session.query(ReportsUpgradeOld)
|
||||
op.create_table('reports',
|
||||
sa.Column('data', mysql.MEDIUMBLOB(), nullable=False),
|
||||
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
|
||||
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('report_type', 'date')
|
||||
)
|
||||
# insert records
|
||||
new_reports = []
|
||||
# insert records
|
||||
for report in reports:
|
||||
new_reports.append({
|
||||
"data":gzip.compress(json.dumps(report.data).encode('utf-8')),
|
||||
"report_type":report.report_type,
|
||||
"date":report.date
|
||||
})
|
||||
op.bulk_insert(ReportsUpgradeNew.__table__, new_reports)
|
||||
op.drop_table('reports_old')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('reports')
|
||||
op.create_table('reports',
|
||||
sa.Column('data', JSON(), nullable=False),
|
||||
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
|
||||
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('report_type', 'date')
|
||||
)
|
||||
# ### end Alembic commands ###
|
@ -77,11 +77,11 @@ def upgrade():
|
||||
sa.Column('created_at', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('play_key_id', mysql.INTEGER(), nullable=True),
|
||||
sa.Column('mute_expire', mysql.BIGINT(unsigned=True), server_default='0', nullable=False),
|
||||
sa.ForeignKeyConstraint(['play_key_id'], ['play_keys.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
|
||||
op.create_foreign_key(None, 'accounts', 'play_keys', ['play_key_id'], ['id'], ondelete='CASCADE')
|
||||
op.add_column('accounts', sa.Column('active', sa.BOOLEAN(), server_default='1', nullable=False))
|
||||
op.add_column('accounts', sa.Column('email_confirmed_at', sa.DateTime(), nullable=True))
|
||||
op.add_column('accounts', sa.Column('email', sa.Unicode(length=255), server_default='', nullable=True))
|
||||
@ -103,14 +103,13 @@ def upgrade():
|
||||
sa.Column('other_player_id', mysql.TEXT(), nullable=False),
|
||||
sa.Column('selection', mysql.TEXT(), nullable=False),
|
||||
sa.Column('submitted', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['resoleved_by_id'], ['accounts.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
op.add_column('bug_reports', sa.Column('resolved_time', mysql.TIMESTAMP(), nullable=True))
|
||||
op.add_column('bug_reports', sa.Column('resoleved_by_id', sa.Integer(), nullable=True))
|
||||
op.add_column('bug_reports', sa.Column('resolution', mysql.TEXT(), nullable=True))
|
||||
op.create_foreign_key(None, 'bug_reports', 'accounts', ['resoleved_by_id'], ['id'])
|
||||
op.create_foreign_key(None, 'bug_reports', 'accounts', ['resoleved_by_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
if 'charinfo' not in tables:
|
||||
op.create_table('charinfo',
|
||||
|
@ -0,0 +1,33 @@
|
||||
"""force play_key_id to be nullable
|
||||
|
||||
Revision ID: a6e42ef03da7
|
||||
Revises: 8e52b5c7568a
|
||||
Create Date: 2022-11-29 19:14:22.645911
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a6e42ef03da7'
|
||||
down_revision = '8e52b5c7568a'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('accounts', 'play_key_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('accounts', 'play_key_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
@ -1,61 +1,20 @@
|
||||
alembic==1.7.5
|
||||
APScheduler==3.8.1
|
||||
astroid==2.9.1
|
||||
autopep8==1.6.0
|
||||
bcrypt==3.2.0
|
||||
blinker==1.4
|
||||
cffi==1.14.6
|
||||
click==8.0.1
|
||||
colorama==0.4.4
|
||||
cryptography==36.0.0
|
||||
dnspython==2.1.0
|
||||
dominate==2.6.0
|
||||
email-validator==1.1.3
|
||||
Flask==2.0.1
|
||||
Flask==3.0.0
|
||||
Flask-APScheduler==1.12.3
|
||||
Flask-Assets==2.0
|
||||
Flask-Login==0.5.0
|
||||
Flask-Mail==0.9.1
|
||||
flask-marshmallow==0.14.0
|
||||
Flask-Assets==2.1.0
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-SQLAlchemy==2.5.1
|
||||
Flask-SQLAlchemy==3.1.1
|
||||
Flask-User==1.0.2.2
|
||||
Flask-WTF==1.0.0
|
||||
greenlet==1.1.0
|
||||
idna==3.3
|
||||
isort==5.10.1
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.1
|
||||
lazy-object-proxy==1.7.1
|
||||
Flask-WTF==1.2.1
|
||||
gunicorn==21.2.0
|
||||
libsass==0.21.0
|
||||
Mako==1.1.6
|
||||
MarkupSafe==2.0.1
|
||||
marshmallow==3.14.1
|
||||
marshmallow-sqlalchemy==0.26.1
|
||||
mccabe==0.6.1
|
||||
passlib==1.7.4
|
||||
platformdirs==2.4.1
|
||||
pycodestyle==2.8.0
|
||||
pycparser==2.20
|
||||
pydocstyle==6.1.1
|
||||
pyflakes==2.4.0
|
||||
pylama==8.3.3
|
||||
pylint==2.12.2
|
||||
PyMySQL==1.0.2
|
||||
python-dateutil==2.8.2
|
||||
pytz==2021.3
|
||||
pytz-deprecation-shim==0.1.0.post0
|
||||
six==1.16.0
|
||||
snowballstemmer==2.2.0
|
||||
SQLAlchemy==1.4.22
|
||||
SQLAlchemy==2.0.23
|
||||
sqlalchemy-datatables==2.0.1
|
||||
toml==0.10.2
|
||||
tzdata==2021.5
|
||||
tzlocal==4.1
|
||||
visitor==0.1.3
|
||||
Wand==0.6.7
|
||||
webassets==2.0
|
||||
Werkzeug==2.0.1
|
||||
wrapt==1.13.3
|
||||
Werkzeug==3.0.1
|
||||
WTForms==3.0.0
|
||||
xmltodict==0.12.0
|
||||
|
2
wsgi.py
2
wsgi.py
@ -18,7 +18,7 @@ else:
|
||||
from logging.handlers import RotatingFileHandler
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
file_handler = RotatingFileHandler('nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
file_handler = RotatingFileHandler('logs/nexus_dashboard.log', maxBytes=1024 * 1024 * 100, backupCount=20)
|
||||
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
Loading…
x
Reference in New Issue
Block a user