mirror of
https://github.com/DarkflameUniverse/NexusDashboard.git
synced 2026-02-19 21:19:46 +00:00
Compare commits
192 Commits
v0.0.1
...
configurat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc6bbdfaa7 | ||
|
|
9cda62cef7 | ||
|
|
99087eb30a | ||
|
|
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 | ||
|
|
0c18d03aa7 | ||
|
|
ded0e55501 | ||
|
|
3668ca8792 | ||
|
|
a07515cdb2 | ||
|
|
35ee0ed457 | ||
|
|
7b4e11d65b | ||
|
|
07136ef283 | ||
|
|
1dbe0ce980 | ||
|
|
1dbb637053 | ||
|
|
26111d0bcf | ||
|
|
4124d98853 | ||
|
|
ef312fc744 | ||
|
|
1790a8b060 | ||
|
|
02bb6bb0aa | ||
|
|
8dd4a2c80e | ||
|
|
6f4b2ef574 | ||
|
|
33d24745e1 | ||
|
|
8e58b0bc1f | ||
|
|
413a2c06a4 | ||
|
|
c32da45a16 | ||
|
|
8ad6249275 | ||
|
|
65ce84e090 | ||
|
|
d66bdee575 | ||
|
|
72be25f5b5 | ||
|
|
63d5d2da87 | ||
|
|
e73d1ecd53 | ||
|
|
5492ae6668 | ||
|
|
7b27cab25d | ||
|
|
b6cad4acae | ||
|
|
900a3cbf3c | ||
|
|
993e5dcfe4 | ||
|
|
2dc5437fc3 | ||
|
|
c94d1cc62c | ||
|
|
f73ce143a1 | ||
|
|
e5428dcd4d | ||
|
|
44689aad36 | ||
|
|
f1d79f1e90 | ||
|
|
1113295c5b | ||
|
|
e45fb3233a | ||
|
|
a3894c3450 | ||
|
|
6fa7ade6a3 | ||
|
|
70aed00d97 | ||
|
|
f8d5928c86 | ||
|
|
5ac384663b | ||
|
|
a79631be8e | ||
|
|
ef7da76629 | ||
|
|
520c5f2d40 | ||
|
|
27c4f1a7f2 | ||
|
|
7f992a5dfa | ||
|
|
560afa5e0d | ||
|
|
17c6d5bc1a | ||
|
|
be80d7b2ab | ||
|
|
521a10756e | ||
|
|
b6905c9235 | ||
|
|
faff08b160 | ||
|
|
c8b21ae6a6 | ||
|
|
179ea43cb8 | ||
|
|
1566df65cb | ||
|
|
4d62e70810 | ||
|
|
24e98190fe | ||
|
|
6606f1558e | ||
|
|
5b949e02d1 | ||
|
|
f0faa4c53b | ||
|
|
418c81944b | ||
|
|
8285f02ed8 | ||
|
|
ede3642efe | ||
|
|
3fed8cb02f | ||
|
|
7cb0b94972 | ||
|
|
1cb64f7594 | ||
|
|
33739456df | ||
|
|
5ce9ac85bc | ||
|
|
70742549b9 | ||
|
|
039b45c824 | ||
|
|
9452e8c801 | ||
|
|
09363fcd06 | ||
|
|
6303a86778 | ||
|
|
7f37343de4 | ||
|
|
78ab5b93bb | ||
|
|
76c862a2ba | ||
|
|
04f4e833a4 | ||
|
|
a66e4aaf80 | ||
|
|
e622c44042 | ||
|
|
819d51c68a | ||
|
|
94411568ce | ||
|
|
b829d666c9 | ||
|
|
2e82f94b9d | ||
|
|
b66d2748b4 | ||
|
|
78ed39905f | ||
|
|
d63dd807e3 | ||
|
|
e9aef0219e | ||
|
|
d77eaf16d1 | ||
|
|
24a398b89c | ||
|
|
26cc2d431c | ||
|
|
8a019c340e | ||
|
|
0dbe9d87fc | ||
|
|
d792596926 | ||
|
|
9cc3dbb4c4 | ||
|
|
de698f86fa | ||
|
|
2e8ce8ea19 | ||
|
|
91e6ed33e7 | ||
|
|
f0cbcee100 | ||
|
|
506d4ac090 | ||
|
|
121c407cdb | ||
|
|
d3c8c5d77b | ||
|
|
8aec4a45d2 | ||
|
|
87384c1b98 | ||
|
|
f0df357258 | ||
|
|
869bbdf7e6 | ||
|
|
8646c40943 | ||
|
|
ba0c4801df | ||
|
|
b0e76ce691 | ||
|
|
cf359e2c6b | ||
|
|
c6d624e154 | ||
|
|
0066e0ea2d | ||
|
|
3df9f143ed | ||
|
|
649c986345 | ||
|
|
172bbe5b60 | ||
|
|
7194a04c5d | ||
|
|
7bb0e46e56 | ||
|
|
2a5fad3c87 | ||
|
|
ac835de53b | ||
|
|
fba782fd96 | ||
|
|
59db1df604 | ||
|
|
aede0f4f1a | ||
|
|
1b5f002d6d | ||
|
|
256ea19bfc | ||
|
|
4bda98670b | ||
|
|
6deb02b11d | ||
|
|
d6d8bb085d | ||
|
|
0fa91c9cfa | ||
|
|
8cc630d420 | ||
|
|
7f7b854b8b | ||
|
|
81193499f4 | ||
|
|
dbe6da370a | ||
|
|
6197f630e9 | ||
|
|
683a1d6f7d | ||
|
|
ab25db182d | ||
|
|
c183d1ff5a | ||
|
|
de89647421 | ||
|
|
b2af6d967a | ||
|
|
1a5531eb19 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,4 +1,4 @@
|
||||
app/static/bootstrap-4.2.1/* linguist-vendored
|
||||
app/static/bootswatch-master/* linguist-vendored
|
||||
app/static/datatables/* linguist-vendored
|
||||
app/static/font-awesome/* linguist-vendored
|
||||
app/static/bootstrap-4.2.1/* linguist-vendored=true
|
||||
app/static/datatables/* linguist-vendored=true
|
||||
app/static/font-awesome/* linguist-vendored=true
|
||||
app/static/chartjs/* linguist-vendored=true
|
||||
|
||||
8
.gitignore
vendored
8
.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
|
||||
@@ -13,3 +13,9 @@ app/static/ldddb/*
|
||||
locale.xml
|
||||
app/luclient/*
|
||||
app/cache/*
|
||||
property_files/*
|
||||
*.log
|
||||
app/settings.py
|
||||
*.exe
|
||||
*.csv
|
||||
*.sql
|
||||
|
||||
27
Jenkinsfile
vendored
27
Jenkinsfile
vendored
@@ -5,22 +5,25 @@ properties([
|
||||
branchFilter: 'origin/(.*)',
|
||||
defaultValue: 'origin/main',
|
||||
description: '',
|
||||
name: 'BRANCH',
|
||||
quickFilterEnabled: false,
|
||||
name: 'GIT_BRANCH',
|
||||
quickFilterEnabled: true,
|
||||
selectedValue: 'DEFAULT',
|
||||
sortMode: 'NONE',
|
||||
tagFilter: '*',
|
||||
useRepository: 'git@github.com:DarkflameUniverse/NexusDashboard.git',
|
||||
type: 'PT_BRANCH'
|
||||
type: 'PT_BRANCH',
|
||||
listSize: "1"
|
||||
)
|
||||
])
|
||||
])
|
||||
|
||||
node('worker'){
|
||||
currentBuild.setDescription(params.GIT_BRANCH)
|
||||
|
||||
stage('Clone Code'){
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: params.BRANCH]],
|
||||
branches: [[name: params.GIT_BRANCH]],
|
||||
extensions: [],
|
||||
userRemoteConfigs: [
|
||||
[
|
||||
@@ -31,19 +34,19 @@ node('worker'){
|
||||
])
|
||||
}
|
||||
def tag = ''
|
||||
stage("Build Container"){
|
||||
if (params.BRANCH.contains('main')){
|
||||
stage('Build Container'){
|
||||
if (params.GIT_BRANCH.contains('main')){
|
||||
tag = 'latest'
|
||||
} else {
|
||||
tag = params.BRANCH.replace('\\', '-')
|
||||
tag = params.GIT_BRANCH.replace('\\', '-')
|
||||
}
|
||||
sh "docker build -t aronwk/nexus-dashboard:${tag} ."
|
||||
sh "docker build -t aronwk/nexus-dashboard:$tag ."
|
||||
}
|
||||
stage("Push Container"){
|
||||
stage('Push Container'){
|
||||
withCredentials([usernamePassword(credentialsId: 'docker-hub-token', passwordVariable: 'password', usernameVariable: 'username')]) {
|
||||
sh "docker login -u ${username} -p ${password}"
|
||||
sh "docker push aronwk/nexus-dashboard:${tag}"
|
||||
sh 'docker logout'
|
||||
sh "docker login -u $username -p $password"
|
||||
sh "docker push aronwk/nexus-dashboard:$tag"
|
||||
sh "docker logout"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
352
README.md
352
README.md
@@ -1,69 +1,333 @@
|
||||
# Nexus Dashboard
|
||||
|
||||
**This is a WIP: For Advanced Users**
|
||||
|
||||
<p align="center">
|
||||
<img src="app/static/logo/logo.png" alt="Sublime's custom image"/>
|
||||
<img src="app/static/logo/logo.png" alt="DLU logo"/>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
* Account Management:
|
||||
* Ban, Lock, and Mute accounts (This Mute affects all Characters)
|
||||
* Account Deletion
|
||||
* Email ( all optional ):
|
||||
* Require email verification
|
||||
* Reset Password via Email
|
||||
* Edit Email ( by Admin only )
|
||||
* User Registration
|
||||
* Invitations ( TODO: Implement this )
|
||||
* Invitation Only Registration ( TODO: Implement this )
|
||||
* Play Key Management:
|
||||
* Create, Edit, and Add notes to play keys
|
||||
* View accounts Tied to a play key
|
||||
* Character Management:
|
||||
* Rescue: Pull character to a previously visited world
|
||||
* Restrict Trade: Toggle the character's ability to trade
|
||||
* Restrict Mail: Toggle the character's ability to send mail
|
||||
* Restrict Chat: Toggle the character's ability to send chat messages
|
||||
* Inventory viewer
|
||||
* View backpack contents, vault, models, and more!
|
||||
* Stats Viewer
|
||||
* Moderation:
|
||||
* Character Names:
|
||||
* Approve and mark as needs rename
|
||||
* Pet Names:
|
||||
* Auto-moderation of Pet names based on already moderated names
|
||||
* This is a scheduled tack that runs in the background every hour
|
||||
* Character Association, to see who has requested what name
|
||||
* Name cleanup: remove names of deleted pets/characters
|
||||
* Properties:
|
||||
* Approve and Un-approve Properties
|
||||
* Property/Model viewer
|
||||
* Pre-built and UGC model rendering
|
||||
* View Properties in full 360 in the browser!
|
||||
* View in LOD0 (High), LOD1(Medium), or LOD2(Low) quality
|
||||
* Download models
|
||||
* Bug Reports:
|
||||
* View and Resolve bug reports
|
||||
* Logs:
|
||||
* Command: View commands that have been run
|
||||
* Activity: View character activity of entering and exiting worlds
|
||||
* Audit:
|
||||
* View moderation activity (characters, pets, properties)
|
||||
* View GM Level changes
|
||||
* View Send Mail usage
|
||||
* System: View Extra logging of background activities of Nexus Dashboard
|
||||
* Send Mail:
|
||||
* Send Mail to characters
|
||||
* Attach items to Mail
|
||||
* Economy Reports:
|
||||
* Reports are generated as a scheduled background task run every day at 2300 UTC
|
||||
* Accounts with GM Level 3 and above are ignored
|
||||
* Item reports:
|
||||
* Reports numbers of items in existence
|
||||
* Includes backpack and Vault items
|
||||
* Currency:
|
||||
* Reports how much currency that characters posses
|
||||
* U-Score:
|
||||
* Reports how much U-Score that characters posses
|
||||
|
||||
# Deployment
|
||||
|
||||
> **NOTE: This tutorial assumes you have a working DLU server instance and**
|
||||
> **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 \
|
||||
-v /path/to/cachedir:/app/cache:rw \ # optional for persistent cache for conversions
|
||||
aronwk/nexus-dashboard:latest
|
||||
|
||||
-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 \
|
||||
-v /path/to/cachedir:/app/cache:rw \
|
||||
aronwk/nexus-dashboard:latest
|
||||
```
|
||||
|
||||
* /app/luclient must be mapped to the location of an unpacked client
|
||||
* you only need `res/` and `locale/` from the client, but dropping the whole cleint in there won't hurt
|
||||
* `/app/luclient` must be mapped to the location of an unpacked client
|
||||
* you only need `res/` and `locale/` from the client, but dropping the whole client in there won't hurt
|
||||
* Use `fdb_to_sqlite.py` in lcdr's utilities on `res/cdclient.fdb` in the unpacked client to convert the client database to `cdclient.sqlite`
|
||||
* Put teh resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite`
|
||||
* unzip `res/brickdb.zip` in-place
|
||||
* **Docker will do this for you**
|
||||
* you should have new folders and files in the following places:
|
||||
* `res/Assemblies/../..` with a bunch of sub folders
|
||||
* `res/Primitives/../..` with a bunch of sub folders
|
||||
* `res/info.xml`
|
||||
* `res/Materials.xml`
|
||||
* Put the resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite`
|
||||
|
||||
### Environmental Variables
|
||||
* Required:
|
||||
|
||||
Please Reference `app/settings_exmaple.py` to see all the variables
|
||||
|
||||
* Required:
|
||||
* APP_SECRET_KEY (Must be provided)
|
||||
* APP_DATABASE_URI (Must be provided)
|
||||
* Optional
|
||||
* USER_ENABLE_REGISTER (Default: True)
|
||||
* USER_ENABLE_EMAIL (Default: True, Needs Mail to be configured)
|
||||
* USER_ENABLE_CONFIRM_EMAIL (Default: True)
|
||||
* USER_ENABLE_INVITE_USER (Default: False)
|
||||
* USER_REQUIRE_INVITATION (Default: False)
|
||||
* REQUIRE_PLAY_KEY (Default: True)
|
||||
* MAIL_SERVER (Default: smtp.gmail.com)
|
||||
* MAIL_PORT (Default: 587)
|
||||
* MAIL_USE_SSL (Default: False)
|
||||
* MAIL_USE_TLS (Default: True)
|
||||
* MAIL_USERNAME (Default: None)
|
||||
* MAIL_PASSWORD (Default: None)
|
||||
* USER_EMAIL_SENDER_NAME (Default: None)
|
||||
* USER_EMAIL_SENDER_EMAIL (Default: None)
|
||||
* Everything else is optional and has defaults
|
||||
|
||||
## Manual
|
||||
## Manual Linux Installation
|
||||
|
||||
Don't, use Docker /s
|
||||
Thanks to [HailStorm32](https://github.com/HailStorm32) for this manual install guide!
|
||||
|
||||
TODO: Make manual deployment easier to configure
|
||||
### Setting Up The Environment
|
||||
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 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>
|
||||
|
||||
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` 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 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 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`, fill in any random 32 character string
|
||||
For `APP_DATABASE_URI`, fill in the respective fields
|
||||
```
|
||||
<username> --> database username
|
||||
<password> --> database password
|
||||
<host> --> database address
|
||||
(this will most likely be localhost if you are running the database on the same machine
|
||||
<port> --> port number of the database
|
||||
(this can most likely be left out if you are running the database on the same machine)
|
||||
<database> --> database name
|
||||
```
|
||||
>*If you are omitting `<port>`, make sure to also omit the `:`*
|
||||
|
||||
For a configuration where the database is running on the same machine, it would similar to this
|
||||
```
|
||||
APP_SECRET_KEY = "abcdefghijklmnopqrstuvwxyz123456"
|
||||
APP_DATABASE_URI = "mysql+pymysql://DBusername:DBpassword@localhost/DBname"
|
||||
```
|
||||
The rest of the file is left at the default values
|
||||
|
||||
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 `~/NexusDashboard/app/luclient`
|
||||
|
||||
Unzip the `brickdb.zip` in place
|
||||
`unzip brickdb.zip`
|
||||
|
||||
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
|
||||
```
|
||||
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 `~/NexusDashboard/app/luclient/res` folder
|
||||
|
||||
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
|
||||
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`
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
166
app/__init__.py
166
app/__init__.py
@@ -7,13 +7,26 @@ 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
|
||||
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 query_cdclient, register_luclient_jinja_helpers
|
||||
from app.luclient import register_luclient_jinja_helpers
|
||||
import pathlib
|
||||
|
||||
from app.commands import init_db, init_accounts
|
||||
from app.models import Account, AccountInvitation
|
||||
from app.commands import (
|
||||
init_db,
|
||||
init_accounts,
|
||||
load_property,
|
||||
gen_image_cache,
|
||||
gen_model_cache,
|
||||
fix_clone_ids
|
||||
)
|
||||
from app.models import Account, AccountInvitation, AuditLog
|
||||
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
# Instantiate Flask extensions
|
||||
csrf_protect = CSRFProtect()
|
||||
@@ -22,7 +35,6 @@ scheduler = APScheduler()
|
||||
|
||||
|
||||
def create_app():
|
||||
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
# decrement uses on a play key after a successful registration
|
||||
@@ -33,14 +45,24 @@ def create_app():
|
||||
play_key_used = PlayKey.query.filter(PlayKey.id == user.play_key_id).first()
|
||||
play_key_used.key_uses = play_key_used.key_uses - 1
|
||||
play_key_used.times_used = play_key_used.times_used + 1
|
||||
app.logger.info(
|
||||
f"USERS::REGISTRATION User with ID {user.id} and name {user.username} Registered \
|
||||
using Play Key ID {play_key_used.id} : {play_key_used.key_string}"
|
||||
)
|
||||
db.session.add(play_key_used)
|
||||
db.session.commit()
|
||||
else:
|
||||
app.logger.info(f"USERS::REGISTRATION User with ID {user.id} and name {user.username} Registered")
|
||||
|
||||
@user_logged_in.connect_via(app)
|
||||
def _after_login_hook(sender, user, **extra):
|
||||
app.logger.info(f"{user.username} Logged in")
|
||||
|
||||
# A bunch of jinja filters to make things easiers
|
||||
@app.template_filter('ctime')
|
||||
def timectime(s):
|
||||
if s:
|
||||
return time.ctime(s) # or datetime.datetime.fromtimestamp(s)
|
||||
return time.ctime(s) # or datetime.datetime.fromtimestamp(s)
|
||||
else:
|
||||
return "Never"
|
||||
|
||||
@@ -51,21 +73,48 @@ def create_app():
|
||||
else:
|
||||
return 0 & (1 << bit)
|
||||
|
||||
@app.template_filter('debug')
|
||||
def debug(text):
|
||||
print(text)
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_connection(exception):
|
||||
cdclient = getattr(g, '_cdclient', None)
|
||||
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)
|
||||
app.cli.add_command(load_property)
|
||||
app.cli.add_command(gen_image_cache)
|
||||
app.cli.add_command(gen_model_cache)
|
||||
app.cli.add_command(fix_clone_ids)
|
||||
|
||||
register_logging(app)
|
||||
register_settings(app)
|
||||
register_extensions(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
|
||||
|
||||
|
||||
@@ -90,7 +139,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)
|
||||
|
||||
|
||||
@@ -123,8 +172,18 @@ def register_blueprints(app):
|
||||
app.register_blueprint(luclient_blueprint, url_prefix='/luclient')
|
||||
from .reports import reports_blueprint
|
||||
app.register_blueprint(reports_blueprint, url_prefix='/reports')
|
||||
from .api import api_blueprint
|
||||
app.register_blueprint(api_blueprint, url_prefix='/api')
|
||||
|
||||
|
||||
def register_logging(app):
|
||||
# file logger
|
||||
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)
|
||||
app.logger.addHandler(file_handler)
|
||||
|
||||
|
||||
def register_settings(app):
|
||||
"""Register setting from setting and env
|
||||
@@ -134,15 +193,26 @@ def register_settings(app):
|
||||
"""
|
||||
|
||||
# Load common settings
|
||||
app.config.from_object('app.settings')
|
||||
try:
|
||||
app.config.from_object('app.settings')
|
||||
except Exception:
|
||||
app.logger.info("No settings.py, loading from example")
|
||||
app.config.from_object('app.settings_example')
|
||||
|
||||
# Load environment specific settings
|
||||
app.config['TESTING'] = False
|
||||
app.config['DEBUG'] = False
|
||||
|
||||
# always pull these two from the env
|
||||
app.config['SECRET_KEY'] = os.getenv('APP_SECRET_KEY')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('APP_DATABASE_URI')
|
||||
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(
|
||||
@@ -177,14 +247,67 @@ def register_settings(app):
|
||||
"pool_pre_ping": True,
|
||||
"pool_use_lifo": True
|
||||
}
|
||||
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
|
||||
app.config['MAIL_PORT'] = os.getenv('MAIL_USE_SSL', 587)
|
||||
app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', False)
|
||||
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', True)
|
||||
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', None)
|
||||
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', None)
|
||||
app.config['USER_EMAIL_SENDER_NAME'] = os.getenv('USER_EMAIL_SENDER_NAME', None)
|
||||
app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv('USER_EMAIL_SENDER_EMAIL', None)
|
||||
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']
|
||||
)
|
||||
app.config['MAIL_USE_SSL'] = os.getenv(
|
||||
'MAIL_USE_SSL',
|
||||
app.config['MAIL_USE_SSL']
|
||||
)
|
||||
app.config['MAIL_USE_TLS'] = os.getenv(
|
||||
'MAIL_USE_TLS',
|
||||
app.config['MAIL_USE_TLS']
|
||||
)
|
||||
app.config['MAIL_USERNAME'] = os.getenv(
|
||||
'MAIL_USERNAME',
|
||||
app.config['MAIL_USERNAME']
|
||||
)
|
||||
app.config['MAIL_PASSWORD'] = os.getenv(
|
||||
'MAIL_PASSWORD',
|
||||
app.config['MAIL_PASSWORD']
|
||||
)
|
||||
app.config['USER_EMAIL_SENDER_NAME'] = os.getenv(
|
||||
'USER_EMAIL_SENDER_NAME',
|
||||
app.config['USER_EMAIL_SENDER_NAME']
|
||||
)
|
||||
app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv(
|
||||
'USER_EMAIL_SENDER_EMAIL',
|
||||
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']
|
||||
)
|
||||
|
||||
|
||||
|
||||
def gm_level(gm_level):
|
||||
@@ -201,3 +324,10 @@ def gm_level(gm_level):
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def log_audit(message):
|
||||
AuditLog(
|
||||
account_id=current_user.id,
|
||||
action=message
|
||||
).save()
|
||||
|
||||
160
app/accounts.py
160
app/accounts.py
@@ -1,18 +1,34 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, current_app, flash
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, current_app, flash
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import bcrypt
|
||||
import datetime
|
||||
import time
|
||||
from app.models import Account, AccountInvitation, db
|
||||
import secrets
|
||||
from app.models import (
|
||||
Account,
|
||||
CharacterInfo,
|
||||
ActivityLog,
|
||||
Leaderboard,
|
||||
Mail,
|
||||
Property,
|
||||
PropertyContent,
|
||||
UGC,
|
||||
AuditLog,
|
||||
BugReport,
|
||||
AccountInvitation,
|
||||
db,
|
||||
Friends
|
||||
)
|
||||
from app.schemas import AccountSchema
|
||||
from app import gm_level
|
||||
from app.forms import EditGMLevelForm
|
||||
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)
|
||||
@@ -38,7 +54,7 @@ def edit_gm_level(id):
|
||||
if current_user.id == int(id):
|
||||
flash("You cannot your own GM Level", "danger")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
account_data = Account.query.filter(Account.id==id).first()
|
||||
account_data = Account.query.filter(Account.id == id).first()
|
||||
if account_data.gm_level >= 8 and current_user.gm_level == 8:
|
||||
flash("You cannot edit this user's GM Level", "warning")
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
@@ -46,8 +62,10 @@ def edit_gm_level(id):
|
||||
form = EditGMLevelForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
log_audit(f"Changed ({account_data.id}){account_data.username}'s GM Level from {account_data.gm_level} to {form.gm_level.data}")
|
||||
account_data.gm_level = form.gm_level.data
|
||||
account_data.save()
|
||||
|
||||
return redirect(url_for('accounts.view', id=account_data.id))
|
||||
|
||||
form.gm_level.data = account_data.gm_level
|
||||
@@ -55,17 +73,38 @@ def edit_gm_level(id):
|
||||
return render_template('accounts/edit_gm_level.html.j2', form=form, username=account_data.username)
|
||||
|
||||
|
||||
@accounts_blueprint.route('/edit_email/<id>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def edit_email(id):
|
||||
account_data = Account.query.filter(Account.id == id).first()
|
||||
form = EditEmailForm()
|
||||
if form.validate_on_submit():
|
||||
log_audit(f"Changed ({account_data.id}){account_data.username}'s Email from {account_data.email} to {form.email.data}")
|
||||
account_data.email = form.email.data
|
||||
account_data.save()
|
||||
return redirect(url_for('accounts.view', id=account_data.id))
|
||||
|
||||
form.email.data = account_data.email
|
||||
return render_template('accounts/edit_email.html.j2', form=form, username=account_data.username)
|
||||
|
||||
|
||||
@accounts_blueprint.route('/lock/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def lock(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
account.locked = not account.locked
|
||||
account.save()
|
||||
if account.locked:
|
||||
if not account.locked:
|
||||
account.locked = True
|
||||
account.active = False
|
||||
log_audit(f"Locked ({account.id}){account.username}")
|
||||
flash("Locked Account", "danger")
|
||||
else:
|
||||
account.locked = False
|
||||
account.active = True
|
||||
log_audit(f"Unlocked ({account.id}){account.username}")
|
||||
flash("Unlocked account", "success")
|
||||
account.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@@ -74,12 +113,17 @@ def lock(id):
|
||||
@gm_level(3)
|
||||
def ban(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
account.banned = not account.banned
|
||||
account.save()
|
||||
if account.banned:
|
||||
if not account.banned:
|
||||
account.banned = True
|
||||
account.active = False
|
||||
log_audit(f"Banned ({account.id}){account.username}")
|
||||
flash("Banned Account", "danger")
|
||||
else:
|
||||
account.banned = False
|
||||
account.active = True
|
||||
log_audit(f"Unbanned ({account.id}){account.username}")
|
||||
flash("Unbanned account", "success")
|
||||
account.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@@ -90,16 +134,93 @@ def mute(id, days=0):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
if days == "0":
|
||||
account.mute_expire = 0
|
||||
log_audit(f"Unmuted ({account.id}){account.username}")
|
||||
flash("Unmuted Account", "success")
|
||||
else:
|
||||
muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days))
|
||||
account.mute_expire = muted_intil.timestamp()
|
||||
log_audit(f"Muted ({account.id}){account.username} for {days} days")
|
||||
flash(f"Muted account for {days} days", "danger")
|
||||
account.save()
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@accounts_blueprint.route('/delete/<id>/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
def delete(id):
|
||||
account = Account.query.filter(Account.id == id).first()
|
||||
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()
|
||||
for activity in activities:
|
||||
activity.delete()
|
||||
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()
|
||||
for mail in mails:
|
||||
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()
|
||||
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.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()
|
||||
for invite in invites:
|
||||
invite.delete()
|
||||
account.delete()
|
||||
flash(message, "danger")
|
||||
log_audit(message)
|
||||
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)
|
||||
@@ -130,10 +251,10 @@ def get():
|
||||
View
|
||||
</a>
|
||||
"""
|
||||
# <a role="button" class="btn btn-danger btn btn-block"
|
||||
# href='{url_for('acounts.delete', id=account["0"])}'>
|
||||
# Delete
|
||||
# </a>
|
||||
# <a role="button" class="btn btn-danger btn btn-block"
|
||||
# href='{url_for('acounts.delete', id=account["0"])}'>
|
||||
# Delete
|
||||
# </a>
|
||||
|
||||
if account["4"]:
|
||||
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
@@ -146,13 +267,13 @@ def get():
|
||||
account["5"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if account["6"]:
|
||||
account["6"] = f'''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
account["6"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
if current_app.config["USER_ENABLE_EMAIL"]:
|
||||
if account["8"]:
|
||||
account["8"] = f'''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
account["8"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
else:
|
||||
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
|
||||
else:
|
||||
@@ -167,4 +288,3 @@ def get():
|
||||
del account["8"]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
27
app/api.py
Normal file
27
app/api.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from flask import Blueprint, current_app, request
|
||||
|
||||
api_blueprint = Blueprint('api', __name__)
|
||||
|
||||
|
||||
@api_blueprint.route('/web', methods=['GET', 'POST'])
|
||||
def web():
|
||||
current_app.logger.info(f"API::WEB [DATA] {request.data}")
|
||||
return
|
||||
|
||||
|
||||
@api_blueprint.route('/game', methods=['GET', 'POST'])
|
||||
def game():
|
||||
current_app.logger.info(f"API::GAME [DATA] {request.data}")
|
||||
return
|
||||
|
||||
|
||||
@api_blueprint.route('/game_content', methods=['GET', 'POST'])
|
||||
def game_content():
|
||||
current_app.logger.info(f"API::GAME CONTENT [DATA] {request.data}")
|
||||
return
|
||||
|
||||
|
||||
@api_blueprint.route('/metrics_data_service', methods=['GET', 'POST'])
|
||||
def metrics_data_service():
|
||||
current_app.logger.info(f"API::METRICS DATA SERVICE [DATA] {request.data}")
|
||||
return
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, flash, redirect
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, BugReport, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
@@ -8,22 +8,28 @@ from app.luclient import translate_from_locale
|
||||
|
||||
bug_report_blueprint = Blueprint('bug_reports', __name__)
|
||||
|
||||
|
||||
@bug_report_blueprint.route('/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index(status):
|
||||
return render_template('bug_reports/index.html.j2', status=status)
|
||||
|
||||
|
||||
@bug_report_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def view(id):
|
||||
report = BugReport.query.filter(BugReport.id == id).first()
|
||||
if current_user.gm_level < 3:
|
||||
chars = CharacterInfo.query.with_entities(CharacterInfo.id).filter(CharacterInfo.account_id == current_user.id).all()
|
||||
char_ids = []
|
||||
for char in chars:
|
||||
char_ids.append(char[0])
|
||||
if report.reporter_id not in char_ids:
|
||||
return redirect(url_for('bug_reports.index', status=all))
|
||||
if report.resoleved_by:
|
||||
rb = report.resoleved_by.username
|
||||
else:
|
||||
rb=""
|
||||
rb = ""
|
||||
return render_template('bug_reports/view.html.j2', report=report, resolved_by=rb)
|
||||
|
||||
|
||||
@@ -49,27 +55,37 @@ def resolve(id):
|
||||
|
||||
@bug_report_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def get(status):
|
||||
columns = [
|
||||
ColumnDT(BugReport.id), # 0
|
||||
ColumnDT(BugReport.body), # 1
|
||||
ColumnDT(BugReport.client_version), # 2
|
||||
ColumnDT(BugReport.other_player_id), # 3
|
||||
ColumnDT(BugReport.selection), # 4
|
||||
ColumnDT(BugReport.submitted), # 5
|
||||
ColumnDT(BugReport.resolved_time), # 6
|
||||
ColumnDT(BugReport.reporter_id), # 1
|
||||
ColumnDT(BugReport.body), # 2
|
||||
ColumnDT(BugReport.client_version), # 3
|
||||
ColumnDT(BugReport.other_player_id), # 4
|
||||
ColumnDT(BugReport.selection), # 5
|
||||
ColumnDT(BugReport.submitted), # 6
|
||||
ColumnDT(BugReport.resolved_time), # 7
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(BugReport)
|
||||
elif status=="resolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None)
|
||||
elif status=="unresolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None)
|
||||
if current_user.gm_level > 0:
|
||||
if status == "resolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None)
|
||||
elif status == "unresolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None)
|
||||
else:
|
||||
query = db.session.query().select_from(BugReport)
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
chars = CharacterInfo.query.with_entities(CharacterInfo.id).filter(CharacterInfo.account_id == current_user.id).all()
|
||||
char_ids = []
|
||||
for char in chars:
|
||||
char_ids.append(char[0])
|
||||
if status == "resolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids)).filter(BugReport.resolved_time != None)
|
||||
elif status == "unresolved":
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids)).filter(BugReport.resolved_time == None)
|
||||
else:
|
||||
query = db.session.query().select_from(BugReport).filter(BugReport.reporter_id.in_(char_ids))
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
@@ -85,31 +101,46 @@ def get(status):
|
||||
</a>
|
||||
"""
|
||||
|
||||
if not report["6"]:
|
||||
if report["7"] is None:
|
||||
report["0"] += f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('bug_reports.resolve', id=id)}'>
|
||||
Resolve
|
||||
</a>
|
||||
"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('bug_reports.resolve', id=id)}'>
|
||||
Resolve
|
||||
</a>
|
||||
"""
|
||||
report["7"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
|
||||
|
||||
if report["3"] == "0":
|
||||
report["3"] = "None"
|
||||
if not report["1"]:
|
||||
report["1"] = "None"
|
||||
else:
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["3"]) & 0xFFFFFFFF).first()
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["1"])).first()
|
||||
if character:
|
||||
report["3"] = f"""
|
||||
report["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=(int(report["3"]) & 0xFFFFFFFF))}'>
|
||||
href='{url_for('characters.view', id=report['1'])}'>
|
||||
{character.name}
|
||||
</a>
|
||||
"""
|
||||
else:
|
||||
report["3"] = "Player Deleted"
|
||||
report["1"] = "Player Deleted"
|
||||
|
||||
report["4"] = translate_from_locale(report["4"][2:-1])
|
||||
if report["4"] == "0":
|
||||
report["4"] = "None"
|
||||
else:
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == int(report["4"]) & 0xFFFFFFFF).first()
|
||||
if character:
|
||||
if current_user.gm_level > 3:
|
||||
report["4"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=(int(report["4"]) & 0xFFFFFFFF))}'>
|
||||
{character.name}
|
||||
</a>
|
||||
"""
|
||||
else:
|
||||
report["4"] = character.name
|
||||
else:
|
||||
report["4"] = "Player Deleted"
|
||||
|
||||
if not report["6"]:
|
||||
report["6"] = '''<h1 class="far fa-times-circle text-danger"></h1>'''
|
||||
report["5"] = translate_from_locale(report["5"][2:-1])
|
||||
|
||||
return data
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, make_response, current_app
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import datetime, time
|
||||
import time
|
||||
from app.models import CharacterInfo, CharacterXML, Account, db
|
||||
from app.schemas import CharacterInfoSchema
|
||||
from app import gm_level
|
||||
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
|
||||
|
||||
|
||||
character_blueprint = Blueprint('characters', __name__)
|
||||
|
||||
character_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@character_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -23,21 +28,33 @@ def index():
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def approve_name(id, action):
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
character = CharacterInfo.query.filter(CharacterInfo.id == id).first()
|
||||
|
||||
if action == "approve":
|
||||
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
|
||||
flash(
|
||||
f"Approved name {character.name}",
|
||||
"success"
|
||||
)
|
||||
|
||||
elif action == "rename":
|
||||
character.needs_rename = True
|
||||
log_audit(
|
||||
f"Marked character ({character.id}){character.name} \
|
||||
(Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename")
|
||||
flash(
|
||||
f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
|
||||
f"Marked character {character.name} \
|
||||
(Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename",
|
||||
"danger"
|
||||
)
|
||||
|
||||
@@ -60,11 +77,11 @@ def view(id):
|
||||
abort(403)
|
||||
return
|
||||
character_json = xmltodict.parse(
|
||||
CharacterXML.query.filter(
|
||||
CharacterXML.id==id
|
||||
).first().xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
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:
|
||||
@@ -75,10 +92,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',
|
||||
@@ -87,6 +104,58 @@ def view(id):
|
||||
)
|
||||
|
||||
|
||||
@character_blueprint.route('/view_xml/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view_xml(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_xml = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data.replace("\"stt=", "\" stt=")
|
||||
|
||||
response = make_response(character_xml)
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
return response
|
||||
|
||||
|
||||
@character_blueprint.route('/get_xml/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_xml(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_xml = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first().xml_data
|
||||
|
||||
response = make_response(character_xml)
|
||||
response.headers.set('Content-Type', 'attachment/xml')
|
||||
response.headers.set(
|
||||
'Content-Disposition',
|
||||
'attachment',
|
||||
filename=f"{character_data.name}.xml"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@character_blueprint.route('/restrict/<bit>/<id>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -103,12 +172,76 @@ def restrict(id, bit):
|
||||
abort(404)
|
||||
return
|
||||
|
||||
log_audit(f"Updated ({character_data.id}){character_data.name}'s permission map to \
|
||||
{character_data.permission_map ^ (1 << int(bit))} from {character_data.permission_map}")
|
||||
|
||||
character_data.permission_map ^= (1 << int(bit))
|
||||
character_data.save()
|
||||
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
|
||||
@character_blueprint.route('/rescue/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def rescue(id):
|
||||
|
||||
form = RescueForm()
|
||||
|
||||
character_data = CharacterXML.query.filter(
|
||||
CharacterXML.id == id
|
||||
).first()
|
||||
|
||||
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(
|
||||
(
|
||||
zone.attrib["w"],
|
||||
translate_from_locale(f"ZoneTable_{zone.attrib['w']}_DisplayDescription")
|
||||
)
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
new_zone = character_xml.find(f'.//r[@w="{form.save_world.data}"]')
|
||||
char = character_xml.find(".//char")
|
||||
char.attrib["lzx"] = new_zone.attrib["x"]
|
||||
char.attrib["lzy"] = new_zone.attrib["y"]
|
||||
char.attrib["lzz"] = new_zone.attrib["z"]
|
||||
char.attrib["lwid"] = form.save_world.data
|
||||
|
||||
character_data.xml_data = ET.tostring(character_xml)
|
||||
character_data.save()
|
||||
|
||||
return redirect(url_for('characters.view', id=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 = character_data.xml_data
|
||||
return render_template("character/upload.html.j2", form=form)
|
||||
|
||||
|
||||
@character_blueprint.route('/get/<status>', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -124,14 +257,12 @@ def get(status):
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account)
|
||||
elif status=="approved":
|
||||
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))
|
||||
elif status == "unapproved":
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") & (CharacterInfo.needs_rename == False))
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
query = db.session.query().select_from(CharacterInfo).join(Account)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
@@ -190,6 +321,4 @@ def get(status):
|
||||
if perm_map & (1 << 6):
|
||||
character["6"] += "Restricted Chat</br>"
|
||||
|
||||
|
||||
return data
|
||||
|
||||
|
||||
225
app/commands.py
225
app/commands.py
@@ -1,10 +1,21 @@
|
||||
import click
|
||||
import json
|
||||
from flask.cli import with_appcontext
|
||||
import random, string, datetime
|
||||
import random
|
||||
import string
|
||||
import datetime
|
||||
from flask_user import current_app
|
||||
from app import db
|
||||
from app.models import Account, PlayKey
|
||||
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
|
||||
import pathlib
|
||||
import zlib
|
||||
from wand import image
|
||||
from wand.exceptions import BlobError as BE
|
||||
import app.pylddlib as ldd
|
||||
from multiprocessing import Pool
|
||||
from functools import partial
|
||||
from sqlalchemy import func
|
||||
import time
|
||||
|
||||
|
||||
@click.command("init_db")
|
||||
@click.argument('drop_tables', nargs=1)
|
||||
@@ -29,16 +40,203 @@ def init_accounts():
|
||||
|
||||
# Add accounts
|
||||
print('Creating Admin account.')
|
||||
admin_account = find_or_create_account(
|
||||
find_or_create_account(
|
||||
'admin',
|
||||
'example@example.com',
|
||||
'Nope',
|
||||
)
|
||||
|
||||
|
||||
return
|
||||
|
||||
|
||||
@click.command("fix_clone_ids")
|
||||
@with_appcontext
|
||||
def fix_clone_ids():
|
||||
"""
|
||||
Fix incorrect prop_clone_id's
|
||||
Remove duplicate properties
|
||||
Either the one with most models or most recently claimed
|
||||
Retuen Pre-built models via mail
|
||||
(May have errors and need to be run multiple times)
|
||||
"""
|
||||
properties = Property.query.all()
|
||||
count = 0
|
||||
for prop in properties:
|
||||
char = CharacterInfo.query.filter(CharacterInfo.id == prop.owner_id).first()
|
||||
if char.prop_clone_id != prop.clone_id:
|
||||
count += 1
|
||||
prop.clone_id = char.prop_clone_id
|
||||
prop.save()
|
||||
|
||||
print(f"Fixed {count} props where clone id did not match owner's clone id")
|
||||
|
||||
dupes = 0
|
||||
characters = CharacterInfo.query.all()
|
||||
for char in characters:
|
||||
props = Property.query.with_entities(
|
||||
Property.zone_id, func.count(Property.zone_id)
|
||||
).group_by(Property.zone_id).filter(
|
||||
Property.owner_id == char.id
|
||||
).all()
|
||||
for prop in props:
|
||||
if prop[1] != 1:
|
||||
dupes += 1
|
||||
print(f"found dupe on {char.name}'s {prop[0]}")
|
||||
dupe_props = Property.query.filter(
|
||||
Property.owner_id == char.id
|
||||
).filter(
|
||||
Property.zone_id == prop[0]).all()
|
||||
dupe_data = []
|
||||
# id, content_count
|
||||
for dprop in dupe_props:
|
||||
dupe_data.append(
|
||||
[
|
||||
dprop.id,
|
||||
PropertyContent.query.filter(PropertyContent.property_id == dprop.id).count(),
|
||||
dprop.time_claimed
|
||||
]
|
||||
)
|
||||
max_models = max(dupe_data, key=lambda x: x[1])
|
||||
if max_models[1] == 0:
|
||||
newest = max(dupe_data, key=lambda x: x[2])
|
||||
for data in dupe_data:
|
||||
if data[2] != newest[2]:
|
||||
Property.query.filter(Property.id == data[0]).first().delete()
|
||||
else:
|
||||
for data in dupe_data:
|
||||
if data[1] != max_models[1]:
|
||||
contents = PropertyContent.query.filter(PropertyContent.property_id == dprop.id).all()
|
||||
if contents:
|
||||
for content in contents:
|
||||
if content.lot == 14:
|
||||
UGC.query.filter(content.ugc_id).first().delete()
|
||||
content.delete()
|
||||
else:
|
||||
Mail(
|
||||
sender_id=0,
|
||||
sender_name="System",
|
||||
receiver_id=char.id,
|
||||
receiver_name=char.name,
|
||||
time_sent=time.time(),
|
||||
subject="Returned Model",
|
||||
body="This model was returned to you from a property cleanup script",
|
||||
attachment_id=0,
|
||||
attachment_lot=content.lot,
|
||||
attachment_count=1
|
||||
).save()
|
||||
content.delete()
|
||||
time.sleep(1)
|
||||
Property.query.filter(Property.id == data[0]).first().delete()
|
||||
return
|
||||
|
||||
|
||||
@click.command("load_property")
|
||||
@click.argument('zone')
|
||||
@click.argument('player')
|
||||
@with_appcontext
|
||||
def load_property(zone, player):
|
||||
"""shoves property data into db"""
|
||||
char = CharacterInfo.query.filter(CharacterInfo.name == player).first()
|
||||
if not char:
|
||||
print("Character not Found")
|
||||
return 404
|
||||
|
||||
prop = Property.query.filter(Property.owner_id == char.id).filter(Property.zone_id == zone).first()
|
||||
|
||||
if not prop:
|
||||
print(f"Property {zone} not claimed by Character: {char.name}")
|
||||
return 404
|
||||
|
||||
prop_files = pathlib.Path('property_files/')
|
||||
for i in prop_files.glob('**/*'):
|
||||
if i.suffix == '.lxfml':
|
||||
lxfml = ""
|
||||
with open(i, "r") as file:
|
||||
lxfml = file.read()
|
||||
compressed_lxfml = zlib.compress(lxfml.encode())
|
||||
|
||||
new_ugc = UGC(
|
||||
account_id=char.account_id,
|
||||
character_id=char.id,
|
||||
is_optimized=0,
|
||||
lxfml=compressed_lxfml,
|
||||
bake_ao=0,
|
||||
filename=i.name
|
||||
)
|
||||
new_ugc.save()
|
||||
|
||||
new_prop_content = PropertyContent(
|
||||
id=i.stem,
|
||||
property_id=prop.id,
|
||||
ugc_id=new_ugc.id,
|
||||
lot=14,
|
||||
x=0,
|
||||
y=0,
|
||||
z=0,
|
||||
rx=0,
|
||||
ry=0,
|
||||
rz=0,
|
||||
rw=1
|
||||
)
|
||||
new_prop_content.save()
|
||||
|
||||
|
||||
@click.command("gen_image_cache")
|
||||
def gen_image_cache():
|
||||
"""generates image cache"""
|
||||
luclient = pathlib.Path('app/luclient/res')
|
||||
files = [path for path in luclient.rglob("*.dds") if path.is_file()]
|
||||
|
||||
for file in files:
|
||||
cache = get_cache_file(file).with_suffix(".png")
|
||||
if not cache.exists():
|
||||
try:
|
||||
print(f"Convert {file.as_posix()} to {cache}")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
with image.Image(filename=str(file.as_posix())) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=str(cache.as_posix()))
|
||||
except BE:
|
||||
return print(f"Error on {file}")
|
||||
|
||||
|
||||
@click.command("gen_model_cache")
|
||||
def gen_model_cache():
|
||||
"""generate model obj cache"""
|
||||
luclient = pathlib.Path('app/luclient/res')
|
||||
files = [path for path in luclient.rglob("*.lxfml") if path.is_file()]
|
||||
pool = Pool(processes=4)
|
||||
pool.map(partial(convert_lxfml_to_obj, lod=0), files)
|
||||
pool.map(partial(convert_lxfml_to_obj, lod=1), files)
|
||||
pool.map(partial(convert_lxfml_to_obj, lod=2), files)
|
||||
|
||||
|
||||
def convert_lxfml_to_obj(file, lod):
|
||||
mtl = get_cache_file(file).with_suffix(f".lod{lod}.mtl")
|
||||
if not mtl.exists():
|
||||
mtl.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(f"Convert LXFML {file.as_posix()} to obj and mtl @ {mtl}")
|
||||
try:
|
||||
ldd.main(str(file.as_posix()), str(mtl.with_suffix("").as_posix()), lod) # convert to OBJ
|
||||
except Exception as e:
|
||||
print(f"ERROR on {file}:\n {e}")
|
||||
else:
|
||||
# print(f"Already Exists: {file} with LOD {lod}")
|
||||
return
|
||||
|
||||
|
||||
def get_cache_file(path):
|
||||
"""helper"""
|
||||
# convert to list so that we can change elements
|
||||
parts = list(path.parts)
|
||||
|
||||
# replace part that matches src with dst
|
||||
parts[parts.index("luclient")] = "cache"
|
||||
del parts[parts.index("res")]
|
||||
|
||||
return pathlib.Path(*parts)
|
||||
|
||||
|
||||
def find_or_create_account(name, email, password, gm_level=9):
|
||||
""" Find existing account or create new account """
|
||||
account = Account.query.filter(Account.email == email).first()
|
||||
@@ -56,15 +254,16 @@ def find_or_create_account(name, email, password, gm_level=9):
|
||||
db.session.commit()
|
||||
|
||||
play_key = PlayKey.query.filter(PlayKey.key_string == key).first()
|
||||
account = Account(email=email,
|
||||
username=name,
|
||||
password=current_app.user_manager.password_manager.hash_password(password),
|
||||
play_key_id=play_key.id,
|
||||
email_confirmed_at=datetime.datetime.utcnow(),
|
||||
gm_level=gm_level
|
||||
)
|
||||
account = Account(
|
||||
email=email,
|
||||
username=name,
|
||||
password=current_app.user_manager.password_manager.hash_password(password),
|
||||
play_key_id=play_key.id,
|
||||
email_confirmed_at=datetime.datetime.utcnow(),
|
||||
gm_level=gm_level
|
||||
)
|
||||
play_key.key_uses = 0
|
||||
db.session.add(account)
|
||||
db.session.add(play_key)
|
||||
db.session.commit()
|
||||
return # account
|
||||
return # account
|
||||
|
||||
64
app/forms.py
64
app/forms.py
@@ -16,13 +16,13 @@ from wtforms import (
|
||||
SubmitField,
|
||||
validators,
|
||||
IntegerField,
|
||||
StringField,
|
||||
SelectField
|
||||
)
|
||||
|
||||
from wtforms.validators import DataRequired, Optional
|
||||
from app.models import PlayKey
|
||||
|
||||
|
||||
def validate_play_key(form, field):
|
||||
"""Validates a field for a valid phone number
|
||||
Args:
|
||||
@@ -75,16 +75,19 @@ class CustomRegisterForm(FlaskForm):
|
||||
|
||||
password = PasswordField('Password', validators=[
|
||||
DataRequired(),
|
||||
password_validator
|
||||
password_validator,
|
||||
validators.length(max=40, message="The maximum length of the password is 40 characters due to game client limitations")
|
||||
])
|
||||
retype_password = PasswordField('Retype Password', validators=[
|
||||
validators.EqualTo('password', message='Passwords did not match')
|
||||
validators.EqualTo('password', message='Passwords did not match'),
|
||||
validators.length(max=40, message="The maximum length of the password is 40 characters due to game client limitations")
|
||||
])
|
||||
|
||||
invite_token = HiddenField('Token')
|
||||
|
||||
submit = SubmitField('Register')
|
||||
|
||||
|
||||
class CreatePlayKeyForm(FlaskForm):
|
||||
|
||||
count = IntegerField(
|
||||
@@ -97,6 +100,7 @@ class CreatePlayKeyForm(FlaskForm):
|
||||
)
|
||||
submit = SubmitField('Create!')
|
||||
|
||||
|
||||
class EditPlayKeyForm(FlaskForm):
|
||||
|
||||
active = BooleanField(
|
||||
@@ -119,12 +123,24 @@ class EditGMLevelForm(FlaskForm):
|
||||
|
||||
gm_level = IntegerField(
|
||||
'GM Level',
|
||||
widget=NumberInput(min = 0, max = 9)
|
||||
widget=NumberInput(min=0, max=9)
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class EditEmailForm(FlaskForm):
|
||||
email = StringField(
|
||||
'E-Mail',
|
||||
validators=[
|
||||
Optional(),
|
||||
validators.Email('Invalid email address'),
|
||||
unique_email_validator,
|
||||
]
|
||||
)
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class ResolveBugReportForm(FlaskForm):
|
||||
|
||||
resolution = StringField(
|
||||
@@ -142,8 +158,8 @@ class SendMailForm(FlaskForm):
|
||||
'Recipient: ',
|
||||
coerce=str,
|
||||
choices=[
|
||||
("",""),
|
||||
("0","All Characters"),
|
||||
("", ""),
|
||||
("0", "All Characters"),
|
||||
],
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
@@ -162,7 +178,7 @@ class SendMailForm(FlaskForm):
|
||||
attachment = SelectField(
|
||||
"Attachment",
|
||||
coerce=str,
|
||||
choices=[(0,"No Attachment")]
|
||||
choices=[(0, "No Attachment")]
|
||||
)
|
||||
|
||||
attachment_count = IntegerField(
|
||||
@@ -171,3 +187,37 @@ class SendMailForm(FlaskForm):
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class RescueForm(FlaskForm):
|
||||
|
||||
save_world = SelectField(
|
||||
'Move to:',
|
||||
coerce=str,
|
||||
choices=[
|
||||
("", ""),
|
||||
],
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class RejectPropertyForm(FlaskForm):
|
||||
rejection_reason = StringField(
|
||||
'Rejection Reason',
|
||||
widget=TextArea(),
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
|
||||
class CharXMLUploadForm(FlaskForm):
|
||||
char_xml = StringField(
|
||||
'Paste minified charxml here:',
|
||||
widget=TextArea(),
|
||||
validators=[validators.DataRequired()]
|
||||
)
|
||||
|
||||
submit = SubmitField('Submit')
|
||||
|
||||
65
app/log.py
65
app/log.py
@@ -1,12 +1,13 @@
|
||||
from flask import render_template, Blueprint, request, url_for
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo
|
||||
from flask_user import login_required
|
||||
from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo, AuditLog
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app import gm_level
|
||||
|
||||
log_blueprint = Blueprint('log', __name__)
|
||||
|
||||
|
||||
@log_blueprint.route('/activities', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
@@ -21,16 +22,30 @@ def command():
|
||||
return render_template('logs/command.html.j2')
|
||||
|
||||
|
||||
@log_blueprint.route('/system')
|
||||
@gm_level(8)
|
||||
def system():
|
||||
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)
|
||||
|
||||
|
||||
@log_blueprint.route('/audits')
|
||||
@gm_level(8)
|
||||
def audit():
|
||||
return render_template('logs/audit.html.j2')
|
||||
|
||||
|
||||
@log_blueprint.route('/get_activities', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def get_activities():
|
||||
columns = [
|
||||
ColumnDT(ActivityLog.id), # 0
|
||||
ColumnDT(ActivityLog.character_id), # 1
|
||||
ColumnDT(ActivityLog.activity), # 2
|
||||
ColumnDT(ActivityLog.time), # 3
|
||||
ColumnDT(ActivityLog.map_id), # 4
|
||||
ColumnDT(ActivityLog.id), # 0
|
||||
ColumnDT(ActivityLog.character_id), # 1
|
||||
ColumnDT(ActivityLog.activity), # 2
|
||||
ColumnDT(ActivityLog.time), # 3
|
||||
ColumnDT(ActivityLog.map_id), # 4
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(ActivityLog)
|
||||
@@ -68,9 +83,9 @@ def get_activities():
|
||||
@gm_level(8)
|
||||
def get_commands():
|
||||
columns = [
|
||||
ColumnDT(CommandLog.id), # 0
|
||||
ColumnDT(CommandLog.character_id), # 1
|
||||
ColumnDT(CommandLog.command), # 2
|
||||
ColumnDT(CommandLog.id), # 0
|
||||
ColumnDT(CommandLog.character_id), # 1
|
||||
ColumnDT(CommandLog.command), # 2
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(CommandLog)
|
||||
@@ -96,3 +111,33 @@ def get_commands():
|
||||
"""
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@log_blueprint.route('/get_audits', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(8)
|
||||
def get_audits():
|
||||
columns = [
|
||||
ColumnDT(AuditLog.id), # 0
|
||||
ColumnDT(AuditLog.account_id), # 1
|
||||
ColumnDT(AuditLog.action), # 2
|
||||
ColumnDT(AuditLog.date), # 2
|
||||
]
|
||||
|
||||
query = db.session.query().select_from(AuditLog)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
for audit in data["data"]:
|
||||
char_id = audit["1"]
|
||||
audit["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('accounts.view', id=char_id)}'>
|
||||
{Account.query.filter(Account.id==audit['1']).first().username}
|
||||
</a>
|
||||
"""
|
||||
|
||||
return data
|
||||
|
||||
213
app/luclient.py
213
app/luclient.py
@@ -3,7 +3,10 @@ from flask import (
|
||||
send_file,
|
||||
g,
|
||||
redirect,
|
||||
url_for
|
||||
url_for,
|
||||
make_response,
|
||||
abort,
|
||||
current_app
|
||||
)
|
||||
from flask_user import login_required
|
||||
from app.models import CharacterInfo
|
||||
@@ -11,6 +14,8 @@ import glob
|
||||
import os
|
||||
from wand import image
|
||||
from wand.exceptions import BlobError as BE
|
||||
import pathlib
|
||||
import json
|
||||
|
||||
import sqlite3
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -18,6 +23,7 @@ import xml.etree.ElementTree as ET
|
||||
luclient_blueprint = Blueprint('luclient', __name__)
|
||||
locale = {}
|
||||
|
||||
|
||||
@luclient_blueprint.route('/get_dds_as_png/<filename>')
|
||||
@login_required
|
||||
def get_dds_as_png(filename):
|
||||
@@ -26,8 +32,8 @@ def get_dds_as_png(filename):
|
||||
|
||||
cache = f'cache/{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
if not os.path.exists(cache):
|
||||
root = f"{current_app.config['CLIENT_LOCATION']}res/"
|
||||
|
||||
path = glob.glob(
|
||||
root + f'**/{filename}',
|
||||
@@ -36,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)
|
||||
|
||||
@@ -47,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}',
|
||||
@@ -60,34 +66,43 @@ 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('select icon_asset from RenderComponent where id = ?',
|
||||
filename = query_cdclient(
|
||||
'select icon_asset from RenderComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)[0]
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
if filename:
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
else:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
if not os.path.exists(cache):
|
||||
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:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
img.save(filename=cache)
|
||||
except BE:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
return send_file(cache)
|
||||
return send_file(pathlib.Path(cache).resolve())
|
||||
|
||||
|
||||
@luclient_blueprint.route('/get_icon_iconid/<id>')
|
||||
@@ -102,36 +117,76 @@ def get_icon_iconid(id):
|
||||
|
||||
filename = filename.replace("..\\", "").replace("\\", "/")
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
if not os.path.exists(cache):
|
||||
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:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
img.save(filename=cache)
|
||||
except BE:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
return send_file(cache)
|
||||
return send_file(pathlib.Path(cache).resolve())
|
||||
|
||||
|
||||
@luclient_blueprint.route('/ldddb/')
|
||||
@login_required
|
||||
def brick_list():
|
||||
brick_list = []
|
||||
if len(brick_list) == 0:
|
||||
suffixes = [".g", ".g1", ".g2", ".g3", ".xml"]
|
||||
res = pathlib.Path(f"{current_app.config['CLIENT_LOCATION']}res/")
|
||||
# Load g files
|
||||
for path in res.rglob("*.*"):
|
||||
if str(path.suffix) in suffixes:
|
||||
brick_list.append(
|
||||
{
|
||||
"type": "file",
|
||||
"name": str(path.as_posix()).replace("{current_app.config['CLIENT_LOCATION']}res/", "")
|
||||
}
|
||||
)
|
||||
response = make_response(json.dumps(brick_list))
|
||||
response.headers.set('Content-Type', 'application/json')
|
||||
return response
|
||||
|
||||
|
||||
@luclient_blueprint.route('/ldddb/', defaults={'req_path': ''})
|
||||
@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"{current_app.config['CLIENT_LOCATION']}res/{req_path}").resolve()))
|
||||
# Return 404 if path doesn't exist
|
||||
if not rel_path.exists():
|
||||
return abort(404)
|
||||
|
||||
# Check if path is a file and serve
|
||||
if rel_path.is_file():
|
||||
return send_file(rel_path)
|
||||
else:
|
||||
return abort(404)
|
||||
|
||||
|
||||
@luclient_blueprint.route('/unknown')
|
||||
@login_required
|
||||
def unknown():
|
||||
filename = "textures/ui/inventory/unknown.dds"
|
||||
|
||||
cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png'
|
||||
cache = f'{current_app.config["CACHE_LOCATION"]}{filename.split(".")[0]}.png'
|
||||
|
||||
if not os.path.exists("app/" + cache):
|
||||
root = 'app/luclient/res/'
|
||||
if not os.path.exists(cache):
|
||||
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:
|
||||
img.compression = "no"
|
||||
img.save(filename=cache)
|
||||
except BE:
|
||||
return redirect(url_for('luclient.unknown'))
|
||||
|
||||
with image.Image(filename=f'{root}{filename}'.lower()) as img:
|
||||
img.compression = "no"
|
||||
img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png')
|
||||
|
||||
|
||||
return send_file(cache)
|
||||
return send_file(pathlib.Path(cache).resolve())
|
||||
|
||||
|
||||
def get_cdclient():
|
||||
@@ -142,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
|
||||
|
||||
|
||||
@@ -174,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()
|
||||
@@ -192,26 +256,47 @@ def translate_from_locale(trans_string):
|
||||
else:
|
||||
return trans_string
|
||||
|
||||
|
||||
def get_lot_name(lot_id):
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
name = translate_from_locale(f'Objects_{lot_id}_name')
|
||||
if name == f'Objects_{lot_id}_name':
|
||||
intermed = query_cdclient(
|
||||
'select * from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
if intermed:
|
||||
name = intermed[7] if (intermed[7] != "None" and intermed[7] != "" and intermed[7] is None) else intermed[1]
|
||||
return name
|
||||
|
||||
|
||||
def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('get_zone_name')
|
||||
def get_zone_name(zone_id):
|
||||
if not zone_id:
|
||||
return "Missing"
|
||||
return translate_from_locale(f'ZoneTable_{zone_id}_DisplayDescription')
|
||||
|
||||
@app.template_filter('get_skill_desc')
|
||||
def get_skill_desc(skill_id):
|
||||
return translate_from_locale(f'SkillBehavior_{skill_id}_descriptionUI').replace(
|
||||
"%(DamageCombo)", "Damage Combo: "
|
||||
).replace(
|
||||
"%(AltCombo)", "<br/>Skeleton Combo: "
|
||||
).replace(
|
||||
"%(Description)", "<br/>"
|
||||
).replace(
|
||||
"%(ChargeUp)", "<br/>Charge-up: "
|
||||
)
|
||||
return translate_from_locale(
|
||||
f'SkillBehavior_{skill_id}_descriptionUI'
|
||||
).replace(
|
||||
"%(DamageCombo)", "Damage Combo: "
|
||||
).replace(
|
||||
"%(AltCombo)", "<br/>Skeleton Combo: "
|
||||
).replace(
|
||||
"%(Description)", "<br/>"
|
||||
).replace(
|
||||
"%(ChargeUp)", "<br/>Charge-up: "
|
||||
)
|
||||
|
||||
@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)),
|
||||
@@ -228,27 +313,23 @@ def register_luclient_jinja_helpers(app):
|
||||
return None
|
||||
|
||||
@app.template_filter('get_lot_name')
|
||||
def get_lot_name(lot_id):
|
||||
name = translate_from_locale(f'Objects_{lot_id}_name')
|
||||
if name == translate_from_locale(f'Objects_{lot_id}_name'):
|
||||
intermed = query_cdclient(
|
||||
'select * from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)
|
||||
name = intermed[7] if (intermed[7] != "None" and intermed[7] !="" and intermed[7] != None) else intermed[1]
|
||||
return name
|
||||
def jinja_get_lot_name(lot_id):
|
||||
return get_lot_name(lot_id)
|
||||
|
||||
@app.template_filter('get_lot_rarity')
|
||||
def get_lot_rarity(lot_id):
|
||||
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
render_component_id = query_cdclient(
|
||||
'select component_id from ComponentsRegistry where component_type = 2 and id = ?',
|
||||
'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 = ?',
|
||||
rarity = query_cdclient(
|
||||
'select rarity from ItemComponent where id = ?',
|
||||
[render_component_id],
|
||||
one=True
|
||||
)
|
||||
@@ -258,19 +339,29 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('get_lot_desc')
|
||||
def get_lot_desc(lot_id):
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
desc = translate_from_locale(f'Objects_{lot_id}_description')
|
||||
if desc == f'Objects_{lot_id}_description':
|
||||
desc = query_cdclient(
|
||||
'select description from Objects where id = ?',
|
||||
[lot_id],
|
||||
one=True
|
||||
)[0]
|
||||
)
|
||||
if desc in ("", None):
|
||||
desc = None
|
||||
else:
|
||||
desc = desc[0]
|
||||
if desc in ("", None):
|
||||
desc = None
|
||||
if desc:
|
||||
desc = desc.replace('"', "“")
|
||||
return desc
|
||||
|
||||
@app.template_filter('get_item_set')
|
||||
def check_if_in_set(lot_id):
|
||||
if not lot_id:
|
||||
return None
|
||||
item_set = query_cdclient(
|
||||
'select * from ItemSets where itemIDs like ? or itemIDs like ? or itemIDs like ?',
|
||||
[f'{lot_id}%', f'%, {lot_id}%', f'%,{lot_id}%'],
|
||||
@@ -283,6 +374,8 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
@app.template_filter('get_lot_stats')
|
||||
def get_lot_stats(lot_id):
|
||||
if not lot_id:
|
||||
return None
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ObjectSkills WHERE objectTemplate=?\
|
||||
@@ -292,9 +385,10 @@ def register_luclient_jinja_helpers(app):
|
||||
|
||||
return consolidate_stats(stats)
|
||||
|
||||
|
||||
@app.template_filter('get_set_stats')
|
||||
def get_set_stats(lot_id):
|
||||
if not lot_id:
|
||||
return "Missing"
|
||||
stats = query_cdclient(
|
||||
'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\
|
||||
SELECT skillID FROM ItemSetSkills WHERE SkillSetID=?\
|
||||
@@ -321,7 +415,7 @@ def register_luclient_jinja_helpers(app):
|
||||
def consolidate_stats(stats):
|
||||
|
||||
if len(stats) > 1:
|
||||
consolidated_stats = {"im": 0,"life": 0,"armor": 0, "skill": []}
|
||||
consolidated_stats = {"im": 0, "life": 0, "armor": 0, "skill": []}
|
||||
for stat in stats:
|
||||
if stat[0]:
|
||||
consolidated_stats["im"] += stat[0]
|
||||
@@ -330,8 +424,7 @@ def consolidate_stats(stats):
|
||||
if stat[2]:
|
||||
consolidated_stats["armor"] += stat[2]
|
||||
if stat[3]:
|
||||
consolidated_stats["skill"].append([stat[3],stat[4]])
|
||||
|
||||
consolidated_stats["skill"].append([stat[3], stat[4]])
|
||||
|
||||
stats = consolidated_stats
|
||||
elif len(stats) == 1:
|
||||
|
||||
63
app/mail.py
63
app/mail.py
@@ -1,9 +1,8 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
|
||||
from flask import render_template, Blueprint, redirect, url_for, flash, request
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, Mail, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.models import Mail, CharacterInfo
|
||||
from app.forms import SendMailForm
|
||||
from app import gm_level
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import translate_from_locale, query_cdclient
|
||||
import time
|
||||
|
||||
@@ -25,54 +24,58 @@ def send():
|
||||
form = SendMailForm()
|
||||
|
||||
if request.method == "POST":
|
||||
# if form.validate_on_submit():
|
||||
if form.attachment.data != "0" and form.attachment_count.data == 0:
|
||||
form.attachment_count.data = 1
|
||||
if form.recipient.data == "0":
|
||||
log_audit(f"Sending {form.subject.data}: {form.body.data} to All Characters with {form.attachment_count.data} of item {form.attachment.data}")
|
||||
for character in CharacterInfo.query.all():
|
||||
Mail(
|
||||
sender_id = 0,
|
||||
sender_name = f"[GM] {current_user.username}",
|
||||
receiver_id = character.id,
|
||||
receiver_name = character.name,
|
||||
time_sent = time.time(),
|
||||
subject = form.subject.data,
|
||||
body = form.body.data,
|
||||
attachment_id = 0,
|
||||
attachment_lot = form.attachment.data,
|
||||
attachment_count = form.attachment_count.data
|
||||
sender_id=0,
|
||||
sender_name=f"[GM] {current_user.username}",
|
||||
receiver_id=character.id,
|
||||
receiver_name=character.name,
|
||||
time_sent=time.time(),
|
||||
subject=form.subject.data,
|
||||
body=form.body.data,
|
||||
attachment_id=0,
|
||||
attachment_lot=form.attachment.data,
|
||||
attachment_count=form.attachment_count.data
|
||||
).save()
|
||||
log_audit(f"Sent {form.subject.data}: \
|
||||
{form.body.data} to ({character.id}){character.name} \
|
||||
with {form.attachment_count.data} of item {form.attachment.data}")
|
||||
else:
|
||||
Mail(
|
||||
sender_id = 0,
|
||||
sender_name = f"[GM] {current_user.username}",
|
||||
receiver_id = form.recipient.data,
|
||||
receiver_name = CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name,
|
||||
time_sent = time.time(),
|
||||
subject = form.subject.data,
|
||||
body = form.body.data,
|
||||
attachment_id = 0,
|
||||
attachment_lot = form.attachment.data,
|
||||
attachment_count = form.attachment_count.data
|
||||
sender_id=0,
|
||||
sender_name=f"[GM] {current_user.username}",
|
||||
receiver_id=form.recipient.data,
|
||||
receiver_name=CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name,
|
||||
time_sent=time.time(),
|
||||
subject=form.subject.data,
|
||||
body=form.body.data,
|
||||
attachment_id=0,
|
||||
attachment_lot=form.attachment.data,
|
||||
attachment_count=form.attachment_count.data
|
||||
).save()
|
||||
log_audit(f"Sent {form.subject.data}: \
|
||||
{form.body.data} to ({form.recipient.data}){CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name} \
|
||||
with {form.attachment_count.data} of item {form.attachment.data}")
|
||||
|
||||
flash("Sent Mail", "success")
|
||||
return redirect(url_for('mail.send'))
|
||||
|
||||
|
||||
recipients = CharacterInfo.query.all()
|
||||
for character in recipients:
|
||||
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:
|
||||
name = translate_from_locale(f'Objects_{item[0]}_name')
|
||||
if name == f'Objects_{item[0]}_name':
|
||||
name = (item[2] if (item[2] != "None" and item[2] !="" and item[2] != None) else item[1])
|
||||
name = (item[2] if (item[2] != "None" and item[2] != "" and item[2] is not None) else item[1])
|
||||
form.attachment.choices.append(
|
||||
(
|
||||
item[0],
|
||||
@@ -80,6 +83,4 @@ def send():
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
return render_template('mail/send.html.j2', form=form)
|
||||
|
||||
|
||||
40
app/main.py
40
app/main.py
@@ -1,22 +1,22 @@
|
||||
from flask import render_template, Blueprint, redirect, request, send_from_directory, make_response, send_file
|
||||
from flask_user import login_required, current_user
|
||||
import json, glob, os
|
||||
from wand import image
|
||||
from flask import render_template, Blueprint, send_from_directory
|
||||
from flask_user import current_user, login_required
|
||||
|
||||
from app.models import Account, AccountInvitation, CharacterInfo
|
||||
from app.models import Account, CharacterInfo, ActivityLog
|
||||
from app.schemas import AccountSchema, CharacterInfoSchema
|
||||
from app.luclient import query_cdclient
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
main_blueprint = Blueprint('main', __name__)
|
||||
|
||||
account_schema = AccountSchema()
|
||||
char_info_schema = CharacterInfoSchema()
|
||||
|
||||
|
||||
@main_blueprint.route('/', methods=['GET'])
|
||||
def index():
|
||||
"""Home/Index Page"""
|
||||
if current_user.is_authenticated:
|
||||
|
||||
account_data = Account.query.filter(Account.id == current_user.id).first()
|
||||
|
||||
return render_template(
|
||||
@@ -28,9 +28,33 @@ def index():
|
||||
|
||||
|
||||
@main_blueprint.route('/about')
|
||||
@login_required
|
||||
def about():
|
||||
"""About Page"""
|
||||
return render_template('main/about.html.j2')
|
||||
mods = Account.query.filter(Account.gm_level > 1).order_by(Account.gm_level.desc()).all()
|
||||
online = 0
|
||||
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.map_id
|
||||
).filter(
|
||||
ActivityLog.character_id == char.id
|
||||
).order_by(ActivityLog.id.desc()).first()
|
||||
|
||||
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, users=users, zones=zones)
|
||||
|
||||
|
||||
@main_blueprint.route('/favicon.ico')
|
||||
|
||||
111
app/models.py
111
app/models.py
@@ -7,10 +7,12 @@ import logging
|
||||
from flask_sqlalchemy import BaseQuery
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy.exc import OperationalError, StatementError
|
||||
from sqlalchemy.types import JSON
|
||||
from time import sleep
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
# retrying query to work around python trash collector
|
||||
# killing connections of other gunicorn workers
|
||||
class RetryingQuery(BaseQuery):
|
||||
@@ -45,9 +47,11 @@ class RetryingQuery(BaseQuery):
|
||||
raise
|
||||
self.session.rollback()
|
||||
|
||||
|
||||
db = SQLAlchemy(query_class=RetryingQuery)
|
||||
migrate = Migrate()
|
||||
|
||||
|
||||
class PlayKey(db.Model):
|
||||
__tablename__ = 'play_keys'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -110,6 +114,7 @@ class PlayKey(db.Model):
|
||||
)
|
||||
db.session.add(new_key)
|
||||
db.session.commit()
|
||||
return key
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
@@ -120,6 +125,7 @@ class PlayKey(db.Model):
|
||||
db.session.commit()
|
||||
db.session.refresh(self)
|
||||
|
||||
|
||||
class Account(db.Model, UserMixin):
|
||||
__tablename__ = 'accounts'
|
||||
id = db.Column(
|
||||
@@ -199,7 +205,7 @@ class Account(db.Model, UserMixin):
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id(*, user_id=None):
|
||||
return User.query.filter(user_id == User.id).first()
|
||||
return Account.query.filter(user_id == Account.id).first()
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
@@ -210,6 +216,7 @@ class Account(db.Model, UserMixin):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class AccountInvitation(db.Model):
|
||||
__tablename__ = 'account_invites'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -244,14 +251,10 @@ class AccountInvitation(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_user_by_id(*, user_id=None):
|
||||
return User.query.filter(user_id == User.id).first()
|
||||
return Account.query.filter(user_id == Account.id).first()
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
# This table is cursed, see prop_clone_id
|
||||
class CharacterInfo(db.Model):
|
||||
@@ -326,6 +329,7 @@ class CharacterInfo(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class CharacterXML(db.Model):
|
||||
__tablename__ = 'charxml'
|
||||
id = db.Column(
|
||||
@@ -347,6 +351,7 @@ class CharacterXML(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class CommandLog(db.Model):
|
||||
__tablename__ = 'command_log'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -363,7 +368,7 @@ class CommandLog(db.Model):
|
||||
passive_deletes=True
|
||||
)
|
||||
|
||||
command = db.Column(
|
||||
command = db.Column(
|
||||
mysql.VARCHAR(256),
|
||||
nullable=False
|
||||
)
|
||||
@@ -377,6 +382,7 @@ class CommandLog(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Friends(db.Model):
|
||||
__tablename__ = 'friends'
|
||||
player_id = db.Column(
|
||||
@@ -422,6 +428,7 @@ class Friends(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Leaderboard(db.Model):
|
||||
__tablename__ = 'leaderboard'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -471,6 +478,7 @@ class Leaderboard(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Mail(db.Model):
|
||||
__tablename__ = 'mail'
|
||||
id = db.Column(
|
||||
@@ -559,6 +567,7 @@ class Mail(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class ObjectIDTracker(db.Model):
|
||||
__tablename__ = 'object_id_tracker'
|
||||
last_object_id = db.Column(
|
||||
@@ -568,6 +577,7 @@ class ObjectIDTracker(db.Model):
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
|
||||
class PetNames(db.Model):
|
||||
__tablename__ = 'pet_names'
|
||||
id = db.Column(mysql.BIGINT, primary_key=True)
|
||||
@@ -581,6 +591,11 @@ class PetNames(db.Model):
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
owner_id = db.Column(
|
||||
mysql.BIGINT,
|
||||
nullable=True
|
||||
)
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
@@ -599,7 +614,7 @@ class Property(db.Model):
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
owner_id = db.Column(
|
||||
owner_id = db.Column(
|
||||
mysql.BIGINT,
|
||||
db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'),
|
||||
nullable=False
|
||||
@@ -617,7 +632,7 @@ class Property(db.Model):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
clone_id = db.Column(
|
||||
clone_id = db.Column(
|
||||
mysql.BIGINT(unsigned=True),
|
||||
db.ForeignKey(CharacterInfo.prop_clone_id, ondelete='CASCADE'),
|
||||
)
|
||||
@@ -680,6 +695,15 @@ class Property(db.Model):
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
performance_cost = db.Column(
|
||||
mysql.DOUBLE(
|
||||
precision=20,
|
||||
scale=15,
|
||||
asdecimal=False
|
||||
),
|
||||
server_default='0.0'
|
||||
)
|
||||
|
||||
zone_id = db.Column(
|
||||
mysql.INTEGER,
|
||||
nullable=False,
|
||||
@@ -757,6 +781,7 @@ class UGC(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class PropertyContent(db.Model):
|
||||
__tablename__ = 'properties_contents'
|
||||
id = db.Column(
|
||||
@@ -837,6 +862,7 @@ class PropertyContent(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class ActivityLog(db.Model):
|
||||
__tablename__ = 'activity_log'
|
||||
id = db.Column(mysql.INTEGER, primary_key=True)
|
||||
@@ -877,6 +903,7 @@ class ActivityLog(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class BugReport(db.Model):
|
||||
__tablename__ = 'bug_reports'
|
||||
id = db.Column(mysql.INTEGER, primary_key=True)
|
||||
@@ -929,6 +956,12 @@ class BugReport(db.Model):
|
||||
nullable=True
|
||||
)
|
||||
|
||||
reporter_id = db.Column(
|
||||
mysql.INTEGER(),
|
||||
nullable=False,
|
||||
server_default='0'
|
||||
)
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
@@ -938,6 +971,7 @@ class BugReport(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Server(db.Model):
|
||||
__tablename__ = 'servers'
|
||||
id = db.Column(
|
||||
@@ -980,23 +1014,25 @@ class Server(db.Model):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class ItemReports(db.Model):
|
||||
__tablename__ = 'item_reports'
|
||||
class Reports(db.Model):
|
||||
__tablename__ = 'reports'
|
||||
|
||||
item = db.Column(
|
||||
db.Integer(),
|
||||
primary_key=True,
|
||||
data = db.Column(
|
||||
JSON(),
|
||||
nullable=False
|
||||
)
|
||||
|
||||
count = db.Column(
|
||||
db.Integer(),
|
||||
nullable=False
|
||||
report_type = db.Column(
|
||||
db.VARCHAR(35),
|
||||
nullable=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
date = db.Column(
|
||||
db.Date(),
|
||||
primary_key=False,
|
||||
primary_key=True,
|
||||
autoincrement=False
|
||||
)
|
||||
|
||||
def save(self):
|
||||
@@ -1008,3 +1044,42 @@ class ItemReports(db.Model):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class AuditLog(db.Model):
|
||||
__tablename__ = 'audit_logs'
|
||||
id = db.Column(
|
||||
mysql.INTEGER,
|
||||
primary_key=True
|
||||
)
|
||||
|
||||
account_id = db.Column(
|
||||
db.Integer(),
|
||||
db.ForeignKey(Account.id, ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
account = db.relationship(
|
||||
'Account',
|
||||
backref="audit_logs",
|
||||
passive_deletes=True
|
||||
)
|
||||
|
||||
action = db.Column(
|
||||
mysql.TEXT,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
date = db.Column(
|
||||
mysql.TIMESTAMP,
|
||||
nullable=False,
|
||||
server_default=db.func.now()
|
||||
)
|
||||
|
||||
def save(self):
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
db.session.refresh(self)
|
||||
|
||||
def delete(self):
|
||||
db.session.delete(self)
|
||||
db.session.commit()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, flash, current_app
|
||||
from flask_user import login_required
|
||||
from app.models import PetNames, db
|
||||
from app.models import PetNames, db, CharacterXML, CharacterInfo
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
|
||||
from app import gm_level
|
||||
from app import gm_level, log_audit, scheduler
|
||||
|
||||
moderation_blueprint = Blueprint('moderation', __name__)
|
||||
|
||||
@@ -20,10 +19,11 @@ def index(status):
|
||||
@gm_level(3)
|
||||
def approve_pet(id):
|
||||
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
|
||||
pet_data.approved = 2
|
||||
flash(f"Approved pet name {pet_data.pet_name}", "success")
|
||||
log_audit(f"Approved pet name {pet_data.pet_name} from {pet_data.owner_id}")
|
||||
flash(f"Approved pet name {pet_data.pet_name} from {pet_data.owner_id}", "success")
|
||||
pet_data.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
@@ -33,10 +33,11 @@ def approve_pet(id):
|
||||
@gm_level(3)
|
||||
def reject_pet(id):
|
||||
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
pet_data = PetNames.query.filter(PetNames.id == id).first()
|
||||
|
||||
pet_data.approved = 0
|
||||
flash(f"Rejected pet name {pet_data.pet_name}", "danger")
|
||||
log_audit(f"Rejected pet name {pet_data.pet_name} from {pet_data.owner_id}")
|
||||
flash(f"Rejected pet name {pet_data.pet_name} from {pet_data.owner_id}", "danger")
|
||||
pet_data.save()
|
||||
return redirect(request.referrer if request.referrer else url_for("main.index"))
|
||||
|
||||
@@ -49,18 +50,16 @@ def get_pets(status="all"):
|
||||
ColumnDT(PetNames.id),
|
||||
ColumnDT(PetNames.pet_name),
|
||||
ColumnDT(PetNames.approved),
|
||||
ColumnDT(PetNames.owner_id),
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(PetNames)
|
||||
elif status=="approved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved==2)
|
||||
elif status=="unapproved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved==1)
|
||||
if status == "approved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved == 2)
|
||||
elif status == "unapproved":
|
||||
query = db.session.query().select_from(PetNames).filter(PetNames.approved == 1)
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
query = db.session.query().select_from(PetNames)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
@@ -105,4 +104,47 @@ def get_pets(status="all"):
|
||||
"""
|
||||
pet_data["2"] = "<span class='text-danger'>Rejected</span>"
|
||||
|
||||
if pet_data["3"]:
|
||||
try:
|
||||
pet_data["3"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{url_for('characters.view', id=pet_data["3"])}'>
|
||||
{CharacterInfo.query.filter(CharacterInfo.id==pet_data['3']).first().name}
|
||||
</a>
|
||||
"""
|
||||
except Exception:
|
||||
PetNames.query.filter(PetNames.id == id).first().delete()
|
||||
pet_data["0"] = "<span class='text-danger'>Deleted Refresh to make go away</span>"
|
||||
pet_data["3"] = "<span class='text-danger'>Character Deleted</span>"
|
||||
else:
|
||||
pet_data["3"] = "Pending Character Association"
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@scheduler.task("cron", id="pet_name_maintenance", hour="*", timezone="UTC")
|
||||
def pet_name_maintenance():
|
||||
with scheduler.app.app_context():
|
||||
# associate pet names to characters
|
||||
# current_app.logger.info("Started Pet Name Maintenance")
|
||||
unassociated_pets = PetNames.query.filter(PetNames.owner_id == None).all()
|
||||
if unassociated_pets:
|
||||
current_app.logger.info("Found un-associated pets")
|
||||
for pet in unassociated_pets:
|
||||
owner = CharacterXML.query.filter(CharacterXML.xml_data.like(f"%<p id=\"{pet.id}\" l=\"%")).first()
|
||||
if owner:
|
||||
pet.owner_id = owner.id
|
||||
pet.save()
|
||||
else:
|
||||
pet.delete()
|
||||
|
||||
# auto-moderate based on already moderated names
|
||||
unmoderated_pets = PetNames.query.filter(PetNames.approved == 1).all()
|
||||
if unmoderated_pets:
|
||||
current_app.logger.info("Found un-moderated Pets")
|
||||
for pet in unmoderated_pets:
|
||||
existing_pet = PetNames.query.filter(PetNames.approved.in_([0, 2])).filter(PetNames.pet_name == pet.pet_name).first()
|
||||
if existing_pet:
|
||||
pet.approved = existing_pet.approved
|
||||
pet.save()
|
||||
# current_app.logger.info("Finished Pet Name Maintenance")
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import Account, AccountInvitation, PlayKey, db
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, flash
|
||||
from flask_user import login_required
|
||||
from app.models import Account, PlayKey, db
|
||||
from datatables import ColumnDT, DataTables
|
||||
from app.forms import CreatePlayKeyForm, EditPlayKeyForm
|
||||
from app import gm_level
|
||||
from app import gm_level, log_audit
|
||||
|
||||
play_keys_blueprint = Blueprint('play_keys', __name__)
|
||||
|
||||
# Key creation page
|
||||
|
||||
# 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):
|
||||
PlayKey.create(count=count, uses=uses)
|
||||
flash(f"Created {count} Play Key(s) with {uses} uses!", "success")
|
||||
key = PlayKey.create(count=count, uses=uses)
|
||||
log_audit(f"Created {count} Play Key(s) with {uses} uses!")
|
||||
flash(f"Created {count} Play Key(s) with {uses} uses! {key}", "success")
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
|
||||
@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():
|
||||
PlayKey.create(count=form.count.data, uses=form.uses.data)
|
||||
log_audit(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!")
|
||||
flash(f"Created {form.count.data} Play Key(s) with {form.uses.data} uses!", "success")
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
return render_template('play_keys/bulk.html.j2', form=form)
|
||||
@@ -39,10 +42,11 @@ 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()
|
||||
# associated_accounts = Account.query.filter(Account.play_key_id==id).all()
|
||||
log_audit(f"Deleted Play Key {key.key_string}")
|
||||
flash(f"Deleted Play Key {key.key_string}", "danger")
|
||||
key.delete()
|
||||
return redirect(url_for('play_keys.index'))
|
||||
@@ -50,16 +54,22 @@ 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()
|
||||
key = PlayKey.query.filter(PlayKey.id == id).first()
|
||||
form = EditPlayKeyForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
log_audit(f"Updated Play key {key.id} \
|
||||
Uses: {key.key_uses}:{form.uses.data} \
|
||||
Active: {key.active}:{form.active.data} \
|
||||
Notes: {key.notes}:{form.notes.data} \
|
||||
")
|
||||
key.key_uses = form.uses.data
|
||||
key.active = form.active.data
|
||||
key.notes = form.notes.data
|
||||
key.save()
|
||||
|
||||
return redirect(url_for('play_keys.index'))
|
||||
|
||||
form.uses.data = key.key_uses
|
||||
@@ -71,16 +81,16 @@ 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()
|
||||
accounts = Account.query.filter(Account.play_key_id == id).all()
|
||||
return render_template('play_keys/view.html.j2', key=key, accounts=accounts)
|
||||
|
||||
|
||||
@play_keys_blueprint.route('/get', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(9)
|
||||
@gm_level(5)
|
||||
def get():
|
||||
columns = [
|
||||
ColumnDT(PlayKey.id),
|
||||
@@ -115,7 +125,12 @@ def get():
|
||||
Delete
|
||||
</a>
|
||||
|
||||
<div class="modal fade bd-example-modal-lg" id="delete-{play_key["1"]}-modal" tabindex="-1" role="dialog" aria-labelledby="delete-{play_key["1"]}-modalLabel" aria-hidden="true">
|
||||
<div class="modal
|
||||
fade bd-example-modal-lg"
|
||||
id="delete-{play_key["1"]}-modal"
|
||||
tabindex="-1" role="dialog"
|
||||
aria-labelledby="delete-{play_key["1"]}-modalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content bg-dark border-primary">
|
||||
<div class="modal-header">
|
||||
|
||||
@@ -5,29 +5,28 @@ from flask import (
|
||||
url_for,
|
||||
request,
|
||||
abort,
|
||||
jsonify,
|
||||
send_from_directory,
|
||||
make_response,
|
||||
flash
|
||||
flash,
|
||||
current_app
|
||||
)
|
||||
from flask_user import login_required, current_user
|
||||
import json
|
||||
from datatables import ColumnDT, DataTables
|
||||
import time
|
||||
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account
|
||||
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail
|
||||
from app.schemas import PropertySchema
|
||||
from app import gm_level
|
||||
from app import gm_level, log_audit
|
||||
from app.luclient import query_cdclient
|
||||
from app.forms import RejectPropertyForm
|
||||
|
||||
import zlib
|
||||
import xmltodict
|
||||
import os
|
||||
import app.pylddlib as ldd
|
||||
import pathlib
|
||||
|
||||
property_blueprint = Blueprint('properties', __name__)
|
||||
|
||||
property_schema = PropertySchema()
|
||||
|
||||
|
||||
@property_blueprint.route('/', methods=['GET'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
@@ -40,7 +39,7 @@ def index():
|
||||
@gm_level(3)
|
||||
def approve(id):
|
||||
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
|
||||
property_data.mod_approved = not property_data.mod_approved
|
||||
|
||||
@@ -49,26 +48,30 @@ def approve(id):
|
||||
property_data.rejection_reason = ""
|
||||
|
||||
if property_data.mod_approved:
|
||||
flash(
|
||||
f"""Approved Property
|
||||
message = f"""Approved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
|
||||
log_audit(message)
|
||||
flash(
|
||||
message,
|
||||
"success"
|
||||
)
|
||||
else:
|
||||
flash(
|
||||
f"""Unapproved Property
|
||||
message = f"""Unapproved Property
|
||||
{property_data.name if property_data.name else query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]}
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""",
|
||||
"danger"
|
||||
from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}"""
|
||||
log_audit(message)
|
||||
flash(
|
||||
message,
|
||||
"warning"
|
||||
)
|
||||
|
||||
property_data.save()
|
||||
@@ -83,11 +86,73 @@ def approve(id):
|
||||
else:
|
||||
go_to = url_for('main.index')
|
||||
|
||||
|
||||
|
||||
return redirect(go_to)
|
||||
|
||||
|
||||
@property_blueprint.route('/reject/<id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def reject(id):
|
||||
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
|
||||
form = RejectPropertyForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
char_name = CharacterInfo.query.filter(CharacterInfo.id == property_data.owner_id).first().name
|
||||
zone_name = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data.zone_id],
|
||||
one=True
|
||||
)[0]
|
||||
property_data.mod_approved = False
|
||||
property_data.rejection_reason = form.rejection_reason.data
|
||||
message = f"""Rejected Property
|
||||
{property_data.name if property_data.name else zone_name}
|
||||
from {char_name} with reason \"{form.rejection_reason.data}\""""
|
||||
log_audit(message)
|
||||
flash(
|
||||
message,
|
||||
"danger"
|
||||
)
|
||||
|
||||
property_data.save()
|
||||
|
||||
# send rejection reason to their mailbox
|
||||
# cause the game doesn't present it otherwise
|
||||
mail_message = f"""Rejected Property
|
||||
{property_data.name} on {zone_name}
|
||||
with reason \"{form.rejection_reason.data}\""""
|
||||
Mail(
|
||||
sender_id=0,
|
||||
sender_name=f"[GM] {current_user.username}",
|
||||
receiver_id=property_data.owner_id,
|
||||
receiver_name=char_name,
|
||||
time_sent=time.time(),
|
||||
subject="Property Rejected",
|
||||
body=mail_message,
|
||||
attachment_id=0,
|
||||
attachment_lot=0,
|
||||
attachment_count=0
|
||||
).save()
|
||||
|
||||
go_to = ""
|
||||
|
||||
if request.referrer:
|
||||
if "view_models" in request.referrer:
|
||||
go_to = url_for('properties.view', id=id)
|
||||
else:
|
||||
go_to = url_for('properties.index')
|
||||
else:
|
||||
go_to = url_for('main.index')
|
||||
|
||||
return redirect(go_to)
|
||||
|
||||
form.rejection_reason.data = property_data.rejection_reason
|
||||
|
||||
return render_template('properties/reject.html.j2', property_data=property_data, form=form)
|
||||
|
||||
|
||||
@property_blueprint.route('/view/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def view(id):
|
||||
@@ -123,26 +188,29 @@ def get(status="all"):
|
||||
ColumnDT(Property.time_claimed), # 9
|
||||
ColumnDT(Property.rejection_reason), # 10
|
||||
ColumnDT(Property.reputation), # 11
|
||||
ColumnDT(Property.zone_id), # 12
|
||||
ColumnDT(Account.username) # 13
|
||||
ColumnDT(Property.performance_cost), # 12
|
||||
ColumnDT(Property.zone_id), # 13
|
||||
ColumnDT(Account.username) # 14
|
||||
]
|
||||
|
||||
query = None
|
||||
if status=="all":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account)
|
||||
elif status=="approved":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==True)
|
||||
elif status=="unapproved":
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==False)
|
||||
if status == "approved":
|
||||
query = db.session.query().select_from(Property).join(
|
||||
CharacterInfo, CharacterInfo.id == Property.owner_id
|
||||
).join(Account).filter(Property.mod_approved == True).filter(Property.privacy_option == 2) # noqa
|
||||
elif status == "unapproved":
|
||||
query = db.session.query().select_from(Property).join(
|
||||
CharacterInfo, CharacterInfo.id == Property.owner_id
|
||||
).join(Account).filter(Property.mod_approved == False).filter(Property.privacy_option == 2).filter(Property.rejection_reason == "") # noqa
|
||||
else:
|
||||
raise Exception("Not a valid filter")
|
||||
|
||||
query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id == Property.owner_id).join(Account)
|
||||
|
||||
params = request.args.to_dict()
|
||||
|
||||
rowTable = DataTables(params, query, columns)
|
||||
|
||||
data = rowTable.output_result()
|
||||
print(data)
|
||||
for property_data in data["data"]:
|
||||
id = property_data["0"]
|
||||
|
||||
@@ -167,6 +235,13 @@ def get(status="all"):
|
||||
Unapprove
|
||||
</a>
|
||||
"""
|
||||
if not property_data["10"]:
|
||||
property_data["0"] += f"""
|
||||
<a role="button" class="btn btn-danger btn btn-block"
|
||||
href='{url_for('properties.reject', id=id)}'>
|
||||
Reject
|
||||
</a>
|
||||
"""
|
||||
|
||||
property_data["1"] = f"""
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
@@ -177,10 +252,10 @@ def get(status="all"):
|
||||
|
||||
if property_data["4"] == "":
|
||||
property_data["4"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["12"]],
|
||||
one=True
|
||||
)
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["13"]],
|
||||
one=True
|
||||
)
|
||||
|
||||
if property_data["6"] == 0:
|
||||
property_data["6"] = "Private"
|
||||
@@ -197,25 +272,25 @@ def get(status="all"):
|
||||
else:
|
||||
property_data["7"] = '''<h2 class="far fa-check-square text-success"></h2>'''
|
||||
|
||||
property_data["12"] = query_cdclient(
|
||||
property_data["13"] = query_cdclient(
|
||||
'select DisplayDescription from ZoneTable where zoneID = ?',
|
||||
[property_data["12"]],
|
||||
[property_data["13"]],
|
||||
one=True
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@property_blueprint.route('/view_model/<id>', methods=['GET'])
|
||||
@property_blueprint.route('/view_model/<id>/<lod>', methods=['GET'])
|
||||
@login_required
|
||||
def view_model(id):
|
||||
property_content_data = PropertyContent.query.filter(PropertyContent.id==id).all()
|
||||
def view_model(id, lod):
|
||||
property_content_data = PropertyContent.query.filter(PropertyContent.id == id).all()
|
||||
|
||||
# TODO: Restrict somehow
|
||||
formatted_data = [
|
||||
{
|
||||
"obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj'),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl'),
|
||||
"obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj', lod=lod),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl', lod=lod),
|
||||
"lot": property_content_data[0].lot,
|
||||
"id": property_content_data[0].id,
|
||||
"pos": [{
|
||||
@@ -232,9 +307,11 @@ def view_model(id):
|
||||
|
||||
return render_template(
|
||||
'ldd/ldd.html.j2',
|
||||
content=formatted_data
|
||||
content=formatted_data,
|
||||
lod=lod
|
||||
)
|
||||
|
||||
|
||||
property_center = {
|
||||
1150: "(-17, 432, -60)",
|
||||
1151: "(0, 455, -110)",
|
||||
@@ -245,11 +322,11 @@ property_center = {
|
||||
}
|
||||
|
||||
|
||||
@property_blueprint.route('/view_models/<id>', methods=['GET'])
|
||||
@property_blueprint.route('/view_models/<id>/<lod>', methods=['GET'])
|
||||
@login_required
|
||||
def view_models(id):
|
||||
def view_models(id, lod):
|
||||
property_content_data = PropertyContent.query.filter(
|
||||
PropertyContent.property_id==id
|
||||
PropertyContent.property_id == id
|
||||
).order_by(PropertyContent.lot).all()
|
||||
|
||||
consolidated_list = []
|
||||
@@ -273,8 +350,8 @@ def view_models(id):
|
||||
# add new lot
|
||||
consolidated_list.append(
|
||||
{
|
||||
"obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj'),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl'),
|
||||
"obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj', lod=lod),
|
||||
"mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl', lod=lod),
|
||||
"lot": property_content_data[item].lot,
|
||||
"id": property_content_data[item].id,
|
||||
"pos": [{
|
||||
@@ -288,37 +365,39 @@ def view_models(id):
|
||||
}]
|
||||
}
|
||||
)
|
||||
property_data = Property.query.filter(Property.id==id).first()
|
||||
property_data = Property.query.filter(Property.id == id).first()
|
||||
return render_template(
|
||||
'ldd/ldd.html.j2',
|
||||
property_data=property_data,
|
||||
content=consolidated_list,
|
||||
center=property_center[property_data.zone_id]
|
||||
center=property_center[property_data.zone_id],
|
||||
lod=lod
|
||||
)
|
||||
|
||||
@property_blueprint.route('/get_model/<id>/<file_format>', methods=['GET'])
|
||||
@login_required
|
||||
def get_model(id, file_format):
|
||||
content = PropertyContent.query.filter(PropertyContent.id==id).first()
|
||||
|
||||
if content.lot == 14: # ugc model
|
||||
@property_blueprint.route('/get_model/<id>/<file_format>/<lod>', methods=['GET'])
|
||||
@login_required
|
||||
def get_model(id, file_format, lod):
|
||||
content = PropertyContent.query.filter(PropertyContent.id == id).first()
|
||||
if not(0 <= int(lod) <= 2):
|
||||
abort(404)
|
||||
if content.lot == 14: # ugc model
|
||||
response = ugc(content)[0]
|
||||
else: # prebuild model
|
||||
response = prebuilt(content, file_format)[0]
|
||||
else: # prebuilt model
|
||||
response = prebuilt(content, file_format, lod)[0]
|
||||
|
||||
response.headers.set('Content-Type', 'text/xml')
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@property_blueprint.route('/download_model/<id>', methods=['GET'])
|
||||
@login_required
|
||||
def download_model(id):
|
||||
content = PropertyContent.query.filter(PropertyContent.id==id).first()
|
||||
content = PropertyContent.query.filter(PropertyContent.id == id).first()
|
||||
|
||||
if content.lot == 14: # ugc model
|
||||
if content.lot == 14: # ugc model
|
||||
response, filename = ugc(content)
|
||||
else: # prebuild model
|
||||
else: # prebuilt model
|
||||
response, filename = prebuilt(content, "lxfml")
|
||||
|
||||
response.headers.set('Content-Type', 'attachment/xml')
|
||||
@@ -331,55 +410,69 @@ 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)
|
||||
ugc_data = UGC.query.filter(UGC.id == content.ugc_id).first()
|
||||
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):
|
||||
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
|
||||
filename = query_cdclient('select render_asset from RenderComponent where 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:
|
||||
filename = filename.split("/")[-1].lower()
|
||||
else:
|
||||
return f"No filename for LOT {content.lot}"
|
||||
|
||||
# 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":
|
||||
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
|
||||
|
||||
with open(lxfml, 'r') as file:
|
||||
lxfml_data = file.read()
|
||||
# print(lxfml_data)
|
||||
response = make_response(lxfml_data)
|
||||
|
||||
# else we handle getting the files for lddviewer
|
||||
elif file_format in ["obj", "mtl"]:
|
||||
|
||||
cache = f"app/cache/{filename}.{file_format}"
|
||||
|
||||
if os.path.exists(cache):
|
||||
with open(cache, 'r') as file:
|
||||
cache_data = file.read()
|
||||
response = make_response(cache_data)
|
||||
|
||||
else:
|
||||
lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml'
|
||||
ldd.main(lxfml, cache.split('.')[0]) # convert to OBJ
|
||||
|
||||
if os.path.exists(cache):
|
||||
with open(cache, 'r') as file:
|
||||
cache_data = file.read()
|
||||
response = make_response(cache_data)
|
||||
# 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:
|
||||
raise(Exception("INVALID FILE FORMAT"))
|
||||
|
||||
383
app/pylddlib.py
383
app/pylddlib.py
@@ -1,43 +1,24 @@
|
||||
#!/usr/bin/env python
|
||||
# pylddlib version 0.4.9.7
|
||||
# based on pyldd2obj version 0.4.8 - Copyright (c) 2019 by jonnysp
|
||||
#
|
||||
# Updates:
|
||||
# 0.4.9.8 Make work with LEGO Universe brickdb
|
||||
# 0.4.9.7 corrected bug of incorrectly parsing the primitive xml file, specifically with comments. Add support LDDLIFTREE envirnment variable to set location of db.lif.
|
||||
# 0.4.9.6 preliminary Linux support
|
||||
# 0.4.9.5 corrected bug of incorrectly Bounding / GeometryBounding parsing the primitive xml file.
|
||||
# 0.4.9.4 improved lif.db checking for crucial files (because of the infamous botched 4.3.12 LDD Windows update).
|
||||
# 0.4.9.3 improved Windows and Python 3 compatibility
|
||||
# 0.4.9.2 changed handling of material = 0 for a part. Now a 0 will choose the 1st material (the base material of a part) and not the previous material of the subpart before. This will fix "Chicken Helmet Part 11262". It may break other parts and this change needs further regression.
|
||||
# 0.4.9.1 improved custom2DField handling, fixed decorations bug, improved material assignments handling
|
||||
# 0.4.9 updates to support reading extracted db.lif from db folder
|
||||
#
|
||||
# License: MIT License
|
||||
#
|
||||
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import zipfile
|
||||
from xml.dom import minidom
|
||||
import time
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
from flask import current_app
|
||||
|
||||
PRIMITIVEPATH = '/Primitives/'
|
||||
GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/'
|
||||
GEOMETRIEPATH = PRIMITIVEPATH
|
||||
DECORATIONPATH = '/Decorations/'
|
||||
MATERIALNAMESPATH = '/MaterialNames/'
|
||||
|
||||
LOGOONSTUDSCONNTYPE = {"0:4", "0:4:1", "0:4:2", "0:4:33", "2:4:1", "2:4:34"}
|
||||
|
||||
|
||||
class Matrix3D:
|
||||
def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1):
|
||||
def __init__(self, n11=1, n12=0, n13=0, n14=0, n21=0, n22=1, n23=0, n24=0, n31=0, n32=0, n33=1, n34=0, n41=0, n42=0, n43=0, n44=1):
|
||||
self.n11 = n11
|
||||
self.n12 = n12
|
||||
self.n13 = n13
|
||||
@@ -56,9 +37,26 @@ class Matrix3D:
|
||||
self.n44 = n44
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(self.n11, self.n12, self.n13,self.n14,self.n21, self.n22, self.n23,self.n24,self.n31, self.n32, self.n33,self.n34,self.n41, self.n42, self.n43,self.n44)
|
||||
return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(
|
||||
self.n11,
|
||||
self.n12,
|
||||
self.n13,
|
||||
self.n14,
|
||||
self.n21,
|
||||
self.n22,
|
||||
self.n23,
|
||||
self.n24,
|
||||
self.n31,
|
||||
self.n32,
|
||||
self.n33,
|
||||
self.n34,
|
||||
self.n41,
|
||||
self.n42,
|
||||
self.n43,
|
||||
self.n44
|
||||
)
|
||||
|
||||
def rotate(self,angle=0,axis=0):
|
||||
def rotate(self, angle=0, axis=0):
|
||||
c = math.cos(angle)
|
||||
s = math.sin(angle)
|
||||
t = 1 - c
|
||||
@@ -109,21 +107,22 @@ class Matrix3D:
|
||||
self.n12 * other.n41 + self.n22 * other.n42 + self.n32 * other.n43 + self.n42 * other.n44,
|
||||
self.n13 * other.n41 + self.n23 * other.n42 + self.n33 * other.n43 + self.n43 * other.n44,
|
||||
self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Point3D:
|
||||
def __init__(self, x=0,y=0,z=0):
|
||||
def __init__(self, x=0, y=0, z=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2}]'.format(self.x, self.y,self.z)
|
||||
return '[{0},{1},{2}]'.format(self.x, self.y, self.z)
|
||||
|
||||
def string(self,prefix = "v"):
|
||||
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z)
|
||||
def string(self, prefix="v"):
|
||||
return '{0} {1:f} {2:f} {3:f}\n'.format(prefix, self.x, self.y, self.z)
|
||||
|
||||
def transformW(self,matrix):
|
||||
def transformW(self, matrix):
|
||||
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z
|
||||
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z
|
||||
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z
|
||||
@@ -131,7 +130,7 @@ class Point3D:
|
||||
self.y = y
|
||||
self.z = z
|
||||
|
||||
def transform(self,matrix):
|
||||
def transform(self, matrix):
|
||||
x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41
|
||||
y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42
|
||||
z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43
|
||||
@@ -140,41 +139,58 @@ class Point3D:
|
||||
self.z = z
|
||||
|
||||
def copy(self):
|
||||
return Point3D(x=self.x,y=self.y,z=self.z)
|
||||
return Point3D(x=self.x, y=self.y, z=self.z)
|
||||
|
||||
|
||||
class Point2D:
|
||||
def __init__(self, x=0,y=0):
|
||||
def __init__(self, x=0, y=0):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1}]'.format(self.x, self.y * -1)
|
||||
def string(self,prefix="t"):
|
||||
return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 )
|
||||
|
||||
def string(self, prefix="t"):
|
||||
return '{0} {1:f} {2:f}\n'.format(prefix, self.x, self.y * -1)
|
||||
|
||||
def copy(self):
|
||||
return Point2D(x=self.x,y=self.y)
|
||||
return Point2D(x=self.x, y=self.y)
|
||||
|
||||
|
||||
class Face:
|
||||
def __init__(self,a=0,b=0,c=0):
|
||||
def __init__(self, a=0, b=0, c=0):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
def string(self,prefix="f", indexOffset=0 ,textureoffset=0):
|
||||
|
||||
def string(self, prefix="f", indexOffset=0, textureoffset=0):
|
||||
if textureoffset == 0:
|
||||
return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset)
|
||||
else:
|
||||
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset)
|
||||
return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(
|
||||
self.a + indexOffset,
|
||||
self.b + indexOffset,
|
||||
self.c + indexOffset,
|
||||
self.a + textureoffset,
|
||||
self.b + textureoffset,
|
||||
self.c + textureoffset
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return '[{0},{1},{2}]'.format(self.a, self.b, self.c)
|
||||
|
||||
|
||||
class Group:
|
||||
def __init__(self, node):
|
||||
self.partRefs = node.getAttribute('partRefs').split(',')
|
||||
|
||||
|
||||
class Bone:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
||||
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
||||
self.matrix = Matrix3D(n11=a, n12=b, n13=c, n14=0, n21=d, n22=e, n23=f, n24=0, n31=g, n32=h, n33=i, n34=0, n41=x, n42=y, n43=z, n44=1)
|
||||
|
||||
|
||||
class Part:
|
||||
def __init__(self, node):
|
||||
@@ -185,19 +201,17 @@ class Part:
|
||||
self.designID = node.getAttribute('designID')
|
||||
self.materials = list(map(str, node.getAttribute('materials').split(',')))
|
||||
|
||||
lastm = '0'
|
||||
for i, m in enumerate(self.materials):
|
||||
if (m == '0'):
|
||||
# self.materials[i] = lastm
|
||||
self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material
|
||||
else:
|
||||
lastm = m
|
||||
self.materials[i] = self.materials[0] # in case of 0 choose the 'base' material
|
||||
if node.hasAttribute('decoration'):
|
||||
self.decoration = list(map(str,node.getAttribute('decoration').split(',')))
|
||||
self.decoration = list(map(str, node.getAttribute('decoration').split(',')))
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Bone':
|
||||
self.Bones.append(Bone(node=childnode))
|
||||
|
||||
|
||||
class Brick:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
@@ -207,14 +221,16 @@ class Brick:
|
||||
if childnode.nodeName == 'Part':
|
||||
self.Parts.append(Part(node=childnode))
|
||||
|
||||
|
||||
class SceneCamera:
|
||||
def __init__(self, node):
|
||||
self.refID = node.getAttribute('refID')
|
||||
(a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(','))
|
||||
self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1)
|
||||
self.matrix = Matrix3D(n11=a, n12=b, n13=c, n14=0, n21=d, n22=e, n23=f, n24=0, n31=g, n32=h, n33=i, n34=0, n41=x, n42=y, n43=z, n44=1)
|
||||
self.fieldOfView = float(node.getAttribute('fieldOfView'))
|
||||
self.distance = float(node.getAttribute('distance'))
|
||||
|
||||
|
||||
class Scene:
|
||||
def __init__(self, file):
|
||||
self.Bricks = []
|
||||
@@ -238,10 +254,10 @@ class Scene:
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'BrickSet':
|
||||
self.Version = str(childnode.getAttribute('version'))
|
||||
elif node.nodeName == 'Cameras':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Camera':
|
||||
self.Scenecamera.append(SceneCamera(node=childnode))
|
||||
# elif node.nodeName == 'Cameras':
|
||||
# for childnode in node.childNodes:
|
||||
# if childnode.nodeName == 'Camera':
|
||||
# self.Scenecamera.append(SceneCamera(node=childnode))
|
||||
elif node.nodeName == 'Bricks':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Brick':
|
||||
@@ -262,6 +278,7 @@ class Scene:
|
||||
|
||||
# print('Scene "'+ self.Name + '" Brickversion: ' + str(self.Version))
|
||||
|
||||
|
||||
class GeometryReader:
|
||||
def __init__(self, data):
|
||||
self.offset = 0
|
||||
@@ -282,10 +299,10 @@ class GeometryReader:
|
||||
options = self.readInt()
|
||||
|
||||
for i in range(0, self.valueCount):
|
||||
self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
||||
self.positions.append(Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat()))
|
||||
|
||||
for i in range(0, self.valueCount):
|
||||
self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat()))
|
||||
self.normals.append(Point3D(x=self.readFloat(), y=self.readFloat(), z=self.readFloat()))
|
||||
|
||||
if (options & 3) == 3:
|
||||
self.texCount = self.valueCount
|
||||
@@ -293,7 +310,7 @@ class GeometryReader:
|
||||
self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat()))
|
||||
|
||||
for i in range(0, self.faceCount):
|
||||
self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt()))
|
||||
self.faces.append(Face(a=self.readInt(), b=self.readInt(), c=self.readInt()))
|
||||
|
||||
if (options & 48) == 48:
|
||||
num = self.readInt()
|
||||
@@ -311,7 +328,7 @@ class GeometryReader:
|
||||
boneoffset = self.readInt() + 4
|
||||
self.bonemap[i] = self.read_Int(datastart + boneoffset)
|
||||
|
||||
def read_Int(self,_offset):
|
||||
def read_Int(self, _offset):
|
||||
if sys.version_info < (3, 0):
|
||||
return int(struct.unpack_from('i', self.data, _offset)[0])
|
||||
else:
|
||||
@@ -330,6 +347,7 @@ class GeometryReader:
|
||||
self.offset += 4
|
||||
return ret
|
||||
|
||||
|
||||
class Geometry:
|
||||
def __init__(self, designID, database):
|
||||
self.designID = designID
|
||||
@@ -337,21 +355,25 @@ class Geometry:
|
||||
self.maxGeoBounding = -1
|
||||
self.studsFields2D = []
|
||||
|
||||
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID,'.g')
|
||||
GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID, '.g')
|
||||
GeometryCount = 0
|
||||
while str(GeometryLocation) in database.filelist:
|
||||
self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read())
|
||||
GeometryCount += 1
|
||||
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID,'.g',GeometryCount)
|
||||
GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID, '.g', GeometryCount)
|
||||
|
||||
primitive = Primitive(data = database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
|
||||
primitive = Primitive(data=database.filelist[PRIMITIVEPATH + designID + '.xml'].read())
|
||||
self.Partname = primitive.Designname
|
||||
self.studsFields2D = primitive.Fields2D
|
||||
try:
|
||||
geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))]
|
||||
geoBoundingList = [
|
||||
abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])),
|
||||
abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])),
|
||||
abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))
|
||||
]
|
||||
geoBoundingList.sort()
|
||||
self.maxGeoBounding = geoBoundingList[-1]
|
||||
except KeyError as e:
|
||||
except KeyError:
|
||||
# print('\nBounding errror in part {0}: {1}\n'.format(designID, e))
|
||||
pass
|
||||
|
||||
@@ -386,25 +408,33 @@ class Geometry:
|
||||
count += self.Parts[part].texCount
|
||||
return count
|
||||
|
||||
|
||||
class Bone2:
|
||||
def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
||||
def __init__(self, boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
||||
self.boneId = boneId
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
rotationMatrix.rotate(
|
||||
angle=(-angle * math.pi / 180.0),
|
||||
axis=Point3D(x=ax, y=ay, z=az)
|
||||
)
|
||||
p = Point3D(x=tx, y=ty, z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
rotationMatrix.n43 -= p.z
|
||||
self.matrix = rotationMatrix
|
||||
|
||||
|
||||
class Field2D:
|
||||
def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'):
|
||||
self.type = type
|
||||
self.field2DRawData = field2DRawData
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
rotationMatrix.rotate(
|
||||
angle=(-angle * math.pi / 180.0),
|
||||
axis=Point3D(x=ax, y=ay, z=az)
|
||||
)
|
||||
p = Point3D(x=tx, y=ty, z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
@@ -413,7 +443,7 @@ class Field2D:
|
||||
self.matrix = rotationMatrix
|
||||
self.custom2DField = []
|
||||
|
||||
#The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
|
||||
# The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1.
|
||||
rows_count = height + 1
|
||||
cols_count = width + 1
|
||||
# creation looks reverse
|
||||
@@ -432,18 +462,23 @@ class Field2D:
|
||||
def __str__(self):
|
||||
return '[type="{0}" transform="{1}" custom2DField="{2}"]'.format(self.type, self.matrix, self.custom2DField)
|
||||
|
||||
|
||||
class CollisionBox:
|
||||
def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0):
|
||||
rotationMatrix = Matrix3D()
|
||||
rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az))
|
||||
p = Point3D(x=tx,y=ty,z=tz)
|
||||
rotationMatrix.rotate(
|
||||
angle=(-angle * math.pi / 180.0),
|
||||
axis=Point3D(x=ax, y=ay, z=az)
|
||||
)
|
||||
p = Point3D(x=tx, y=ty,
|
||||
z=tz)
|
||||
p.transformW(rotationMatrix)
|
||||
rotationMatrix.n41 -= p.x
|
||||
rotationMatrix.n42 -= p.y
|
||||
rotationMatrix.n43 -= p.z
|
||||
|
||||
self.matrix = rotationMatrix
|
||||
self.corner = Point3D(x=sX,y=sY,z=sZ)
|
||||
self.corner = Point3D(x=sX, y=sY, z=sZ)
|
||||
self.positions = []
|
||||
|
||||
self.positions.append(Point3D(x=0, y=0, z=0))
|
||||
@@ -452,11 +487,14 @@ class CollisionBox:
|
||||
self.positions.append(Point3D(x=sX, y=sY, z=0))
|
||||
self.positions.append(Point3D(x=0, y=0, z=sZ))
|
||||
self.positions.append(Point3D(x=0, y=sY, z=sZ))
|
||||
self.positions.append(Point3D(x=sX ,y=0, z=sZ))
|
||||
self.positions.append(Point3D(x=sX ,y=sY, z=sZ))
|
||||
self.positions.append(Point3D(x=sX, y=0, z=sZ))
|
||||
self.positions.append(Point3D(x=sX, y=sY, z=sZ))
|
||||
|
||||
def __str__(self):
|
||||
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(self.corner.x, self.corner.y, self.corner.z)
|
||||
return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(
|
||||
self.corner.x, self.corner.y, self.corner.z
|
||||
)
|
||||
|
||||
|
||||
class Primitive:
|
||||
def __init__(self, data):
|
||||
@@ -475,7 +513,18 @@ class Primitive:
|
||||
if node.nodeName == 'Flex':
|
||||
for node in node.childNodes:
|
||||
if node.nodeName == 'Bone':
|
||||
self.Bones.append(Bone2(boneId=int(node.getAttribute('boneId')), angle=float(node.getAttribute('angle')), ax=float(node.getAttribute('ax')), ay=float(node.getAttribute('ay')), az=float(node.getAttribute('az')), tx=float(node.getAttribute('tx')), ty=float(node.getAttribute('ty')), tz=float(node.getAttribute('tz'))))
|
||||
self.Bones.append(
|
||||
Bone2(
|
||||
boneId=int(node.getAttribute('boneId')),
|
||||
angle=float(node.getAttribute('angle')),
|
||||
ax=float(node.getAttribute('ax')),
|
||||
ay=float(node.getAttribute('ay')),
|
||||
az=float(node.getAttribute('az')),
|
||||
tx=float(node.getAttribute('tx')),
|
||||
ty=float(node.getAttribute('ty')),
|
||||
tz=float(node.getAttribute('tz'))
|
||||
)
|
||||
)
|
||||
elif node.nodeName == 'Annotations':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Annotation' and childnode.hasAttribute('designname'):
|
||||
@@ -483,23 +532,73 @@ class Primitive:
|
||||
elif node.nodeName == 'Collision':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Box':
|
||||
self.CollisionBoxes.append(CollisionBox(sX=float(childnode.getAttribute('sX')), sY=float(childnode.getAttribute('sY')), sZ=float(childnode.getAttribute('sZ')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz'))))
|
||||
self.CollisionBoxes.append(
|
||||
CollisionBox(
|
||||
sX=float(childnode.getAttribute('sX')),
|
||||
sY=float(childnode.getAttribute('sY')),
|
||||
sZ=float(childnode.getAttribute('sZ')),
|
||||
angle=float(childnode.getAttribute('angle')),
|
||||
ax=float(childnode.getAttribute('ax')),
|
||||
ay=float(childnode.getAttribute('ay')),
|
||||
az=float(childnode.getAttribute('az')),
|
||||
tx=float(childnode.getAttribute('tx')),
|
||||
ty=float(childnode.getAttribute('ty')),
|
||||
tz=float(childnode.getAttribute('tz'))
|
||||
)
|
||||
)
|
||||
elif node.nodeName == 'PhysicsAttributes':
|
||||
self.PhysicsAttributes = {"inertiaTensor": node.getAttribute('inertiaTensor'),"centerOfMass": node.getAttribute('centerOfMass'),"mass": node.getAttribute('mass'),"frictionType": node.getAttribute('frictionType')}
|
||||
self.PhysicsAttributes = {
|
||||
"inertiaTensor": node.getAttribute('inertiaTensor'),
|
||||
"centerOfMass": node.getAttribute('centerOfMass'),
|
||||
"mass": node.getAttribute('mass'),
|
||||
"frictionType": node.getAttribute('frictionType')
|
||||
}
|
||||
elif node.nodeName == 'Bounding':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'AABB':
|
||||
self.Bounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
||||
self.Bounding = {
|
||||
"minX": childnode.getAttribute('minX'),
|
||||
"minY": childnode.getAttribute('minY'),
|
||||
"minZ": childnode.getAttribute('minZ'),
|
||||
"maxX": childnode.getAttribute('maxX'),
|
||||
"maxY": childnode.getAttribute('maxY'),
|
||||
"maxZ": childnode.getAttribute('maxZ')
|
||||
}
|
||||
elif node.nodeName == 'GeometryBounding':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'AABB':
|
||||
self.GeometryBounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')}
|
||||
self.GeometryBounding = {
|
||||
"minX": childnode.getAttribute('minX'),
|
||||
"minY": childnode.getAttribute('minY'),
|
||||
"minZ": childnode.getAttribute('minZ'),
|
||||
"maxX": childnode.getAttribute('maxX'),
|
||||
"maxY": childnode.getAttribute('maxY'),
|
||||
"maxZ": childnode.getAttribute('maxZ')
|
||||
}
|
||||
elif node.nodeName == 'Connectivity':
|
||||
for childnode in node.childNodes:
|
||||
if childnode.nodeName == 'Custom2DField':
|
||||
self.Fields2D.append(Field2D(type=int(childnode.getAttribute('type')), width=int(childnode.getAttribute('width')), height=int(childnode.getAttribute('height')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')), field2DRawData=str(childnode.firstChild.data)))
|
||||
self.Fields2D.append(
|
||||
Field2D(
|
||||
type=int(childnode.getAttribute('type')),
|
||||
width=int(childnode.getAttribute('width')),
|
||||
height=int(childnode.getAttribute('height')),
|
||||
angle=float(childnode.getAttribute('angle')),
|
||||
ax=float(childnode.getAttribute('ax')),
|
||||
ay=float(childnode.getAttribute('ay')),
|
||||
az=float(childnode.getAttribute('az')),
|
||||
tx=float(childnode.getAttribute('tx')),
|
||||
ty=float(childnode.getAttribute('ty')),
|
||||
tz=float(childnode.getAttribute('tz')),
|
||||
field2DRawData=str(childnode.firstChild.data)
|
||||
)
|
||||
)
|
||||
elif node.nodeName == 'Decoration':
|
||||
self.Decoration = {"faces": node.getAttribute('faces'), "subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')}
|
||||
self.Decoration = {
|
||||
"faces": node.getAttribute('faces'),
|
||||
"subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')
|
||||
}
|
||||
|
||||
|
||||
class Materials:
|
||||
def __init__(self, data):
|
||||
@@ -514,13 +613,14 @@ class Materials:
|
||||
b=int(node.getAttribute('Blue')),
|
||||
a=int(node.getAttribute('Alpha')),
|
||||
mtype=str(node.getAttribute('MaterialType'))
|
||||
)
|
||||
)
|
||||
|
||||
def getMaterialbyId(self, mid):
|
||||
return self.Materials[mid]
|
||||
|
||||
|
||||
class Material:
|
||||
def __init__(self,id, r, g, b, a, mtype):
|
||||
def __init__(self, id, r, g, b, a, mtype):
|
||||
self.id = id
|
||||
self.name = id
|
||||
self.mattype = mtype
|
||||
@@ -528,18 +628,25 @@ class Material:
|
||||
self.g = float(g)
|
||||
self.b = float(b)
|
||||
self.a = float(a)
|
||||
|
||||
def string(self):
|
||||
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format( self.r / 255, self.g / 255,self.b / 255)
|
||||
out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format(
|
||||
self.r / 255,
|
||||
self.g / 255,
|
||||
self.b / 255
|
||||
)
|
||||
if self.a < 255:
|
||||
out += 'Ni 1.575\n' + 'd {0}'.format(0.05) + '\n' + 'Tr {0}\n'.format(0.05)
|
||||
return out
|
||||
|
||||
|
||||
class DBinfo:
|
||||
def __init__(self, data):
|
||||
xml = minidom.parseString(data)
|
||||
self.Version = xml.getElementsByTagName('Bricks')[0].attributes['version'].value
|
||||
# print('DB Version: ' + str(self.Version))
|
||||
|
||||
|
||||
class DBFolderFile:
|
||||
def __init__(self, name, handle):
|
||||
self.handle = handle
|
||||
@@ -554,6 +661,7 @@ class DBFolderFile:
|
||||
finally:
|
||||
reader.close()
|
||||
|
||||
|
||||
class LIFFile:
|
||||
def __init__(self, name, offset, size, handle):
|
||||
self.handle = handle
|
||||
@@ -565,6 +673,7 @@ class LIFFile:
|
||||
self.handle.seek(self.offset, 0)
|
||||
return self.handle.read(self.size)
|
||||
|
||||
|
||||
class DBFolderReader:
|
||||
def __init__(self, folder):
|
||||
self.filelist = {}
|
||||
@@ -574,14 +683,14 @@ class DBFolderReader:
|
||||
|
||||
try:
|
||||
os.path.isdir(self.location)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.initok = False
|
||||
# print("db folder read FAIL")
|
||||
return
|
||||
else:
|
||||
self.parse()
|
||||
if self.fileexist(os.path.join(self.location,'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
|
||||
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location,'info.xml')].read())
|
||||
if self.fileexist(os.path.join(self.location, 'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')):
|
||||
self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location, 'info.xml')].read())
|
||||
# print("DB folder OK.")
|
||||
self.initok = True
|
||||
else:
|
||||
@@ -593,7 +702,6 @@ class DBFolderReader:
|
||||
# print(MATERIALNAMESPATH)
|
||||
pass
|
||||
|
||||
|
||||
def fileexist(self, filename):
|
||||
return filename in self.filelist
|
||||
|
||||
@@ -603,6 +711,7 @@ class DBFolderReader:
|
||||
entryName = os.path.join(path, name)
|
||||
self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName)
|
||||
|
||||
|
||||
class LIFReader:
|
||||
def __init__(self, file):
|
||||
self.packedFilesOffset = 84
|
||||
@@ -614,7 +723,7 @@ class LIFReader:
|
||||
try:
|
||||
self.filehandle = open(self.location, "rb")
|
||||
self.filehandle.seek(0, 0)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.initok = False
|
||||
# print("Database FAIL")
|
||||
return
|
||||
@@ -632,7 +741,7 @@ class LIFReader:
|
||||
# print("Database FAIL")
|
||||
self.initok = False
|
||||
|
||||
def fileexist(self,filename):
|
||||
def fileexist(self, filename):
|
||||
return filename in self.filelist
|
||||
|
||||
def parse(self, prefix='', offset=0):
|
||||
@@ -648,7 +757,7 @@ class LIFReader:
|
||||
entryType = self.readShort(offset=offset)
|
||||
offset += 6
|
||||
|
||||
entryName = '{0}{1}'.format(prefix,'/');
|
||||
entryName = '{0}{1}'.format(prefix, '/')
|
||||
self.filehandle.seek(offset + 1, 0)
|
||||
if sys.version_info < (3, 0):
|
||||
t = ord(self.filehandle.read(1))
|
||||
@@ -656,7 +765,7 @@ class LIFReader:
|
||||
t = int.from_bytes(self.filehandle.read(1), byteorder='big')
|
||||
|
||||
while not t == 0:
|
||||
entryName ='{0}{1}'.format(entryName,chr(t))
|
||||
entryName = '{0}{1}'.format(entryName, chr(t))
|
||||
self.filehandle.seek(1, 1)
|
||||
if sys.version_info < (3, 0):
|
||||
t = ord(self.filehandle.read(1))
|
||||
@@ -692,36 +801,37 @@ class LIFReader:
|
||||
else:
|
||||
return int.from_bytes(self.filehandle.read(2), byteorder='big')
|
||||
|
||||
|
||||
class Converter:
|
||||
def LoadDBFolder(self, dbfolderlocation):
|
||||
self.database = DBFolderReader(folder=dbfolderlocation)
|
||||
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation,'Materials.xml')):
|
||||
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation,'Materials.xml')].read());
|
||||
if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation, 'Materials.xml')):
|
||||
self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation, 'Materials.xml')].read())
|
||||
|
||||
def LoadDatabase(self,databaselocation):
|
||||
def LoadDatabase(self, databaselocation):
|
||||
self.database = LIFReader(file=databaselocation)
|
||||
|
||||
if self.database.initok and self.database.fileexist('/Materials.xml'):
|
||||
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read());
|
||||
self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read())
|
||||
|
||||
def LoadScene(self,filename):
|
||||
def LoadScene(self, filename):
|
||||
if self.database.initok:
|
||||
self.scene = Scene(file=filename)
|
||||
|
||||
def Export(self,filename):
|
||||
def Export(self, filename):
|
||||
invert = Matrix3D()
|
||||
#invert.n33 = -1 #uncomment to invert the Z-Axis
|
||||
# invert.n33 = -1 #uncomment to invert the Z-Axis
|
||||
|
||||
indexOffset = 1
|
||||
textOffset = 1
|
||||
usedmaterials = []
|
||||
geometriecache = {}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
out = open(filename + ".obj.tmp", "w+")
|
||||
out.truncate(0)
|
||||
out.write("mtllib " + filename + ".mtl" + '\n\n')
|
||||
outtext = open(filename + ".mtl.tmp", "w+")
|
||||
outtext.truncate(0)
|
||||
|
||||
total = len(self.scene.Bricks)
|
||||
current = 0
|
||||
@@ -733,12 +843,12 @@ class Converter:
|
||||
|
||||
if pa.designID not in geometriecache:
|
||||
geo = Geometry(designID=pa.designID, database=self.database)
|
||||
progress(current ,total , "(" + geo.designID + ") " + geo.Partname, ' ')
|
||||
progress(current, total, "(" + geo.designID + ") " + geo.Partname, ' ')
|
||||
geometriecache[pa.designID] = geo
|
||||
else:
|
||||
geo = geometriecache[pa.designID]
|
||||
|
||||
progress(current ,total , "(" + geo.designID + ") " + geo.Partname ,'-')
|
||||
progress(current, total, "(" + geo.designID + ") " + geo.Partname, '-')
|
||||
|
||||
out.write("o\n")
|
||||
|
||||
@@ -750,11 +860,11 @@ class Converter:
|
||||
# positions
|
||||
for j, p in enumerate(geo.Parts[part].outpositions):
|
||||
if (geo.Parts[part].bonemap[j] == i):
|
||||
p.transform( invert * b.matrix)
|
||||
p.transform(invert * b.matrix)
|
||||
# normals
|
||||
for k, n in enumerate(geo.Parts[part].outnormals):
|
||||
if (geo.Parts[part].bonemap[k] == i):
|
||||
n.transformW( invert * b.matrix)
|
||||
n.transformW(invert * b.matrix)
|
||||
|
||||
for point in geo.Parts[part].outpositions:
|
||||
out.write(point.string("v"))
|
||||
@@ -770,13 +880,12 @@ class Converter:
|
||||
last_color = 0
|
||||
for part in geo.Parts:
|
||||
|
||||
|
||||
#try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
|
||||
# try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file
|
||||
try:
|
||||
materialCurrentPart = pa.materials[part]
|
||||
last_color = pa.materials[part]
|
||||
except IndexError:
|
||||
# print('WARNING: {0}.g{1} has NO material assignment in lxf. Replaced with color {2}. Fix {0}.xml faces values.'.format(pa.designID, part, last_color))
|
||||
|
||||
materialCurrentPart = last_color
|
||||
|
||||
lddmat = self.allMaterials.getMaterialbyId(materialCurrentPart)
|
||||
@@ -784,7 +893,7 @@ class Converter:
|
||||
|
||||
deco = '0'
|
||||
if hasattr(pa, 'decoration') and len(geo.Parts[part].textures) > 0:
|
||||
#if decoCount <= len(pa.decoration):
|
||||
# if decoCount <= len(pa.decoration):
|
||||
if decoCount < len(pa.decoration):
|
||||
deco = pa.decoration[decoCount]
|
||||
decoCount += 1
|
||||
@@ -799,7 +908,7 @@ class Converter:
|
||||
f.write(self.database.filelist[decofilename].read())
|
||||
f.close()
|
||||
|
||||
if not matname in usedmaterials:
|
||||
if matname not in usedmaterials:
|
||||
usedmaterials.append(matname)
|
||||
outtext.write("newmtl " + matname + '\n')
|
||||
outtext.write(lddmat.string())
|
||||
@@ -809,9 +918,9 @@ class Converter:
|
||||
out.write("usemtl " + matname + '\n')
|
||||
for face in geo.Parts[part].faces:
|
||||
if len(geo.Parts[part].textures) > 0:
|
||||
out.write(face.string("f",indexOffset,textOffset))
|
||||
out.write(face.string("f", indexOffset, textOffset))
|
||||
else:
|
||||
out.write(face.string("f",indexOffset))
|
||||
out.write(face.string("f", indexOffset))
|
||||
|
||||
indexOffset += len(geo.Parts[part].outpositions)
|
||||
textOffset += len(geo.Parts[part].textures)
|
||||
@@ -823,55 +932,20 @@ class Converter:
|
||||
sys.stdout.write('%s\r' % (' '))
|
||||
# print("--- %s seconds ---" % (time.time() - start_time))
|
||||
|
||||
def setDBFolderVars(dbfolderlocation):
|
||||
|
||||
def setDBFolderVars(dbfolderlocation, lod):
|
||||
global PRIMITIVEPATH
|
||||
global GEOMETRIEPATH
|
||||
global DECORATIONPATH
|
||||
global MATERIALNAMESPATH
|
||||
PRIMITIVEPATH = os.path.join(dbfolderlocation, 'Primitives', '')
|
||||
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', 'lod0', '')
|
||||
GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', f'lod{lod}', '')
|
||||
DECORATIONPATH = os.path.join(dbfolderlocation, 'Decorations', '')
|
||||
MATERIALNAMESPATH = os.path.join(dbfolderlocation, 'MaterialNames', '')
|
||||
# print(MATERIALNAMESPATH)
|
||||
|
||||
def FindDatabase():
|
||||
lddliftree = os.getenv('LDDLIFTREE')
|
||||
if lddliftree is not None:
|
||||
if os.path.isdir(str(lddliftree)): #LDDLIFTREE points to folder
|
||||
return str(lddliftree)
|
||||
elif os.path.isfile(str(lddliftree)): #LDDLIFTREE points to file (should be db.lif)
|
||||
return str(lddliftree)
|
||||
|
||||
else: #Env variable LDDLIFTREE not set. Check for default locations per different platform.
|
||||
if platform.system() == 'Darwin':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
elif platform.system() == 'Windows':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
elif platform.system() == 'Linux':
|
||||
if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))
|
||||
elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))):
|
||||
return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))
|
||||
else:
|
||||
# print("no LDD database found please install LEGO-Digital-Designer")
|
||||
os._exit()
|
||||
else:
|
||||
# print('Your OS {0} is not supported yet.'.format(platform.system()))
|
||||
os._exit()
|
||||
|
||||
def progress(count, total, status='', suffix = ''):
|
||||
def progress(count, total, status='', suffix=''):
|
||||
bar_len = 40
|
||||
filled_len = int(round(bar_len * count / float(total)))
|
||||
percents = round(100.0 * count / float(total), 1)
|
||||
@@ -880,7 +954,8 @@ def progress(count, total, status='', suffix = ''):
|
||||
sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, status))
|
||||
sys.stdout.flush()
|
||||
|
||||
def main(lxf_filename, obj_filename):
|
||||
|
||||
def main(lxf_filename, obj_filename, lod="2"):
|
||||
# print("- - - pylddlib - - -")
|
||||
# print(" _ ")
|
||||
# print(" [_]")
|
||||
@@ -890,13 +965,15 @@ def main(lxf_filename, obj_filename):
|
||||
# print(" [=|=]")
|
||||
# print("")
|
||||
# print("- - - - - - - - - - - -")
|
||||
|
||||
global GEOMETRIEPATH
|
||||
GEOMETRIEPATH = GEOMETRIEPATH + f"LOD{lod}/"
|
||||
converter = Converter()
|
||||
# print("Found DB folder. Will use this instead of db.lif!")
|
||||
setDBFolderVars(dbfolderlocation = "app/luclient/res/")
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
378
app/reports.py
378
app/reports.py
@@ -1,58 +1,356 @@
|
||||
from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request
|
||||
from flask_user import login_required, current_user
|
||||
from app.models import db, CharacterInfo, Account, CharacterXML, ItemReports
|
||||
from flask import render_template, Blueprint, current_app
|
||||
from flask_user import login_required
|
||||
from app.models import CharacterInfo, Account, CharacterXML, Reports
|
||||
from app.luclient import get_lot_name
|
||||
from app import gm_level, scheduler
|
||||
import datetime, xmltodict
|
||||
from sqlalchemy.orm import load_only
|
||||
import datetime
|
||||
import xmltodict
|
||||
|
||||
reports_blueprint = Blueprint('reports', __name__)
|
||||
|
||||
|
||||
@reports_blueprint.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def index():
|
||||
items = ItemReports.query.distinct(ItemReports.date).group_by(ItemReports.date).all()
|
||||
reports_items = Reports.query.distinct(
|
||||
Reports.date
|
||||
).filter(
|
||||
Reports.report_type == "items"
|
||||
).group_by(Reports.date).options(load_only(Reports.date)).all()
|
||||
|
||||
reports_currency = Reports.query.distinct(
|
||||
Reports.date
|
||||
).filter(
|
||||
Reports.report_type == "currency"
|
||||
).group_by(Reports.date).options(load_only(Reports.date)).all()
|
||||
|
||||
reports_uscore = Reports.query.distinct(
|
||||
Reports.date
|
||||
).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,
|
||||
reports_currency=reports_currency,
|
||||
reports_uscore=reports_uscore,
|
||||
)
|
||||
|
||||
return render_template('reports/index.html.j2', items=items)
|
||||
|
||||
@reports_blueprint.route('/items/by_date/<date>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def items_by_date(date):
|
||||
items = ItemReports.query.filter(ItemReports.date==date).order_by(ItemReports.count.desc()).all()
|
||||
return render_template('reports/items/by_date.html.j2', items=items, date=date)
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
|
||||
return render_template('reports/items/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@scheduler.task("cron", id="gen_item_report", hour=3)
|
||||
def gen_item_report():
|
||||
char_xmls = CharacterXML.query.join(
|
||||
CharacterInfo,
|
||||
CharacterInfo.id==CharacterXML.id
|
||||
).join(
|
||||
Account,
|
||||
CharacterInfo.account_id==Account.id
|
||||
).filter(Account.gm_level < 3).all()
|
||||
date = datetime.date.today().strftime('%Y-%m-%d')
|
||||
for char_xml in char_xmls:
|
||||
character_json = xmltodict.parse(
|
||||
char_xml.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
for inv in character_json["obj"]["inv"]["items"]["in"]:
|
||||
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"])==0 or int(inv["attr_t"])==0):
|
||||
for item in inv["i"]:
|
||||
entry = ItemReports.query.filter(
|
||||
ItemReports.item == int(item["attr_l"]) and \
|
||||
ItemReports.date == date
|
||||
).first()
|
||||
if entry:
|
||||
entry.count = entry.count + int(item["attr_c"])
|
||||
entry.save()
|
||||
@reports_blueprint.route('/items/graph/<start>/<end>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def items_graph(start, end):
|
||||
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
|
||||
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
|
||||
entries = Reports.query.filter(
|
||||
Reports.report_type == "items"
|
||||
).filter(Reports.date.between(start_date, end_date)).all()
|
||||
# transform data for chartjs
|
||||
labels = []
|
||||
items = dict()
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
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():
|
||||
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:
|
||||
new_entry = ItemReports(
|
||||
item=int(item["attr_l"]),
|
||||
count=int(item["attr_c"]),
|
||||
date=date
|
||||
)
|
||||
new_entry.save()
|
||||
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 "Done"
|
||||
return render_template(
|
||||
'reports/graph.html.j2',
|
||||
labels=labels,
|
||||
datasets=datasets,
|
||||
name="Item",
|
||||
start=start,
|
||||
end=end,
|
||||
data_type="items",
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
@reports_blueprint.route('/currency/by_date/<date>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def currency_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
|
||||
return render_template('reports/currency/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@reports_blueprint.route('/currency/graph/<start>/<end>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def currency_graph(start, end):
|
||||
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
|
||||
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
|
||||
entries = Reports.query.filter(
|
||||
Reports.report_type == "currency"
|
||||
).filter(Reports.date.between(start_date, end_date)).all()
|
||||
characters = CharacterInfo.query.options(load_only(CharacterInfo.name)).all()
|
||||
labels = []
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
for entry in entries:
|
||||
if character.name in entry.data.keys():
|
||||
data.append(entry.data[character.name])
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + character.name.encode("utf-8").hex()[1:7]
|
||||
if max(data) > 10000:
|
||||
datasets.append({
|
||||
"label": character.name,
|
||||
"data": data,
|
||||
"backgroundColor": color,
|
||||
"borderColor": color
|
||||
})
|
||||
return render_template(
|
||||
'reports/graph.html.j2',
|
||||
labels=labels,
|
||||
datasets=datasets,
|
||||
name="Currency",
|
||||
start=start,
|
||||
end=end,
|
||||
data_type="currency",
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
@reports_blueprint.route('/uscore/by_date/<date>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def uscore_by_date(date):
|
||||
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
|
||||
return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
|
||||
|
||||
|
||||
@reports_blueprint.route('/uscore/graph/<start>/<end>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@gm_level(3)
|
||||
def uscore_graph(start, end):
|
||||
start_date = datetime.date.today() - datetime.timedelta(weeks=int(start))
|
||||
end_date = datetime.date.today() - datetime.timedelta(weeks=int(end))
|
||||
entries = Reports.query.filter(
|
||||
Reports.report_type == "uscore"
|
||||
).filter(Reports.date.between(start_date, end_date)).all()
|
||||
characters = CharacterInfo.query.options(load_only(CharacterInfo.name)).all()
|
||||
labels = []
|
||||
datasets = []
|
||||
# get stuff ready
|
||||
for entry in entries:
|
||||
labels.append(entry.date.strftime("%m/%d/%Y"))
|
||||
for character in characters:
|
||||
data = []
|
||||
for entry in entries:
|
||||
if character.name in entry.data.keys():
|
||||
data.append(entry.data[character.name])
|
||||
else:
|
||||
data.append(0)
|
||||
color = "#" + character.name.encode("utf-8").hex()[1:7]
|
||||
if max(data) > 1000:
|
||||
datasets.append({
|
||||
"label": character.name,
|
||||
"data": data,
|
||||
"backgroundColor": color,
|
||||
"borderColor": color
|
||||
})
|
||||
return render_template(
|
||||
'reports/graph.html.j2',
|
||||
labels=labels,
|
||||
datasets=datasets,
|
||||
name="U-Score",
|
||||
start=start,
|
||||
end=end,
|
||||
data_type="uscore",
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
|
||||
@scheduler.task("cron", id="gen_item_report", hour=23, timezone="UTC")
|
||||
def gen_item_report():
|
||||
with scheduler.app.app_context():
|
||||
try:
|
||||
current_app.logger.info("Start Item Report Generation")
|
||||
|
||||
date = datetime.date.today().strftime('%Y-%m-%d')
|
||||
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first()
|
||||
|
||||
# Only one report per day
|
||||
if report is not None:
|
||||
current_app.logger.info(f"Item Report Already Generated for {date}")
|
||||
return
|
||||
|
||||
char_xmls = CharacterXML.query.join(
|
||||
CharacterInfo,
|
||||
CharacterInfo.id == CharacterXML.id
|
||||
).join(
|
||||
Account,
|
||||
CharacterInfo.account_id == Account.id
|
||||
).filter(Account.gm_level < 3).all()
|
||||
|
||||
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,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
for inv in character_json["obj"]["inv"]["items"]["in"]:
|
||||
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"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
|
||||
else:
|
||||
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
|
||||
if name in report_data[item["attr_l"]]["chars"]:
|
||||
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] = int(item["attr_c"])
|
||||
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,
|
||||
report_type="items",
|
||||
date=date
|
||||
)
|
||||
|
||||
new_report.save()
|
||||
current_app.logger.info(f"Generated Item Report for {date}")
|
||||
except Exception as e:
|
||||
current_app.logger.critical(f"REPORT::ITEMS - {e}")
|
||||
return
|
||||
|
||||
|
||||
@scheduler.task("cron", id="gen_currency_report", hour=23, timezone="UTC")
|
||||
def gen_currency_report():
|
||||
with scheduler.app.app_context():
|
||||
try:
|
||||
current_app.logger.info("Start Currency Report Generation")
|
||||
|
||||
date = datetime.date.today().strftime('%Y-%m-%d')
|
||||
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first()
|
||||
|
||||
# Only one report per day
|
||||
if report is not None:
|
||||
current_app.logger.info(f"Currency Report Already Generated for {date}")
|
||||
return
|
||||
|
||||
characters = CharacterXML.query.join(
|
||||
CharacterInfo,
|
||||
CharacterInfo.id == CharacterXML.id
|
||||
).join(
|
||||
Account,
|
||||
CharacterInfo.account_id == Account.id
|
||||
).filter(Account.gm_level < 3).all()
|
||||
|
||||
report_data = {}
|
||||
|
||||
for character in characters:
|
||||
try:
|
||||
character_json = xmltodict.parse(
|
||||
character.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_cc"])
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::CURRENCY - ERROR PARSING CHARACTER {character.id}")
|
||||
current_app.logger.error(f"REPORT::CURRENCY - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=report_data,
|
||||
report_type="currency",
|
||||
date=date
|
||||
)
|
||||
|
||||
new_report.save()
|
||||
current_app.logger.info(f"Generated Currency Report for {date}")
|
||||
except Exception as e:
|
||||
current_app.logger.critical(f"REPORT::CURRENCY - {e}")
|
||||
return
|
||||
|
||||
|
||||
@scheduler.task("cron", id="gen_uscore_report", hour=23, timezone="UTC")
|
||||
def gen_uscore_report():
|
||||
with scheduler.app.app_context():
|
||||
try:
|
||||
current_app.logger.info("Start U-Score Report Generation")
|
||||
|
||||
date = datetime.date.today().strftime('%Y-%m-%d')
|
||||
report = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first()
|
||||
|
||||
# Only one report per day
|
||||
if report is not None:
|
||||
current_app.logger.info(f"U-Score Report Already Generated for {date}")
|
||||
return
|
||||
|
||||
characters = CharacterXML.query.join(
|
||||
CharacterInfo,
|
||||
CharacterInfo.id == CharacterXML.id
|
||||
).join(
|
||||
Account,
|
||||
CharacterInfo.account_id == Account.id
|
||||
).filter(Account.gm_level < 3).all()
|
||||
|
||||
report_data = {}
|
||||
|
||||
for character in characters:
|
||||
try:
|
||||
character_json = xmltodict.parse(
|
||||
character.xml_data,
|
||||
attr_prefix="attr_"
|
||||
)
|
||||
report_data[CharacterInfo.query.filter(CharacterInfo.id == character.id).first().name] = int(character_json["obj"]["char"]["attr_ls"])
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"REPORT::U-SCORE - ERROR PARSING CHARACTER {character.id}")
|
||||
current_app.logger.error(f"REPORT::U-SCORE - {e}")
|
||||
|
||||
new_report = Reports(
|
||||
data=report_data,
|
||||
report_type="uscore",
|
||||
date=date
|
||||
)
|
||||
|
||||
new_report.save()
|
||||
current_app.logger.info(f"Generated U-Score Report for {date}")
|
||||
except Exception as e:
|
||||
current_app.logger.critical(f"REPORT::U-SCORE - {e}")
|
||||
return
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
from flask_marshmallow import Marshmallow
|
||||
from app.models import *
|
||||
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
|
||||
@@ -44,7 +58,6 @@ class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
|
||||
ugc = ma.Nested(UGCSchema)
|
||||
|
||||
|
||||
|
||||
class PropertySchema(ma.SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
model = Property
|
||||
|
||||
@@ -4,6 +4,18 @@
|
||||
APP_NAME = "Nexus Dashboard"
|
||||
APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error"
|
||||
|
||||
APP_SECRET_KEY = ""
|
||||
APP_DATABASE_URI = "mysql+pymysql://<username>:<password>@<host>:<port>/<database>"
|
||||
|
||||
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
|
||||
|
||||
@@ -14,7 +26,7 @@ WTF_CSRF_TIME_LIMIT = 86400
|
||||
# Flask-User settings
|
||||
USER_APP_NAME = APP_NAME
|
||||
USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password
|
||||
USER_ENABLE_CHANGE_USERNAME = True # Allow users to change their username
|
||||
USER_ENABLE_CHANGE_USERNAME = False # Allow users to change their username
|
||||
USER_ENABLE_REGISTER = False # Allow new users to register
|
||||
|
||||
# Should alwyas be set to true
|
||||
@@ -22,18 +34,30 @@ USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password`
|
||||
USER_ENABLE_USERNAME = True # Register and Login with username
|
||||
|
||||
# Email Related Settings
|
||||
USER_ENABLE_EMAIL = True # Register with Email WILL - DISABLE OTHER THINGS TOO
|
||||
USER_ENABLE_EMAIL = False # Register with Email WILL - DISABLE OTHER THINGS TOO
|
||||
USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email
|
||||
USER_ENABLE_INVITE_USER = False # Allow users to be invited
|
||||
USER_REQUIRE_INVITATION = False # Only invited users may - WILL DISABLE REGISTRATION
|
||||
USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords
|
||||
|
||||
MAIL_SERVER = 'smtp.gmail.com'
|
||||
MAIL_PORT = 587
|
||||
MAIL_USE_SSL = False
|
||||
MAIL_USE_TLS = True
|
||||
MAIL_USERNAME = None
|
||||
MAIL_PASSWORD = None
|
||||
USER_EMAIL_SENDER_NAME = None
|
||||
USER_EMAIL_SENDER_EMAIL = None
|
||||
|
||||
# Require Play Key
|
||||
REQUIRE_PLAY_KEY = True
|
||||
|
||||
# Password hashing settings
|
||||
# Password hashing settings DO NOT CHANGE
|
||||
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
|
||||
13
app/static/chartjs/chart.min.js
vendored
Normal file
13
app/static/chartjs/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
0
app/static/css/.gitkeep
Normal file
0
app/static/css/.gitkeep
Normal file
@@ -8,7 +8,7 @@ $theme-colors: (
|
||||
$link-color: #005ac2;
|
||||
|
||||
@import "../bootstrap-4.2.1/scss/bootstrap";
|
||||
@import url(http://fonts.googleapis.com/css?family=Nunito:700);
|
||||
@import url(https://fonts.googleapis.com/css?family=Nunito:700);
|
||||
|
||||
body { font-family:'Nunito', Helvetica, Arial, sans-serif; }
|
||||
|
||||
@@ -92,3 +92,13 @@ body { font-family:'Nunito', Helvetica, Arial, sans-serif; }
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
div.dataTables_paginate {
|
||||
float: right;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
div.dataTables_filter{
|
||||
float: right;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
21
app/templates/accounts/edit_email.html.j2
Normal file
21
app/templates/accounts/edit_email.html.j2
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Edit E-mail for User {{ username }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Edit E-mail for User {{ username }}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.email) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
@@ -9,7 +9,7 @@
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<table class="table" id="accounts_table">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="accounts_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
@@ -41,7 +41,11 @@
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('accounts.get') }}",
|
||||
"columnDefs": [
|
||||
{ "searchable": false, "targets": [0,7] },
|
||||
{% if config.USER_ENABLE_EMAIL %}
|
||||
{ "searchable": false, "targets": [0,7] },
|
||||
{% else %}
|
||||
{ "searchable": false, "targets": [0,6] },
|
||||
{% endif %}
|
||||
{ "orderable": false, "targets": [0] }
|
||||
]
|
||||
});
|
||||
|
||||
@@ -84,16 +84,6 @@
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='bootstrap-4.2.1/js/bootstrap.bundle.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='datatables/datatables.min.js') }}"></script>
|
||||
<script type="sytylesheet" src="{{ url_for('static', filename='datatables/datatables.min.css') }}"></script>
|
||||
<script>
|
||||
// set the active nav-link item
|
||||
$(function () {
|
||||
let target_nav = '#{{request.endpoint}}'.replace('\.', '-');
|
||||
$(target_nav).addClass('active');
|
||||
});
|
||||
$(function () {
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
{{ status|capitalize }} Bug Reports
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<table class="table" id="accounts_table">
|
||||
<thead>
|
||||
{% block content_override %}
|
||||
<div class="mx-5">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="accounts_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Reporter</th>
|
||||
<th>Body</th>
|
||||
<th>Client Version</th>
|
||||
<th>Other Player</th>
|
||||
@@ -20,9 +22,10 @@
|
||||
<th>Submitted</th>
|
||||
<th>Resolved</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
Resolve Report {{ report.id }}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
{% block content_override %}
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 80vw;">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
@@ -75,4 +75,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
{% endblock content_override %}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
View Report {{ report.id }}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
{% block content_override %}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 80vw;">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row">
|
||||
@@ -102,4 +102,4 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% endblock content_override %}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Account Management
|
||||
Character Management
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Account Management
|
||||
Character Management
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table" id="characters_table">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="characters_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
|
||||
21
app/templates/character/rescue.html.j2
Normal file
21
app/templates/character/rescue.html.j2
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Character Rescue
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Character Rescue
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.save_world) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
22
app/templates/character/upload.html.j2
Normal file
22
app/templates/character/upload.html.j2
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Character XML Upload
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Character XML Upload
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<h3 style="color: orange;">PROCEED WITH CAUTION</h3>
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.char_xml) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -46,42 +46,55 @@
|
||||
<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 >= 2 %}
|
||||
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
<a class="dropdown-item" href='{{ url_for('mail.send') }}'>Send Mail</a>
|
||||
<hr/>
|
||||
<h6 class="text-center">Moderation</h6>
|
||||
<a class="dropdown-item" href='{{ url_for('moderation.index', status='unapproved') }}'>View Unapproved Items</a>
|
||||
<a class="dropdown-item" href='{{ url_for('moderation.index', status='approved') }}'>View Approved Items</a>
|
||||
<a class="dropdown-item" href='{{ url_for('moderation.index', status='all') }}'>View All Items</a>
|
||||
<hr/>
|
||||
<h6 class="text-center">Bug Reports</h6>
|
||||
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='unresolved') }}'>View Unresolved Reports</a>
|
||||
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='resolved') }}'>View Resolved Reports</a>
|
||||
<a class="dropdown-item" href='{{ url_for('bug_reports.index', status='all') }}'>View All Reports</a>
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 8 %}
|
||||
<hr/>
|
||||
<h6 class="text-center">Logs</h6>
|
||||
<a class="dropdown-item" href='{{ url_for('log.activity') }}'>Command Log</a>
|
||||
<a class="dropdown-item" href='{{ url_for('log.command') }}'>Activity Log</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% 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 %}
|
||||
|
||||
{# About always right most #}
|
||||
<a id='main-about' class='nav-link' href='{{ url_for('main.about') }}'>About</a>
|
||||
{% if current_user.is_authenticated and current_user.gm_level >= 2 %}
|
||||
<a id='report-index' class='nav-link' href='{{ url_for('reports.index') }}'>Reports</a>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tools</a>
|
||||
<div class="dropdown-menu">
|
||||
|
||||
<a class="dropdown-item text-center" href='{{ url_for('mail.send') }}'>Send Mail</a>
|
||||
<hr/>
|
||||
<h3 class="text-center">Moderation</h3>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='unapproved') }}'>Unapproved Items</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='approved') }}'>Approved Items</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('moderation.index', status='all') }}'>All Items</a>
|
||||
<hr/>
|
||||
<h3 class="text-center">Bug Reports</h3>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='unresolved') }}'>Unresolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='resolved') }}'>Resolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='all') }}'>All Reports</a>
|
||||
{% 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.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 %}
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user.gm_level == 0 %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Bug Reports</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='unresolved') }}'>Unresolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='resolved') }}'>Resolved Reports</a>
|
||||
<a class="dropdown-item text-center" href='{{ url_for('bug_reports.index', status='all') }}'>All Reports</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
{# About always right most #}
|
||||
<a id='main-about' class='nav-link' href='{{ url_for('main.about') }}'>About</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{# Only show logout if unauthenticated #}
|
||||
{% if current_user.is_authenticated %}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
Credit to sttng
|
||||
</a>
|
||||
<br/>
|
||||
{% if property_data %}
|
||||
{% if (property_data and current_user.gm_level >= 3) %}
|
||||
<a role="button" class="btn text-{% if property_data.mod_approved %}danger{% else %}success{% endif %} btn-block"
|
||||
href='{{url_for('properties.approve', id=property_data.id)}}'>
|
||||
{% if property_data.mod_approved %} Unapprove {% else %} Approve {% endif %}
|
||||
@@ -29,16 +29,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.5.0/jszip.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/super-three@0.116.0/build/three.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/super-three@0.116.0/examples/js/controls/OrbitControls.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/three@0.116.0/build/three.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/three@0.116.0/examples/js/controls/OrbitControls.js"></script>
|
||||
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='lddviewer/base64-binary.js') }}"></script>
|
||||
|
||||
<script type='module'>
|
||||
import {MTLLoader} from 'https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/jsm/loaders/MTLLoader.js'
|
||||
import {OBJLoader} from 'https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/jsm/loaders/OBJLoader.js'
|
||||
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'
|
||||
//Three.js stuff
|
||||
const scene = new THREE.Scene();
|
||||
let cammatr = new THREE.Matrix4();
|
||||
@@ -270,7 +269,6 @@
|
||||
|
||||
|
||||
class Scene{
|
||||
//partially done - need zip file handling
|
||||
constructor(){
|
||||
this.Bricks = []
|
||||
this.Scenecamera = []
|
||||
@@ -446,16 +444,16 @@
|
||||
constructor(designID, database){
|
||||
this.designID = designID
|
||||
this.Parts = []
|
||||
|
||||
this.studsFields2D = []
|
||||
let GeometryLocation = `${designID}.g`
|
||||
let PrimitiveLocation = `${designID}.xml`
|
||||
let lod = {{ lod }}
|
||||
let GeometryLocation = `brickprimitives/lod${lod}/${designID}.g`
|
||||
let PrimitiveLocation = `Primitives/${designID}.xml`
|
||||
|
||||
let GeometryCount = 0
|
||||
while (GeometryLocation in database.filelist) {
|
||||
this.Parts[GeometryCount] = new GeometryReader(database.filelist[GeometryLocation].read())
|
||||
GeometryCount = GeometryCount + 1
|
||||
GeometryLocation = `${designID}.g${GeometryCount}`
|
||||
GeometryLocation = `brickprimitives/lod${lod}/${designID}.g${GeometryCount}`
|
||||
}
|
||||
let primitive = new Primitive(database.filelist[PrimitiveLocation].read())
|
||||
this.Partname = primitive.Designname
|
||||
@@ -528,7 +526,7 @@
|
||||
|
||||
LoadDBURL(dbURLlocation){
|
||||
this.database = new DBURLReader(dbURLlocation)
|
||||
if(this.database.initok && this.database.fileexist('Materials.xml') && this.database.fileexist('localizedStrings.loc')){
|
||||
if(this.database.initok && this.database.fileexist('Materials.xml')){
|
||||
this.allMaterials = new Materials(this.database.filelist['Materials.xml'].read())
|
||||
}
|
||||
}
|
||||
@@ -568,13 +566,15 @@
|
||||
for (const bri of this.scene.Bricks){
|
||||
current += 1
|
||||
for (const pa of bri.Parts){
|
||||
let geo = new Geometry(pa.designID, this.database)
|
||||
if (!(pa.designID in geometriecache)) {
|
||||
let geo = new Geometry(pa.designID, this.database)
|
||||
geometriecache[pa.designID] = geo
|
||||
let geo = 0
|
||||
if (geometriecache.hasOwnProperty(pa.designID)) {
|
||||
// console.log(`Re-use brick ${pa.designID}`)
|
||||
geo = geometriecache[pa.designID]
|
||||
}
|
||||
else {
|
||||
geo = geometriecache[pa.designID]
|
||||
// console.log(`New brick ${pa.designID}`)
|
||||
geo = new Geometry(pa.designID, this.database)
|
||||
geometriecache[pa.designID] = geo
|
||||
}
|
||||
|
||||
let ind = 0
|
||||
@@ -675,7 +675,7 @@
|
||||
console.log('partindex: ' + partindex)
|
||||
console.log(pa.materials)
|
||||
|
||||
let lddmat = allMaterials.getMaterialbyId(21)
|
||||
lddmat = allMaterials.getMaterialbyId(21)
|
||||
}
|
||||
|
||||
let deco = '0'
|
||||
@@ -724,11 +724,8 @@
|
||||
// let vnh = new VertexNormalsHelper( mesh, 5 );
|
||||
// scene.add( vnh );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,7 +858,6 @@
|
||||
|
||||
|
||||
class Materials {
|
||||
//done
|
||||
constructor(data) {
|
||||
this.Materials = {}
|
||||
let parser = new DOMParser();
|
||||
@@ -908,13 +904,11 @@
|
||||
|
||||
|
||||
function FindDBURL(){
|
||||
let dburl = 'https://json.aronwk.com/LDD-DB/'
|
||||
let dburl = '/luclient/ldddb/'
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', dburl, false); // `false` makes the request synchronous
|
||||
|
||||
// request state change event
|
||||
xhr.onreadystatechange = function() {
|
||||
|
||||
// request completed?
|
||||
if (xhr.readyState !== 4) {//return;
|
||||
dburl = false;
|
||||
@@ -930,7 +924,6 @@
|
||||
console.log('HTTP error in FindDBURL:', xhr.status, xhr.statusText);
|
||||
}
|
||||
};
|
||||
|
||||
// start request
|
||||
xhr.send();
|
||||
return dburl
|
||||
@@ -999,7 +992,7 @@
|
||||
return self.filelist[filename];
|
||||
}
|
||||
|
||||
parse(dburl) {
|
||||
parse(dburl, folder="") {
|
||||
let self = this;
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', dburl, false);
|
||||
@@ -1018,7 +1011,7 @@
|
||||
let obj = data[i];
|
||||
if (obj.type == 'directory'){
|
||||
// parse subdirs
|
||||
self.parse(dburl + obj.name + '/')
|
||||
self.parse(dburl + obj.name + '/', obj.name)
|
||||
}
|
||||
else if (obj.type == 'file'){
|
||||
self.filelist[obj.name] = new DBURLFile(dburl + obj.name, obj.name)
|
||||
@@ -1043,7 +1036,7 @@
|
||||
let lxfml_file_list = [
|
||||
{% for model in content %}
|
||||
{% if model.lot == 14 %}
|
||||
"{{url_for('properties.get_model', id=model.id, file_format='lxfml')}}"{{ ", " if not loop.last else "" }}
|
||||
"{{url_for('properties.get_model', id=model.id, file_format='lxfml', lod=lod)}}"{{ ", " if not loop.last else "" }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table" id="command_table">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="command_table">
|
||||
<thead>
|
||||
<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>
|
||||
|
||||
46
app/templates/logs/audit.html.j2
Normal file
46
app/templates/logs/audit.html.j2
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Audit Log
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Audit Log
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
{% if message %}
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h3>{{ message }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="audit_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Account</th>
|
||||
<th>Command</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
let audit_table = $('#audit_table').DataTable({
|
||||
"order": [[0, "desc"]],
|
||||
"processing": true,
|
||||
"serverSide": true,
|
||||
"ajax": "{{ url_for('log.get_audits') }}",
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -17,14 +17,12 @@
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table" id="activity_table">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="activity_table">
|
||||
<thead>
|
||||
<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>
|
||||
|
||||
15
app/templates/logs/system.html.j2
Normal file
15
app/templates/logs/system.html.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}LOGS{% endblock %}
|
||||
|
||||
{% block content_before %}
|
||||
LOGS - {{ config.APP_NAME }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_override %}
|
||||
<div class="bg-white mx-5 p-3 rounded">
|
||||
<code>
|
||||
{{ logs }}
|
||||
</code>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -3,66 +3,92 @@
|
||||
{% block title %}About{% endblock %}
|
||||
|
||||
{% block content_before %}
|
||||
About {{ config.APP_NAME }}
|
||||
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">Contributors</h4>
|
||||
<h4 class="text-center">Staff</h4>
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Developer:
|
||||
{% for mod in mods %}
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
{{ mod.username }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{% with gm_level=mod.gm_level %}
|
||||
{% include 'partials/_gm_level.html.j2' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
Aronwk (Aaron Kimbrell)
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Developer:
|
||||
</div>
|
||||
<div class="col">
|
||||
Wincent
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Logo Designer:
|
||||
</div>
|
||||
<div class="col">
|
||||
BlasterBuilder
|
||||
</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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
LDD/LXFML Rendering:
|
||||
{% 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>
|
||||
<div class="col">
|
||||
m2m/sttng
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
TODO: add more Contributors
|
||||
</div>
|
||||
<div class="col">
|
||||
Add more
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Source
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="https://github.com/DarkflameUniverse/AccountManager">
|
||||
<a href="https://github.com/DarkflameUniverse/NexusDashboard">
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -8,59 +8,63 @@
|
||||
Moderation of {{ status|capitalize }} Items
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<h4> Characters </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table" id="character_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Account</th>
|
||||
<th>Name</th>
|
||||
<th>Pending Name</th>
|
||||
<th>Needs Rename</th>
|
||||
<th>Last Login</th>
|
||||
<th>Permission Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<h4> Pets </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table" id="pet_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<h4> Properties </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table" id="property_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Owner</th>
|
||||
<th>Template ID</th>
|
||||
<th>Clone ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Privacy</th>
|
||||
<th>Approved</th>
|
||||
<th>Updated</th>
|
||||
<th>Claimed</th>
|
||||
<th>Rejection Reason</th>
|
||||
<th>Reputation</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
{% block content_override %}
|
||||
<div class="mx-5">
|
||||
<h4> Characters </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="character_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Account</th>
|
||||
<th>Name</th>
|
||||
<th>Pending Name</th>
|
||||
<th>Needs Rename</th>
|
||||
<th>Last Login</th>
|
||||
<th>Permission Map</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<h4> Pets </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="pet_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br/>
|
||||
<h4> Properties </h4>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="property_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Owner</th>
|
||||
<th>Template ID</th>
|
||||
<th>Clone ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Privacy</th>
|
||||
<th>Approved</th>
|
||||
<th>Updated</th>
|
||||
<th>Claimed</th>
|
||||
<th>Rejection Reason</th>
|
||||
<th>Reputation</th>
|
||||
<th>Performance Cost</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<br/>
|
||||
<div class="col">
|
||||
{{ account_data.email }}
|
||||
{% if current_user.gm_level >= 8 and not(current_user.gm_level == 8 and account_data.gm_level == 8)%}
|
||||
<a role="button" class="btn btn-primary"
|
||||
href='{{ url_for('accounts.edit_email', id=account_data.id) }}'>
|
||||
Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -102,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 %}
|
||||
@@ -141,9 +153,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.id != account_data.id and current_user.gm_level > 3 %}
|
||||
{% if current_user.id != account_data.id and current_user.gm_level > 3 %}
|
||||
<hr class="bg-primary"/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
@@ -191,6 +203,42 @@
|
||||
Mute for 1 year
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.gm_level == 9 %}
|
||||
<button type="button" class="btn btn-danger btn-block" data-toggle="modal" data-target="#deleteModal">
|
||||
Delete Account
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_user.gm_level == 9 %}
|
||||
{# delete Modal #}
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title" id="deleteModalLabel">
|
||||
Permanently Delete this Account?
|
||||
</h1>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h2> This can NOT be undone! </h2>
|
||||
<br/>
|
||||
This user is a GM {{account_data.gm_level}} !!!<br/>
|
||||
This will delete their everything, including but not limited to:<br/>
|
||||
Properties, Audit Logs, Bug Reports, and Invitations!
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a role="button" class="btn btn-danger btn-block"
|
||||
href='{{ url_for('accounts.delete', id=account_data.id) }}'>
|
||||
Permanently Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -41,28 +41,38 @@
|
||||
</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/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('accounts.view', id=character.account_id) }}'>
|
||||
View Account: {{character.account.username}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<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.id != character.account_id and current_user.gm_level > 2 %}
|
||||
{% if current_user.gm_level > 2 %}
|
||||
<hr class="bg-primary"/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
@@ -83,6 +93,10 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a role="button" class="btn btn-warning btn-block"
|
||||
href='{{ url_for('characters.rescue', id=character.id) }}'>
|
||||
Rescue
|
||||
</a>
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('characters.restrict', id=character.id, bit=4) }}'>
|
||||
{% if character.permission_map|check_perm_map(4) %}Unrestrict{% else %}Restrict{% endif %} Trade
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h4>
|
||||
Chatacter Data
|
||||
Character Data
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
{% set parsed_lzid = character_json.obj.char.attr_lzid|parse_lzid %}
|
||||
Zone: {{ parsed_lzid[0]|get_zone_name }}<br>
|
||||
Zone: {{ character_json.obj.char.attr_lwid|get_zone_name }}<br>
|
||||
Zone Instance: {{ parsed_lzid[1] }}<br>
|
||||
Zone Clone: {{ parsed_lzid[2] }}<br>
|
||||
</div>
|
||||
@@ -63,9 +63,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"/>
|
||||
@@ -110,9 +114,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -120,9 +130,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -130,9 +146,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -140,9 +162,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -150,9 +178,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -160,9 +194,15 @@
|
||||
{# 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 %}
|
||||
{% if item.i is iterable and (item.i is not string and item.i is not mapping) %}
|
||||
{% for inv_item in item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% with inv_item=item.i %}
|
||||
{% include 'partials/charxml/_inv_grid.html.j2' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -187,10 +227,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>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if gm_level==0 %}
|
||||
Player
|
||||
{% elif gm_level==1 %}
|
||||
Key Distrubuter
|
||||
Elevevated Civilan {# Unused #}
|
||||
{% elif gm_level==2 %}
|
||||
Junior Moderator
|
||||
{% elif gm_level==3 %}
|
||||
|
||||
@@ -69,6 +69,14 @@
|
||||
{{ property.reputation }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Performance Cost:
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ property.performance_cost }}
|
||||
</div>
|
||||
</div>
|
||||
{% if request.endpoint != "properties.view" %}
|
||||
<br/>
|
||||
<div class="row">
|
||||
@@ -81,11 +89,24 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<br/>
|
||||
<h5 class="text-center">Render Quality</h5>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_models', id=property.id) }}'>
|
||||
Render Property
|
||||
href='{{ url_for('properties.view_models', id=property.id, lod=0) }}'>
|
||||
High (0)
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_models', id=property.id, lod=1) }}'>
|
||||
Med (1)
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_models', id=property.id, lod=2) }}'>
|
||||
Low (2)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +117,13 @@
|
||||
href='{{url_for('properties.approve', id=property.id)}}'>
|
||||
{% if property.mod_approved %} Unapprove {% else %} Approve {% endif %}
|
||||
</a>
|
||||
{% if not property.rejection_reason %}
|
||||
<br/>
|
||||
<a role="button" class="btn btn-danger btn-block"
|
||||
href='{{url_for('properties.reject', id=property.id)}}'>
|
||||
Reject
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,21 +44,12 @@
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.get_model', id=item.id, file_format="lxfml") }}'>
|
||||
href='{{ url_for('properties.get_model', id=item.id, file_format="lxfml", lod=0) }}'>
|
||||
View Model XML
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_model', id=item.id) }}'>
|
||||
Render Model
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
@@ -67,5 +58,27 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<h5 class="text-center">Render Quality</h5>
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_model', id=item.id, lod=0) }}'>
|
||||
High (0)
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_model', id=item.id, lod=1) }}'>
|
||||
Med (1)
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-center">
|
||||
<a role="button" class="btn btn-primary btn-block"
|
||||
href='{{ url_for('properties.view_model', id=item.id, lod=2) }}'>
|
||||
Low (2)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
>
|
||||
{% if inv_item.attr_c != "1" %}
|
||||
<span class="inventory-count text-bold">
|
||||
{{ inv_item.attr_c }}
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
+999
|
||||
{% else %}
|
||||
{{ inv_item.attr_c }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if inv_item.attr_b == "true" %}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<br/>
|
||||
--------------------------------
|
||||
<br/>
|
||||
{{ ("ItemSets_" ~ item_set[0] ~ "_kitName")|lu_translate }}: Rank {{ item_set[4] }}<br/>
|
||||
|
||||
{{ ("ItemSets_" ~ item_set[0] ~ "_kitName")|lu_translate }}{% if item_set[4]|int > 0%}: Rank {{ item_set[4] }}{% endif %}<br/>
|
||||
{% if item_set[5] %}
|
||||
<img src='/luclient/get_icon_iconid/{{item_set[5]}}'
|
||||
alt='Kit Image'
|
||||
@@ -65,3 +66,7 @@
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{%if inv_item.attr_c|int > 999 %}
|
||||
<br />Count: {{ inv_item.attr_c|numberFormat }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@
|
||||
{% set skill_desc = skill[0]|get_skill_desc %}
|
||||
{% if "IP" not in skill_desc and "AP" not in skill_desc and "LP" not in skill_desc and skill[0]|string not in skill_desc %}
|
||||
<br/>
|
||||
<img src='{{ url_for('luclient.get_icon_iconid', id=skill[1]) }}'
|
||||
alt='Skill: '
|
||||
width='32'
|
||||
height='32'>
|
||||
{% if skill[1]%}
|
||||
<img src='{{ url_for('luclient.get_icon_iconid', id=skill[1]) }}'
|
||||
alt='Skill: '
|
||||
width='32'
|
||||
height='32'>
|
||||
{% endif %}
|
||||
{{ skill[0]|get_skill_desc }}
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
Bulk Create Play Keys
|
||||
</a>
|
||||
<hr class="bg-primary"/>
|
||||
<table class="table" id="play_key_table">
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="play_key_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col text-right">
|
||||
Uses Left:
|
||||
Times Used:
|
||||
</div>
|
||||
<br/>
|
||||
<div class="col">
|
||||
{{ key.key_uses}}
|
||||
{{ key.times_used}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -8,35 +8,38 @@
|
||||
Property Management
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
{% if message %}
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h3>{{ message }}</h3>
|
||||
{% block content_override %}
|
||||
<div class="mx-5">
|
||||
{% if message %}
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h3>{{ message }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table" id="properties_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Owner</th>
|
||||
<th>Template ID</th>
|
||||
<th>Clone ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Privacy</th>
|
||||
<th>Approved</th>
|
||||
<th>Updated</th>
|
||||
<th>Claimed</th>
|
||||
<th>Rejection Reason</th>
|
||||
<th>Reputation</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<table class="table table-dark table-striped table-bordered table-hover" id="properties_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Owner</th>
|
||||
<th>Template ID</th>
|
||||
<th>Clone ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Privacy</th>
|
||||
<th>Approved</th>
|
||||
<th>Updated</th>
|
||||
<th>Claimed</th>
|
||||
<th>Rejection Reason</th>
|
||||
<th>Reputation</th>
|
||||
<th>Performance Cost</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
32
app/templates/properties/reject.html.j2
Normal file
32
app/templates/properties/reject.html.j2
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Viewing {{property_data.owner.name}}'s
|
||||
{% if property_data.name %}
|
||||
{{ property_data.name }}
|
||||
{% else %}
|
||||
{{ property_data.zone_id|get_zone_name }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content_before %}
|
||||
Viewing {{property_data.owner.name}}'s
|
||||
{% if property_data.name %}
|
||||
{{ property_data.name }}
|
||||
{% else %}
|
||||
{{ property_data.zone_id|get_zone_name }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method=post>
|
||||
{{ form.csrf_token }}
|
||||
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
|
||||
<div class="card-body">
|
||||
{{ helper.render_field(form.rejection_reason) }}
|
||||
{{ helper.render_submit_field(form.submit) }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
||||
51
app/templates/reports/currency/by_date.html.j2
Normal file
51
app/templates/reports/currency/by_date.html.j2
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
Currency on {{ date }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
Currency on {{ date }}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<div class='table-responsive'>
|
||||
<table class="table table-dark table-striped table-bordered table-hover"
|
||||
id="currency_by_date"
|
||||
data-order='[[ 1, "desc" ]]'>
|
||||
<thead>
|
||||
<th scope="col">
|
||||
Character
|
||||
</th>
|
||||
<th scope="col">
|
||||
Currency
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name, currency in data.items() %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ currency }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
let currency_by_date = $('#currency_by_date').DataTable({
|
||||
"processing": false,
|
||||
"serverSide": false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
104
app/templates/reports/graph.html.j2
Normal file
104
app/templates/reports/graph.html.j2
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
{{ name }} History for {{start_date}} to {{end_date}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
{{ name }} History for {{start_date}} to {{end_date}}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<div class ="row">
|
||||
{% if data_type == "items" %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.items_graph', start=(start|int+1), end=(end|int+1))}}'>
|
||||
Previous
|
||||
</a>
|
||||
</div>
|
||||
{% if end|int > 0 %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.items_graph', start=(start|int-1), end=(end|int-1))}}'>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif data_type == "currency" %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.currency_graph', start=(start|int+1), end=(end|int+1))}}'>
|
||||
Previous
|
||||
</a>
|
||||
</div>
|
||||
{% if end|int > 0 %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.currency_graph', start=(start|int-1), end=(end|int-1))}}'>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif data_type == "uscore" %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.uscore_graph', start=(start|int+1), end=(end|int+1))}}'>
|
||||
Previous
|
||||
</a>
|
||||
</div>
|
||||
{% if end|int > 0 %}
|
||||
<div class="col">
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.uscore_graph', start=(start|int-1), end=(end|int-1))}}'>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h1> INVALID DATA TYPE </h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr/>
|
||||
<canvas id="item_chart"></canvas>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='chartjs/chart.min.js') }}"></script>
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
let config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {{labels}},
|
||||
datasets: {{datasets}},
|
||||
},
|
||||
options: {
|
||||
plugins: { legend: { display: false }, },
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Date'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: '{{ name }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let items_chart = new Chart(
|
||||
document.getElementById('item_chart'),
|
||||
config
|
||||
);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -11,23 +11,43 @@
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
Items: <br/>
|
||||
{% for item in items %}
|
||||
Items:
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.items_graph', start=1, end=0)}}'>
|
||||
Graph
|
||||
</a><br/>
|
||||
{% for report in reports_items|reverse %}
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.items_by_date', date=item.date)}}'>
|
||||
{{item.date}}
|
||||
href='{{url_for('reports.items_by_date', date=report.date)}}'>
|
||||
{{report.date}}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col">
|
||||
Currency:
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.currency_graph', start=1, end=0)}}'>
|
||||
Graph
|
||||
</a><br/>
|
||||
{% for report in reports_currency|reverse %}
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.currency_by_date', date=report.date)}}'>
|
||||
{{report.date}}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col">
|
||||
U-Score:
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.uscore_graph', start=1, end=0)}}'>
|
||||
Graph
|
||||
</a><br/>
|
||||
{% for report in reports_uscore|reverse %}
|
||||
<a role="button" class="btn btn-primary btn btn-block"
|
||||
href='{{url_for('reports.uscore_by_date', date=report.date)}}'>
|
||||
{{report.date}}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
{% block content %}
|
||||
<div class='table-responsive'>
|
||||
<table class="table table-dark table-striped table-bordered table-hover"
|
||||
id="two_weeks"
|
||||
data-order='[[ 1, "asc" ]]'
|
||||
data-page-length='25'>
|
||||
id="items_by_date"
|
||||
data-order='[[ 1, "desc" ]]'>
|
||||
<thead>
|
||||
<th scope="col">
|
||||
Item
|
||||
@@ -21,21 +20,37 @@
|
||||
<th scope="col">
|
||||
Count
|
||||
</th>
|
||||
<th scope="col">
|
||||
Breakdown
|
||||
</th>
|
||||
<th scope="col">
|
||||
Rarity
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
{% for lot, details in data.items() %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ item.item|get_lot_name }}
|
||||
{{ lot|get_lot_name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.count }}
|
||||
{% if details.chars %}
|
||||
{{ details.item_count }}
|
||||
{% else %}
|
||||
{{ details }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.item|get_lot_rarity }}
|
||||
{% 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 }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -44,3 +59,14 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
let items_by_date = $('#items_by_date').DataTable({
|
||||
"processing": false,
|
||||
"serverSide": false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
51
app/templates/reports/uscore/by_date.html.j2
Normal file
51
app/templates/reports/uscore/by_date.html.j2
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}
|
||||
U-Score on {{ date }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content_before %}
|
||||
U-Score on {{ date }}
|
||||
{% endblock content_before %}
|
||||
|
||||
{% block content %}
|
||||
<div class='table-responsive'>
|
||||
<table class="table table-dark table-striped table-bordered table-hover"
|
||||
id="uscore_by_date"
|
||||
data-order='[[ 1, "desc" ]]'>
|
||||
<thead>
|
||||
<th scope="col">
|
||||
Character
|
||||
</th>
|
||||
<th scope="col">
|
||||
U-Score
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for name, uscore in data.items() %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ uscore }}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ super () }}
|
||||
<script>
|
||||
$(document).ready(function(){
|
||||
let uscore_by_date = $('#uscore_by_date').DataTable({
|
||||
"processing": false,
|
||||
"serverSide": false,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
18
app/templates/status_codes/500.html.j2
Normal file
18
app/templates/status_codes/500.html.j2
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends 'base.html.j2' %}
|
||||
|
||||
{% block title %}ERROR{% endblock %}
|
||||
|
||||
{% block content_before %}
|
||||
ERROR - {{ config.APP_NAME }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if current_user.gm_level == 9 %}
|
||||
<code>
|
||||
{{ exception }}
|
||||
</code>
|
||||
{% else %}
|
||||
<h2 class="text-center">An Error has Occurred!!!</h2>
|
||||
<div>Please Report this to an Admin</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
2
entrypoint.bat
Normal file
2
entrypoint.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
python3 -m flask db upgrade
|
||||
python3 wsgi.py
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/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
|
||||
|
||||
# update the DB
|
||||
flask db upgrade
|
||||
|
||||
# RUNNNNNNNNNNNNN
|
||||
gunicorn -b :8000 -w 4 wsgi:app
|
||||
|
||||
0
logs/.gitkeep
Normal file
0
logs/.gitkeep
Normal file
38
migrations/versions/3132aaef7413_fix_nullables.py
Normal file
38
migrations/versions/3132aaef7413_fix_nullables.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""fix nullables
|
||||
|
||||
Revision ID: 3132aaef7413
|
||||
Revises: bd908969d8fe
|
||||
Create Date: 2022-02-11 21:51:58.479066
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3132aaef7413'
|
||||
down_revision = 'bd908969d8fe'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('audit_logs', 'account_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=False)
|
||||
op.alter_column('audit_logs', 'action',
|
||||
existing_type=mysql.TEXT(),
|
||||
nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('audit_logs', 'action',
|
||||
existing_type=mysql.TEXT(),
|
||||
nullable=True)
|
||||
op.alter_column('audit_logs', 'account_id',
|
||||
existing_type=mysql.INTEGER(display_width=11),
|
||||
nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -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',
|
||||
|
||||
51
migrations/versions/8e52b5c7568a_reporter_id.py
Normal file
51
migrations/versions/8e52b5c7568a_reporter_id.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""reporter_id
|
||||
|
||||
Revision ID: 8e52b5c7568a
|
||||
Revises: fa97b0d0c351
|
||||
Create Date: 2022-04-02 17:35:54.814007
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy.engine import reflection
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8e52b5c7568a'
|
||||
down_revision = 'fa97b0d0c351'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if not _table_has_column('bug_reports', 'reporter_id'):
|
||||
op.add_column(
|
||||
'bug_reports',
|
||||
sa.Column(
|
||||
'reporter_id',
|
||||
mysql.INTEGER(),
|
||||
server_default='0',
|
||||
nullable=False
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('bug_reports', 'reporter_id')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
def _table_has_column(table, column):
|
||||
config = op.get_context().config
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section), prefix='sqlalchemy.')
|
||||
insp = reflection.Inspector.from_engine(engine)
|
||||
has_column = False
|
||||
for col in insp.get_columns(table):
|
||||
if column not in col['name']:
|
||||
continue
|
||||
has_column = True
|
||||
return has_column
|
||||
@@ -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 ###
|
||||
|
||||
33
migrations/versions/aee4c6c24811_reports.py
Normal file
33
migrations/versions/aee4c6c24811_reports.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""reports
|
||||
|
||||
Revision ID: aee4c6c24811
|
||||
Revises: 8a2966b9f7ee
|
||||
Create Date: 2022-01-16 20:12:39.816567
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'aee4c6c24811'
|
||||
down_revision = '8a2966b9f7ee'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('reports',
|
||||
sa.Column('data', sa.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 ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('reports')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,28 @@
|
||||
"""make pet owner not a forein key
|
||||
|
||||
Revision ID: b470795db8e1
|
||||
Revises: e3e8e05f27ee
|
||||
Create Date: 2022-02-12 20:51:12.318782
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b470795db8e1'
|
||||
down_revision = 'e3e8e05f27ee'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('pet_names_ibfk_1', 'pet_names', type_='foreignkey')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_foreign_key('pet_names_ibfk_1', 'pet_names', 'charinfo', ['owner_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,33 +0,0 @@
|
||||
"""itemreport table
|
||||
|
||||
Revision ID: b89c21b5112f
|
||||
Revises: 8a2966b9f7ee
|
||||
Create Date: 2022-01-15 19:21:44.544653
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b89c21b5112f'
|
||||
down_revision = '8a2966b9f7ee'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('item_reports',
|
||||
sa.Column('item', sa.Integer(), nullable=False),
|
||||
sa.Column('count', sa.Integer(), nullable=False),
|
||||
sa.Column('date', sa.Date(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('item', 'date')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('item_reports')
|
||||
# ### end Alembic commands ###
|
||||
35
migrations/versions/bd908969d8fe_add_audit_log_table.py
Normal file
35
migrations/versions/bd908969d8fe_add_audit_log_table.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Add audit_log table
|
||||
|
||||
Revision ID: bd908969d8fe
|
||||
Revises: aee4c6c24811
|
||||
Create Date: 2022-02-11 21:48:03.798474
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'bd908969d8fe'
|
||||
down_revision = 'aee4c6c24811'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('audit_logs',
|
||||
sa.Column('id', mysql.INTEGER(), nullable=False),
|
||||
sa.Column('account_id', sa.Integer(), nullable=True),
|
||||
sa.Column('action', mysql.TEXT(), nullable=True),
|
||||
sa.Column('date', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
|
||||
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('audit_logs')
|
||||
# ### end Alembic commands ###
|
||||
30
migrations/versions/e3e8e05f27ee_pet_owners.py
Normal file
30
migrations/versions/e3e8e05f27ee_pet_owners.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""pet owners
|
||||
|
||||
Revision ID: e3e8e05f27ee
|
||||
Revises: 3132aaef7413
|
||||
Create Date: 2022-02-11 23:18:20.978203
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e3e8e05f27ee'
|
||||
down_revision = '3132aaef7413'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('pet_names', sa.Column('owner_id', mysql.BIGINT(), nullable=True))
|
||||
op.create_foreign_key(None, 'pet_names', 'charinfo', ['owner_id'], ['id'], ondelete='CASCADE')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint(None, 'pet_names', type_='foreignkey')
|
||||
op.drop_column('pet_names', 'owner_id')
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,56 @@
|
||||
"""property performance index
|
||||
|
||||
Revision ID: fa97b0d0c351
|
||||
Revises: b470795db8e1
|
||||
Create Date: 2022-03-31 10:38:06.367277
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy.engine import reflection
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fa97b0d0c351'
|
||||
down_revision = 'b470795db8e1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
if not _table_has_column('properties', 'performance_cost'):
|
||||
op.add_column(
|
||||
'properties',
|
||||
sa.Column(
|
||||
'performance_cost',
|
||||
mysql.DOUBLE(
|
||||
precision=20,
|
||||
scale=15,
|
||||
asdecimal=False
|
||||
),
|
||||
server_default='0.0',
|
||||
nullable=True
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('properties', 'performance_cost')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def _table_has_column(table, column):
|
||||
config = op.get_context().config
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section), prefix='sqlalchemy.')
|
||||
insp = reflection.Inspector.from_engine(engine)
|
||||
has_column = False
|
||||
for col in insp.get_columns(table):
|
||||
if column not in col['name']:
|
||||
continue
|
||||
has_column = True
|
||||
return has_column
|
||||
0
property_files/.gitkeep
Normal file
0
property_files/.gitkeep
Normal file
6
pylama.ini
Normal file
6
pylama.ini
Normal file
@@ -0,0 +1,6 @@
|
||||
[pylama]
|
||||
ignore = D203, D212, D213, D406, D407, D408, D409, D100, D104, D401, F722
|
||||
max_line_length = 160
|
||||
|
||||
[pylama:mccabe]
|
||||
max-complexity = 35
|
||||
@@ -28,7 +28,7 @@ itsdangerous==2.0.1
|
||||
Jinja2==3.0.1
|
||||
lazy-object-proxy==1.7.1
|
||||
libsass==0.21.0
|
||||
Mako==1.1.6
|
||||
Mako==1.2.2
|
||||
MarkupSafe==2.0.1
|
||||
marshmallow==3.14.1
|
||||
marshmallow-sqlalchemy==0.26.1
|
||||
|
||||
23
wsgi.py
23
wsgi.py
@@ -1,16 +1,33 @@
|
||||
|
||||
from sys import platform
|
||||
from app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
"""Extend the Flask shell context."""
|
||||
return {'app': app}
|
||||
|
||||
running_directly = __name__ == "wsgi" or __name__ == "__main__"
|
||||
running_under_gunicorn = not running_directly and 'gunicorn' in __name__ and 'linux' in platform
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure development running
|
||||
if running_directly:
|
||||
with app.app_context():
|
||||
app.run(host='0.0.0.0')
|
||||
|
||||
# Configure production running
|
||||
if running_under_gunicorn:
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
gunicorn_logger = logging.getLogger('gunicorn.error')
|
||||
app.logger.handlers = gunicorn_logger.handlers
|
||||
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)
|
||||
app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
# Error out if nothing has been setup
|
||||
if not running_directly and not running_under_gunicorn:
|
||||
raise RuntimeError('Unsupported WSGI server')
|
||||
|
||||
Reference in New Issue
Block a user