Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

34 changed files with 609 additions and 643 deletions

View File

@ -3,7 +3,7 @@ name: ci
on: on:
push: push:
branches: branches:
- "main" - "**"
tags: tags:
- "v*.*.*" - "v*.*.*"
pull_request: pull_request:
@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@ -34,7 +34,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# generate Docker tags based on the following events/attributes # generate Docker tags based on the following events/attributes
@ -46,7 +46,7 @@ jobs:
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM python:3.11-slim-bookworm FROM python:3.8-slim-buster
RUN apt update RUN apt update
RUN apt -y install zip RUN apt -y install zip

View File

@ -83,12 +83,11 @@ docker run -d \
-e APP_SECRET_KEY='<secret_key>' \ -e APP_SECRET_KEY='<secret_key>' \
-e APP_DATABASE_URI='mysql+pymysql://<username>:<password>@<host>:<port>/<database>' \ -e APP_DATABASE_URI='mysql+pymysql://<username>:<password>@<host>:<port>/<database>' \
# you can include other optional Environment Variables from below like this # you can include other optional Environment Variables from below like this
-e REQUIRE_PLAY_KEY=True \ -e REQUIRE_PLAY_KEY=True
-p 8000:8000/tcp \ -p 8000:8000/tcp
-v /path/to/logs:/logs:rw / -v /path/to/unpacked/client:/app/luclient:r \
-v /path/to/unpacked/client:/app/luclient:ro \
-v /path/to/cachedir:/app/cache:rw \ -v /path/to/cachedir:/app/cache:rw \
ghcr.io/darkflameuniverse/nexusdashboard:latest aronwk/nexus-dashboard:latest
``` ```
* `/app/luclient` must be mapped to the location of an unpacked client * `/app/luclient` must be mapped to the location of an unpacked client
@ -98,7 +97,7 @@ docker run -d \
### Environmental Variables ### Environmental Variables
Please Reference `app/settings_example.py` to see all the variables Please Reference `app/settings_exmaple.py` to see all the variables
* Required: * Required:
* APP_SECRET_KEY (Must be provided) * APP_SECRET_KEY (Must be provided)

View File

@ -5,6 +5,7 @@ from flask_assets import Environment
from webassets import Bundle from webassets import Bundle
import time import time
from app.models import db, migrate, PlayKey from app.models import db, migrate, PlayKey
from app.schemas import ma
from app.forms import CustomUserManager from app.forms import CustomUserManager
from flask_user import user_registered, current_user, user_logged_in from flask_user import user_registered, current_user, user_logged_in
from flask_wtf.csrf import CSRFProtect from flask_wtf.csrf import CSRFProtect
@ -18,9 +19,7 @@ from app.commands import (
load_property, load_property,
gen_image_cache, gen_image_cache,
gen_model_cache, gen_model_cache,
fix_clone_ids, fix_clone_ids
remove_buffs,
find_missing_commendation_items
) )
from app.models import Account, AccountInvitation, AuditLog from app.models import Account, AccountInvitation, AuditLog
@ -96,8 +95,6 @@ def create_app():
app.cli.add_command(gen_image_cache) app.cli.add_command(gen_image_cache)
app.cli.add_command(gen_model_cache) app.cli.add_command(gen_model_cache)
app.cli.add_command(fix_clone_ids) app.cli.add_command(fix_clone_ids)
app.cli.add_command(remove_buffs)
app.cli.add_command(find_missing_commendation_items)
register_logging(app) register_logging(app)
register_settings(app) register_settings(app)
@ -129,6 +126,8 @@ def register_extensions(app):
""" """
db.init_app(app) db.init_app(app)
migrate.init_app(app, db) migrate.init_app(app, db)
ma.init_app(app)
scheduler.init_app(app) scheduler.init_app(app)
scheduler.start() scheduler.start()
@ -203,23 +202,19 @@ def register_settings(app):
# Load environment specific settings # Load environment specific settings
app.config['TESTING'] = False app.config['TESTING'] = False
app.config['DEBUG'] = False app.config['DEBUG'] = False
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
"pool_pre_ping": True,
"pool_size": 10,
"max_overflow": 2,
"pool_recycle": 300,
"pool_pre_ping": True,
"pool_use_lifo": True
}
# always pull these two from the env
app.config['SECRET_KEY'] = os.getenv( app.config['SECRET_KEY'] = os.getenv(
'APP_SECRET_KEY', 'APP_SECRET_KEY',
app.config['APP_SECRET_KEY'] app.config['APP_SECRET_KEY']
) )
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv( app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
'APP_DATABASE_URI', 'APP_DATABASE_URI',
app.config['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( app.config['USER_ENABLE_REGISTER'] = os.getenv(
'USER_ENABLE_REGISTER', 'USER_ENABLE_REGISTER',
app.config['USER_ENABLE_REGISTER'] app.config['USER_ENABLE_REGISTER']
@ -244,15 +239,21 @@ def register_settings(app):
'USER_REQUIRE_INVITATION', 'USER_REQUIRE_INVITATION',
app.config['USER_REQUIRE_INVITATION'] app.config['USER_REQUIRE_INVITATION']
) )
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
"pool_pre_ping": True,
"pool_size": 10,
"max_overflow": 2,
"pool_recycle": 300,
"pool_pre_ping": True,
"pool_use_lifo": True
}
app.config['MAIL_SERVER'] = os.getenv( app.config['MAIL_SERVER'] = os.getenv(
'MAIL_SERVER', 'MAIL_SERVER',
app.config['MAIL_SERVER'] app.config['MAIL_SERVER']
) )
app.config['MAIL_PORT'] = int( app.config['MAIL_PORT'] = os.getenv(
os.getenv( 'MAIL_USE_SSL',
'MAIL_PORT',
app.config['MAIL_PORT'] app.config['MAIL_PORT']
)
) )
app.config['MAIL_USE_SSL'] = os.getenv( app.config['MAIL_USE_SSL'] = os.getenv(
'MAIL_USE_SSL', 'MAIL_USE_SSL',
@ -307,42 +308,6 @@ def register_settings(app):
app.config['CACHE_LOCATION'] app.config['CACHE_LOCATION']
) )
# Recaptcha settings
if "RECAPTCHA_ENABLE" not in app.config:
app.config['RECAPTCHA_ENABLE'] = False
app.config['RECAPTCHA_ENABLE'] = os.getenv(
'RECAPTCHA_ENABLE',
app.config['RECAPTCHA_ENABLE']
)
if "RECAPTCHA_PUBLIC_KEY" not in app.config:
app.config['RECAPTCHA_PUBLIC_KEY'] = ''
app.config['RECAPTCHA_PUBLIC_KEY'] = os.getenv(
'RECAPTCHA_PUBLIC_KEY',
app.config['RECAPTCHA_PUBLIC_KEY']
)
if "RECAPTCHA_PRIVATE_KEY" not in app.config:
app.config['RECAPTCHA_PRIVATE_KEY'] = ''
app.config['RECAPTCHA_PRIVATE_KEY'] = os.getenv(
'RECAPTCHA_PRIVATE_KEY',
app.config['RECAPTCHA_PRIVATE_KEY']
)
# Optional
if "RECAPTCHA_API_SERVER" in app.config:
app.config['RECAPTCHA_API_SERVER'] = os.getenv(
'RECAPTCHA_API_SERVER',
app.config['RECAPTCHA_API_SERVER']
)
if "RECAPTCHA_PARAMETERS" in app.config:
app.config['RECAPTCHA_PARAMETERS'] = os.getenv(
'RECAPTCHA_PARAMETERS',
app.config['RECAPTCHA_PARAMETERS']
)
if "RECAPTCHA_DATA_ATTRS" in app.config:
app.config['RECAPTCHA_DATA_ATTRS'] = os.getenv(
'RECAPTCHA_DATA_ATTRS',
app.config['RECAPTCHA_DATA_ATTRS']
)
def gm_level(gm_level): def gm_level(gm_level):

View File

@ -19,12 +19,16 @@ from app.models import (
db, db,
Friends Friends
) )
from app.schemas import AccountSchema
from app import gm_level, log_audit from app import gm_level, log_audit
from app.forms import EditGMLevelForm, EditEmailForm from app.forms import EditGMLevelForm, EditEmailForm
from sqlalchemy import or_ from sqlalchemy import or_
accounts_blueprint = Blueprint('accounts', __name__) accounts_blueprint = Blueprint('accounts', __name__)
account_schema = AccountSchema()
@accounts_blueprint.route('/', methods=['GET']) @accounts_blueprint.route('/', methods=['GET'])
@login_required @login_required
@gm_level(3) @gm_level(3)
@ -252,9 +256,6 @@ def get():
# Delete # Delete
# </a> # </a>
if not current_app.config["USER_ENABLE_EMAIL"]:
account["2"] = '''N/A'''
if account["4"]: if account["4"]:
account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>''' account["4"] = '''<h2 class="far fa-times-circle text-danger"></h2>'''
else: else:
@ -270,11 +271,20 @@ def get():
else: else:
account["6"] = '''<h2 class="far fa-check-square text-success"></h2>''' account["6"] = '''<h2 class="far fa-check-square text-success"></h2>'''
if not current_app.config["USER_ENABLE_EMAIL"]: if current_app.config["USER_ENABLE_EMAIL"]:
account["8"] = '''<h2 class="far fa-times-circle text-muted"></h2>''' if account["8"]:
elif account["8"]: account["8"] = '''<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: else:
account["8"] = '''<h2 class="far fa-times-circle text-danger"></h2>''' # shift columns to fill in gap of 2
account["2"] = account["3"]
account["3"] = account["4"]
account["4"] = account["5"]
account["5"] = account["6"]
account["6"] = account["7"]
# remove last two columns
del account["7"]
del account["8"]
return data return data

View File

@ -3,17 +3,20 @@ from flask_user import login_required, current_user
from datatables import ColumnDT, DataTables from datatables import ColumnDT, DataTables
import time import time
from app.models import CharacterInfo, CharacterXML, Account, db from app.models import CharacterInfo, CharacterXML, Account, db
from app.schemas import CharacterInfoSchema
from app.forms import RescueForm, CharXMLUploadForm from app.forms import RescueForm, CharXMLUploadForm
from app import gm_level, log_audit from app import gm_level, log_audit
from app.luclient import translate_from_locale from app.luclient import translate_from_locale
import xmltodict import xmltodict
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import json import json
from xml.dom import minidom
character_blueprint = Blueprint('characters', __name__) character_blueprint = Blueprint('characters', __name__)
character_schema = CharacterInfoSchema()
@character_blueprint.route('/', methods=['GET']) @character_blueprint.route('/', methods=['GET'])
@login_required @login_required
@gm_level(3) @gm_level(3)
@ -100,88 +103,6 @@ def view(id):
character_json=character_json character_json=character_json
) )
@character_blueprint.route('/chardata/<id>', methods=['GET'])
@login_required
def chardata(id):
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
if character_data == {}:
abort(404)
return
if current_user.gm_level < 3:
if character_data.account_id and character_data.account_id != current_user.id:
abort(403)
return
character_json = xmltodict.parse(
CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data.replace("\"stt=", "\" stt="),
attr_prefix="attr_"
)
# print json for reference
# with open("errorchar.json", "a") as file:
# file.write(
# json.dumps(character_json, indent=4)
# )
# stupid fix for jinja parsing
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
# sort by items slot index
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
if "i" in inv.keys() and type(inv["i"]) == list:
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
return render_template(
'partials/_charxml.html.j2',
character_data=character_data,
character_json=character_json
)
@character_blueprint.route('/inventory/<id>/<inventory_id>', methods=['GET'])
@login_required
def inventory(id, inventory_id):
character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first()
if character_data == {}:
abort(404)
return
if current_user.gm_level < 3:
if character_data.account_id and character_data.account_id != current_user.id:
abort(403)
return
character_json = xmltodict.parse(
CharacterXML.query.filter(
CharacterXML.id == id
).first().xml_data.replace("\"stt=", "\" stt="),
attr_prefix="attr_"
)
# print json for reference
# with open("errorchar.json", "a") as file:
# file.write(
# json.dumps(character_json, indent=4)
# )
# stupid fix for jinja parsing
character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items")
# sort by items slot index
if type(character_json["obj"]["inv"]["holdings"]["in"]) == list:
for inv in character_json["obj"]["inv"]["holdings"]["in"]:
if "i" in inv.keys() and type(inv["i"]) == list:
inv["i"] = sorted(inv["i"], key=lambda i: int(i['attr_s']))
for inventory in character_json["obj"]["inv"]["holdings"]["in"]:
if inventory["attr_t"] == inventory_id:
return render_template(
'partials/charxml/_inventory.html.j2',
inventory=inventory
)
return "No Items in Inventory", 404
@character_blueprint.route('/view_xml/<id>', methods=['GET']) @character_blueprint.route('/view_xml/<id>', methods=['GET'])
@login_required @login_required
@ -317,7 +238,7 @@ def upload(id):
flash("You accept all consequences from these actions", "danger") flash("You accept all consequences from these actions", "danger")
log_audit(f"Updated {character_data.id}'s xml data") log_audit(f"Updated {character_data.id}'s xml data")
return redirect(url_for('characters.view', id=id)) return redirect(url_for('characters.view', id=id))
form.char_xml.data = minidom.parseString(character_data.xml_data).toprettyxml(indent=" ") form.char_xml.data = character_data.xml_data
return render_template("character/upload.html.j2", form=form) return render_template("character/upload.html.j2", form=form)

View File

@ -4,8 +4,8 @@ import random
import string import string
import datetime import datetime
from flask_user import current_app from flask_user import current_app
from app import db, luclient from app import db
from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail, CharacterXML from app.models import Account, PlayKey, CharacterInfo, Property, PropertyContent, UGC, Mail
import pathlib import pathlib
import zlib import zlib
from wand import image from wand import image
@ -15,7 +15,7 @@ from multiprocessing import Pool
from functools import partial from functools import partial
from sqlalchemy import func from sqlalchemy import func
import time import time
import xml.etree.ElementTree as ET
@click.command("init_db") @click.command("init_db")
@click.argument('drop_tables', nargs=1) @click.argument('drop_tables', nargs=1)
@ -180,20 +180,6 @@ def load_property(zone, player):
) )
new_prop_content.save() new_prop_content.save()
@click.command("remove_buffs")
@with_appcontext
def remove_buffs():
"""Clears all buff from all characters"""
chars = CharacterXML.query.all()
for char in chars:
character_xml = ET.XML(char.xml_data.replace("\"stt=", "\" stt="))
dest = character_xml.find(".//dest")
if dest:
buff = character_xml.find(".//buff")
if buff:
dest.remove(buff)
char.xml_data = ET.tostring(character_xml)
char.save()
@click.command("gen_image_cache") @click.command("gen_image_cache")
def gen_image_cache(): def gen_image_cache():
@ -281,41 +267,3 @@ def find_or_create_account(name, email, password, gm_level=9):
db.session.add(play_key) db.session.add(play_key)
db.session.commit() db.session.commit()
return # account return # account
@click.command("find_missing_commendation_items")
@with_appcontext
def find_missing_commendation_items():
data = dict()
lots = set()
reward_items = luclient.query_cdclient("Select reward_item1 from Missions;")
for reward_item in reward_items:
lots.add(reward_item[0])
reward_items = luclient.query_cdclient("Select reward_item2 from Missions;")
for reward_item in reward_items:
lots.add(reward_item[0])
reward_items = luclient.query_cdclient("Select reward_item3 from Missions;")
for reward_item in reward_items:
lots.add(reward_item[0])
reward_items = luclient.query_cdclient("Select reward_item4 from Missions;")
for reward_item in reward_items:
lots.add(reward_item[0])
lots.remove(0)
lots.remove(-1)
for lot in lots:
itemcompid = luclient.query_cdclient(
"Select component_id from ComponentsRegistry where component_type = 11 and id = ?;",
[lot],
one=True
)[0]
itemcomp = luclient.query_cdclient(
"Select commendationLOT, commendationCost from ItemComponent where id = ?;",
[itemcompid],
one=True
)
if itemcomp[0] is None or itemcomp[1] is None:
data[lot] = {"name": luclient.get_lot_name(lot)}
print(data)

View File

@ -1,15 +1,17 @@
from flask_wtf import FlaskForm, Recaptcha, RecaptchaField from flask_wtf import FlaskForm
from flask import current_app from flask import current_app
from flask_user.forms import ( from flask_user.forms import (
unique_email_validator, unique_email_validator,
LoginForm, password_validator,
RegisterForm unique_username_validator
) )
from flask_user import UserManager from flask_user import UserManager
from wtforms.widgets import TextArea, NumberInput from wtforms.widgets import TextArea, NumberInput
from wtforms import ( from wtforms import (
StringField, StringField,
HiddenField,
PasswordField,
BooleanField, BooleanField,
SubmitField, SubmitField,
validators, validators,
@ -22,43 +24,69 @@ from app.models import PlayKey
def validate_play_key(form, field): def validate_play_key(form, field):
"""Validates a field for a valid play kyey """Validates a field for a valid phone number
Args: Args:
form: REQUIRED, the field's parent form form: REQUIRED, the field's parent form
field: REQUIRED, the field with data field: REQUIRED, the field with data
Returns: Returns:
None, raises ValidationError if failed None, raises ValidationError if failed
""" """
# jank to get the foreign key that we need back into the field # jank to get the fireign key that we need back into the field
if current_app.config["REQUIRE_PLAY_KEY"]: if current_app.config["REQUIRE_PLAY_KEY"]:
field.data = PlayKey.key_is_valid(key_string=field.data) field.data = PlayKey.key_is_valid(key_string=field.data)
return True return
class CustomRecaptcha(Recaptcha):
def __call__(self, form, field):
if not current_app.config.get("RECAPTCHA_ENABLE", False):
return True
return super(CustomRecaptcha, self).__call__(form, field)
class CustomUserManager(UserManager): class CustomUserManager(UserManager):
def customize(self, app): def customize(self, app):
self.RegisterFormClass = CustomRegisterForm self.RegisterFormClass = CustomRegisterForm
self.LoginFormClass = CustomLoginForm
class CustomRegisterForm(RegisterForm):
class CustomRegisterForm(FlaskForm):
"""Registration form"""
next = HiddenField()
reg_next = HiddenField()
# Login Info
email = StringField(
'E-Mail',
validators=[
Optional(),
validators.Email('Invalid email address'),
unique_email_validator,
]
)
username = StringField(
'Username',
validators=[
DataRequired(),
unique_username_validator,
]
)
play_key_id = StringField( play_key_id = StringField(
'Play Key', 'Play Key',
validators=[validate_play_key] validators=[
) Optional(),
recaptcha = RecaptchaField( validate_play_key,
validators=[CustomRecaptcha()] ]
) )
class CustomLoginForm(LoginForm): password = PasswordField('Password', validators=[
recaptcha = RecaptchaField( DataRequired(),
validators=[CustomRecaptcha()] 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.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): class CreatePlayKeyForm(FlaskForm):

View File

@ -2,12 +2,16 @@ from flask import render_template, Blueprint, send_from_directory
from flask_user import current_user, login_required from flask_user import current_user, login_required
from app.models import Account, CharacterInfo, ActivityLog from app.models import Account, CharacterInfo, ActivityLog
from app.schemas import AccountSchema, CharacterInfoSchema
import datetime import datetime
import time import time
main_blueprint = Blueprint('main', __name__) main_blueprint = Blueprint('main', __name__)
account_schema = AccountSchema()
char_info_schema = CharacterInfoSchema()
@main_blueprint.route('/', methods=['GET']) @main_blueprint.route('/', methods=['GET'])
def index(): def index():

View File

@ -4,9 +4,10 @@ from flask_user import UserMixin
from wtforms import ValidationError from wtforms import ValidationError
import logging import logging
from flask_sqlalchemy.query import Query from flask_sqlalchemy import BaseQuery
from sqlalchemy.dialects import mysql from sqlalchemy.dialects import mysql
from sqlalchemy.exc import OperationalError, StatementError from sqlalchemy.exc import OperationalError, StatementError
from sqlalchemy.types import JSON
from time import sleep from time import sleep
import random import random
import string import string
@ -14,7 +15,7 @@ import string
# retrying query to work around python trash collector # retrying query to work around python trash collector
# killing connections of other gunicorn workers # killing connections of other gunicorn workers
class RetryingQuery(Query): class RetryingQuery(BaseQuery):
__retry_count__ = 3 __retry_count__ = 3
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -1017,7 +1018,7 @@ class Reports(db.Model):
__tablename__ = 'reports' __tablename__ = 'reports'
data = db.Column( data = db.Column(
mysql.MEDIUMBLOB(), JSON(),
nullable=False nullable=False
) )

View File

@ -13,6 +13,7 @@ from flask_user import login_required, current_user
from datatables import ColumnDT, DataTables from datatables import ColumnDT, DataTables
import time import time
from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account, Mail
from app.schemas import PropertySchema
from app import gm_level, log_audit from app import gm_level, log_audit
from app.luclient import query_cdclient from app.luclient import query_cdclient
from app.forms import RejectPropertyForm from app.forms import RejectPropertyForm
@ -23,6 +24,9 @@ import pathlib
property_blueprint = Blueprint('properties', __name__) property_blueprint = Blueprint('properties', __name__)
property_schema = PropertySchema()
@property_blueprint.route('/', methods=['GET']) @property_blueprint.route('/', methods=['GET'])
@login_required @login_required
@gm_level(3) @gm_level(3)

View File

@ -4,7 +4,8 @@ from app.models import CharacterInfo, Account, CharacterXML, Reports
from app.luclient import get_lot_name from app.luclient import get_lot_name
from app import gm_level, scheduler from app import gm_level, scheduler
from sqlalchemy.orm import load_only from sqlalchemy.orm import load_only
import xmltodict, gzip, json, datetime import datetime
import xmltodict
reports_blueprint = Blueprint('reports', __name__) reports_blueprint = Blueprint('reports', __name__)
@ -43,8 +44,6 @@ def index():
@gm_level(3) @gm_level(3)
def items_by_date(date): def items_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/items/by_date.html.j2', data=data, date=date) return render_template('reports/items/by_date.html.j2', data=data, date=date)
@ -63,8 +62,6 @@ def items_graph(start, end):
datasets = [] datasets = []
# get stuff ready # get stuff ready
for entry in entries: for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y")) labels.append(entry.date.strftime("%m/%d/%Y"))
for key in entry.data: for key in entry.data:
items[key] = get_lot_name(key) items[key] = get_lot_name(key)
@ -107,8 +104,6 @@ def items_graph(start, end):
@gm_level(3) @gm_level(3)
def currency_by_date(date): def currency_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/currency/by_date.html.j2', data=data, date=date) return render_template('reports/currency/by_date.html.j2', data=data, date=date)
@ -126,8 +121,6 @@ def currency_graph(start, end):
datasets = [] datasets = []
# get stuff ready # get stuff ready
for entry in entries: for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y")) labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters: for character in characters:
data = [] data = []
@ -162,8 +155,6 @@ def currency_graph(start, end):
@gm_level(3) @gm_level(3)
def uscore_by_date(date): def uscore_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/uscore/by_date.html.j2', data=data, date=date) return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
@ -181,8 +172,6 @@ def uscore_graph(start, end):
datasets = [] datasets = []
# get stuff ready # get stuff ready
for entry in entries: for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y")) labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters: for character in characters:
data = [] data = []
@ -247,31 +236,19 @@ def gen_item_report():
if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"]) == 0 or int(inv["attr_t"]) == 1): 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"]: for item in inv["i"]:
if item["attr_l"] in report_data: if item["attr_l"] in report_data:
if ("attr_c" in item): report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + int(item["attr_c"])
else:
report_data[item["attr_l"]]["item_count"] = report_data[item["attr_l"]]["item_count"] + 1
else: else:
if ("attr_c" in item): report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
report_data[item["attr_l"]] = {"item_count": int(item["attr_c"]), "chars": {}}
else:
report_data[item["attr_l"]] = {"item_count": 1, "chars": {}}
if name in report_data[item["attr_l"]]["chars"]: if name in report_data[item["attr_l"]]["chars"]:
if ("attr_c" in item): report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + int(item["attr_c"])
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + int(item["attr_c"])
else:
report_data[item["attr_l"]]["chars"][name] = report_data[item["attr_l"]]["chars"][name] + 1
else: else:
if ("attr_c" in item): report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
report_data[item["attr_l"]]["chars"][name] = int(item["attr_c"])
else:
report_data[item["attr_l"]]["chars"][name] = 1
except Exception as e: except Exception as e:
current_app.logger.error(f"REPORT::ITEMS - ERROR PARSING CHARACTER {char_xml.id}") current_app.logger.error(f"REPORT::ITEMS - ERROR PARSING CHARACTER {char_xml.id}")
current_app.logger.error(f"REPORT::ITEMS - {e}") current_app.logger.error(f"REPORT::ITEMS - {e}")
new_report = Reports( new_report = Reports(
data=gzip.compress(json.dumps(report_data).encode('utf-8')), data=report_data,
report_type="items", report_type="items",
date=date date=date
) )
@ -319,7 +296,7 @@ def gen_currency_report():
current_app.logger.error(f"REPORT::CURRENCY - {e}") current_app.logger.error(f"REPORT::CURRENCY - {e}")
new_report = Reports( new_report = Reports(
data=gzip.compress(json.dumps(report_data).encode('utf-8')), data=report_data,
report_type="currency", report_type="currency",
date=date date=date
) )
@ -367,7 +344,7 @@ def gen_uscore_report():
current_app.logger.error(f"REPORT::U-SCORE - {e}") current_app.logger.error(f"REPORT::U-SCORE - {e}")
new_report = Reports( new_report = Reports(
data=gzip.compress(json.dumps(report_data).encode('utf-8')), data=report_data,
report_type="uscore", report_type="uscore",
date=date date=date
) )

130
app/schemas.py Normal file
View File

@ -0,0 +1,130 @@
from flask_marshmallow import Marshmallow
from app.models import (
PlayKey,
PetNames,
Mail,
UGC,
PropertyContent,
Property,
CharacterXML,
CharacterInfo,
Account,
AccountInvitation,
ActivityLog,
CommandLog
)
ma = Marshmallow()
class PlayKeySchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = PlayKey
include_relationships = False
load_instance = True
include_fk = True
class PetNamesSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = PetNames
include_relationships = False
load_instance = True
include_fk = False
class MailSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Mail
include_relationships = False
load_instance = True
include_fk = False
class UGCSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = UGC
include_relationships = False
load_instance = True
include_fk = False
class PropertyContentSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = PropertyContent
include_relationships = True
load_instance = True
include_fk = True
ugc = ma.Nested(UGCSchema)
class PropertySchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Property
include_relationships = False
load_instance = True
include_fk = False
properties_contents = ma.Nested(PropertyContentSchema, many=True)
class CharacterXMLSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = CharacterXML
include_relationships = False
load_instance = True
include_fk = False
class CharacterInfoSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = CharacterInfo
include_relationships = False
load_instance = True
include_fk = False
charxml = ma.Nested(CharacterXMLSchema)
properties_owner = ma.Nested(PropertySchema, many=True)
pets = ma.Nested(PetNamesSchema, many=True)
mail = ma.Nested(MailSchema, many=True)
class AccountSchema(ma.SQLAlchemyAutoSchema):
class Meta:
model = Account
include_relationships = False
load_instance = True
include_fk = False
play_key = ma.Nested(PlayKeySchema)
charinfo = ma.Nested(CharacterInfoSchema, many=True)
class AccountInvitationSchema(ma.SQLAlchemyAutoSchema): # noqa
class Meta:
model = AccountInvitation
include_relationships = True
load_instance = True
include_fk = True
invite_by_user = ma.Nested(AccountSchema)
class ActivityLogSchema(ma.SQLAlchemyAutoSchema): # noqa
class Meta:
model = ActivityLog
include_relationships = True
load_instance = True
include_fk = True
character = ma.Nested(CharacterInfoSchema())
class CommandLogSchema(ma.SQLAlchemyAutoSchema): # noqa
class Meta:
model = CommandLog
include_relationships = True
load_instance = True
include_fk = True
character = ma.Nested(CharacterInfoSchema())

View File

@ -61,13 +61,3 @@ USER_AFTER_LOGOUT_ENDPOINT = "main.index"
# Option will be removed once this feature is full implemeted # Option will be removed once this feature is full implemeted
ENABLE_CHAR_XML_UPLOAD = False ENABLE_CHAR_XML_UPLOAD = False
# Recaptcha settings
# See: https://flask-wtf.readthedocs.io/en/1.2.x/form/#recaptcha
RECAPTCHA_ENABLE = False
RECAPTCHA_PUBLIC_KEY = ''
RECAPTCHA_PRIVATE_KEY = ''
# Optional
# RECAPTCHA_API_SERVER = ''
# RECAPTCHA_PARAMETERS = ''
RECAPTCHA_DATA_ATTRS = {'theme': 'white', 'size': 'invisible'}

View File

@ -14,13 +14,17 @@
<tr> <tr>
<th>Actions</th> <th>Actions</th>
<th>Name</th> <th>Name</th>
<th>Email</th> {% if config.USER_ENABLE_EMAIL %}
<th>Email</th>
{% endif %}
<th>GM Level</th> <th>GM Level</th>
<th>Locked</th> <th>Locked</th>
<th>Banned</th> <th>Banned</th>
<th>Muted</th> <th>Muted</th>
<th>Registered</th> <th>Registered</th>
<th>Email Confirmed</th> {% if config.USER_ENABLE_EMAIL %}
<th>Email Confirmed</th>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody></tbody> <tbody></tbody>

View File

@ -0,0 +1,188 @@
{% extends "bootstrap/base.html" %}
{% block title %}Key Creation{% endblock %}
{% block navbar %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">Dashboard</a>
</div>
<ul class="nav navbar-nav">
</ul>
<ul class="nav navbar-nav" style="float: right">
<li class="active"><a href="#">Welcome {{ current_user.username }}!</a></li>
<li><a href="{{ url_for('logout') }}">Logout</a></li>
</ul>
</div>
</nav>
{% endblock navbar %}}
{% block content %}
{# LOGO #}
<div class="container" style="margin-top: 50px">
{# Display logo #}
<div style="margin-bottom: 50px">
<img
src="{{ url_for('static', filename=resources.LOGO) }}"
class="center-block img-responsive mx-auto d-block"
alt="Logo"
width="100"
height="100"
>
</div>
</div>
{# Key creation #}
<div class="container">
<div class="text-center">
<h3>Key Creation</h3>
</div>
<div class="col-lg-3">
</div>
<div class="col-lg-6">
{# If the error value is set, display the error in red text #}
{% if error %}
<div class="alert alert-danger">
{{ error }}
</div>
{% endif %}
{# If the message value is set, display the message in green text #}
{% if message %}
<div class="alert alert-success">
{{ message }}
</div>
{% endif %}
{# Form which takes in Admin Username, Admin Password, and the amount of keys to create. #}
<form action="{{ url_for('dashboard') }}" method="post">
{# Key count input #}
<div class="form-group">
<label for="key_count">Generate keys</label>
<input type="number" class="form-control" name="key_count" placeholder="Enter number of keys...">
<small class="form-text text-muted">Number of keys to create.</small>
</div>
{# Submit button #}
<div class="form-group">
<button type="submit" class="btn btn-primary">Generate Keys</button>
</div>
</form>
{# If the keys value is set, create a list for each key in keys #}
{% if keys %}
<div class="alert alert-success">
<ul>
{% for key in keys %}
<li>{{ key }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="col-lg-3">
</div>
</div>
</div>
{# Activity graphs #}
<div class="container">
<div class="text-center">
<h3>Activity</h3>
</div>
<div class="col-lg-3">
</div>
<div class="col-lg-6">
<canvas id="sessions_graph" width="400" height="400"></canvas>
<canvas id="play_time_graph" width="400" height="400"></canvas>
<canvas id="zone_play_time_graph" width="400" height="400"></canvas>
</div>
<div class="col-lg-3">
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
// Make a get request to the server to load the activity data
$.get("{{ url_for('load_activities') }}", function(data) {
});
// Make a get request to the server to get the activity data for "sessions"
$.get("{{ url_for('activity_data', name='sessions') }}", function(data) {
// Load data as a json object
data = JSON.parse(data);
var ctx = document.getElementById('sessions_graph').getContext('2d');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: data.datasets
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
});
// Make a get request to the server to get the activity data for "play_time"
$.get("{{ url_for('activity_data', name='play_time') }}", function(data) {
// Load data as a json object
data = JSON.parse(data);
var ctx = document.getElementById('play_time_graph').getContext('2d');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: data.datasets
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
});
// Make a get request to the server to get the activity data for "zone_play_time"
$.get("{{ url_for('activity_data', name='zone_play_time') }}", function(data) {
// Load data as a json object
data = JSON.parse(data);
var ctx = document.getElementById('zone_play_time_graph').getContext('2d');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.labels,
datasets: data.datasets
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
});
</script>
{% endblock scripts %}}

View File

@ -84,35 +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='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="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 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');
});
// make tooltips with data work
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
function setInnerHTML(elm, html) {
elm.innerHTML = html;
$("body").tooltip({ selector: '[data-toggle=tooltip]' });
Array.from(elm.querySelectorAll("script"))
.forEach( oldScriptEl => {
const newScriptEl = document.createElement("script");
Array.from(oldScriptEl.attributes).forEach( attr => {
newScriptEl.setAttribute(attr.name, attr.value)
});
const scriptText = document.createTextNode(oldScriptEl.innerHTML);
newScriptEl.appendChild(scriptText);
oldScriptEl.parentNode.replaceChild(newScriptEl, oldScriptEl);
});
}
</script>
{% endblock %} {% endblock %}
</body> </body>

View File

@ -9,22 +9,10 @@
{% endblock content_before %} {% endblock content_before %}
{% block content %} {% block content %}
<style> <h3 style="color: orange;">PROCEED WITH CAUTION</h3>
.blink {
animation: blinker .5s linear infinite;
color: red;
font-family: sans-serif;
}
@keyframes blinker {
50% {
opacity: 0;
}
}
</style>
<h3 class="text-center blink">PROCEED WITH CAUTION</h3>
<form method=post> <form method=post>
{{ form.csrf_token }} {{ form.csrf_token }}
<div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" > <div class="card shadow-sm mx-auto pb-3 bg-dark border-primary" style="width: 20rem;">
<div class="card-body"> <div class="card-body">
{{ helper.render_field(form.char_xml) }} {{ helper.render_field(form.char_xml) }}
{{ helper.render_submit_field(form.submit) }} {{ helper.render_submit_field(form.submit) }}

View File

@ -15,9 +15,8 @@
{% include 'partials/_character.html.j2' %} {% include 'partials/_character.html.j2' %}
{% endwith %} {% endwith %}
</div> </div>
<div class="col-sm" id="charxml"> <div class="col-sm">
Loading Character Data {% include 'partials/_charxml.html.j2'%}
{% include 'partials/_loading.html' %}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
@ -33,14 +32,3 @@
{% endfor %} {% endfor %}
</div> </div>
{% endblock content_after %} {% endblock content_after %}
{% block js %}
{{ super() }}
<script>
fetch({{ url_for("characters.chardata", id=character_data.id)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("charxml"), text);
})
</script>
{% endblock js %}

View File

@ -60,12 +60,7 @@
{# Remember me #} {# Remember me #}
{% if user_manager.USER_ENABLE_REMEMBER_ME %} {% if user_manager.USER_ENABLE_REMEMBER_ME %}
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }} {{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
{% endif %}
{# recaptcha #}
{% if config.RECAPTCHA_ENABLE %}
{{ render_field(form.recaptcha, tabindex=250) }}
{% endif %} {% endif %}
{# Submit button #} {# Submit button #}

View File

@ -29,11 +29,6 @@
{{ render_checkbox_field(login_form.remember_me, tabindex=130) }} {{ render_checkbox_field(login_form.remember_me, tabindex=130) }}
{% endif %} {% endif %}
{# recaptcha #}
{% if config.RECAPTCHA_ENABLE %}
{{ form.recaptcha }}
{% endif %}
{# Submit button #} {# Submit button #}
{{ render_submit_field(login_form.submit, tabindex=180) }} {{ render_submit_field(login_form.submit, tabindex=180) }}
</form> </form>
@ -68,11 +63,6 @@
{{ render_field(register_form.retype_password, tabindex=240) }} {{ render_field(register_form.retype_password, tabindex=240) }}
{% endif %} {% endif %}
{# recaptcha #}
{% if config.RECAPTCHA_ENABLE %}
{{ register_form.recaptcha }}
{% endif %}
{{ render_submit_field(register_form.submit, tabindex=280) }} {{ render_submit_field(register_form.submit, tabindex=280) }}
</form> </form>

View File

@ -47,11 +47,6 @@
{{ render_field(form.retype_password, tabindex=240) }} {{ render_field(form.retype_password, tabindex=240) }}
{% endif %} {% endif %}
{# recaptcha #}
{% if config.RECAPTCHA_ENABLE %}
{{ render_field(form.recaptcha, tabindex=250) }}
{% endif %}
{{ render_submit_field(form.submit, tabindex=280) }} {{ render_submit_field(form.submit, tabindex=280) }}
</form> </form>

View File

@ -82,6 +82,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="row"> <div class="row">
<div class="col text-right"> <div class="col text-right">
Source Source

View File

@ -11,11 +11,9 @@
<div class="col text-right"> <div class="col text-right">
U-Score: {{ character_json.obj.char.attr_ls }} U-Score: {{ character_json.obj.char.attr_ls }}
</div> </div>
{% if "lvl" in character_json.obj %} <div class="col">
<div class="col"> Level: {{ character_json.obj.lvl.attr_l }}
Level: {{ character_json.obj.lvl.attr_l }} </div>
</div>
{% endif %}
</div> </div>
<br/> <br/>
<div class="row"> <div class="row">
@ -114,33 +112,99 @@
<div class="tab-content mt-3" id="nav-invContent"> <div class="tab-content mt-3" id="nav-invContent">
<div class="tab-pane fade show active" id="nav-items" role="tabpanel" aria-labelledby="nav-items-tab"> <div class="tab-pane fade show active" id="nav-items" role="tabpanel" aria-labelledby="nav-items-tab">
{# Inv ID 0 - Index: 0 #} {# Inv ID 0 - Index: 0 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "0" %}
{% 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> </div>
<div class="tab-pane fade" id="nav-vault" role="tabpanel" aria-labelledby="nav-vault-tab"> <div class="tab-pane fade" id="nav-vault" role="tabpanel" aria-labelledby="nav-vault-tab">
{# Inv ID 1 - Index: 1 #} {# Inv ID 1 - Index: 1 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "1" %}
{% 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> </div>
<div class="tab-pane fade" id="nav-vault-models" role="tabpanel" aria-labelledby="nav-vault-models-tab"> <div class="tab-pane fade" id="nav-vault-models" role="tabpanel" aria-labelledby="nav-vault-models-tab">
{# Inv ID 14 - Index: 10 #} {# Inv ID 14 - Index: 10 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "14" %}
{% 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> </div>
<div class="tab-pane fade" id="nav-bricks" role="tabpanel" aria-labelledby="nav-bricks-tab"> <div class="tab-pane fade" id="nav-bricks" role="tabpanel" aria-labelledby="nav-bricks-tab">
{# Inv ID 2 - Index: 2 #} {# Inv ID 2 - Index: 2 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "2" %}
{% 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> </div>
<div class="tab-pane fade" id="nav-models" role="tabpanel" aria-labelledby="nav-models-tab"> <div class="tab-pane fade" id="nav-models" role="tabpanel" aria-labelledby="nav-models-tab">
{# Inv ID 5 - Index: 6 #} {# Inv ID 5 - Index: 6 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "5" %}
{% 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> </div>
<div class="tab-pane fade" id="nav-behaviors" role="tabpanel" aria-labelledby="nav-behaviors-tab"> <div class="tab-pane fade" id="nav-behaviors" role="tabpanel" aria-labelledby="nav-behaviors-tab">
{# Inv ID 7 - Index: 8 #} {# Inv ID 7 - Index: 8 #}
Loading Inventory {% for item in character_json.obj.inv.holdings.in %}
{% include 'partials/_loading.html' %} {% if item.attr_t == "7" %}
{% 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> </div>
</div> </div>
</div> </div>
@ -199,40 +263,3 @@
</div> </div>
</div> </div>
</div> </div>
<script>
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=0)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-items"), text);
})
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=1)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-vault"), text);
})
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=14)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-vault-models"), text);
})
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=2)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-bricks"), text);
})
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=5)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-models"), text);
})
fetch({{ url_for("characters.inventory", id=character_data.id, inventory_id=7)|tojson }})
.then(response => response.text())
.then(text => {
setInnerHTML(document.getElementById("nav-behaviors"), text);
})
</script>

View File

@ -1,21 +0,0 @@
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s;">
<span class="sr-only">Loading 0</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 1s;">
<span class="sr-only">Loading 1</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 2s;">
<span class="sr-only">Loading 2</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 3s;">
<span class="sr-only">Loading 3</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 4s;">
<span class="sr-only">Loading 4</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 5s;">
<span class="sr-only">Loading 5</span>
</div>
<div class="spinner-grow text-light" role="status" style="animation-duration: 6s; animation-delay: 6s;">
<span class="sr-only">Loading 6</span>
</div>

View File

@ -77,14 +77,6 @@
{{ property.performance_cost }} {{ property.performance_cost }}
</div> </div>
</div> </div>
<div class="row">
<div class="col text-right">
Clone ID:
</div>
<div class="col">
{{ property.clone_id }}
</div>
</div>
{% if request.endpoint != "properties.view" %} {% if request.endpoint != "properties.view" %}
<br/> <br/>
<div class="row"> <div class="row">

View File

@ -4,29 +4,25 @@
alt="{{ inv_item.attr_l|get_lot_name }}" alt="{{ inv_item.attr_l|get_lot_name }}"
class="border p-1 border-primary rounded m-1" class="border p-1 border-primary rounded m-1"
width="60" width="60"
{% if 'attr_eq' in inv_item %} {% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
{% if inv_item.attr_eq == "true" %}style="background-color:#d16f05;"{% endif %}
{% endif %}
height="60" height="60"
data-html="true" data-html="true"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="left" data-placement="left"
title="{% include 'partials/charxml/_item_tooltip.html.j2' %}" title="{% include 'partials/charxml/_item_tooltip.html.j2' %}"
> >
{% if 'attr_c' in inv_item %} {% if inv_item.attr_c != "1" %}
<span class="inventory-count text-bold"> <span class="inventory-count text-bold">
{%if inv_item.attr_c|int > 999 %} {%if inv_item.attr_c|int > 999 %}
+999 +999
{% elif inv_item.attr_c|int > 1 %} {% else %}
{{ inv_item.attr_c }} {{ inv_item.attr_c }}
{% endif %} {% endif %}
</span> </span>
{% endif %} {% endif %}
{% if 'attr_b' in inv_item %} {% if inv_item.attr_b == "true" %}
{% if inv_item.attr_b == "true" %} <span class="inventory-lock">
<span class="inventory-lock"> <i class='fas fa-lock'></i>
<i class='fas fa-lock'></i> </span>
</span>
{% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,9 +0,0 @@
{% if inventory.i is iterable and (inventory.i is not string and inventory.i is not mapping) %}
{% for inv_item in inventory.i %}
{% include 'partials/charxml/_inv_grid.html.j2' %}
{% endfor %}
{% else %}
{% with inv_item=inventory.i %}
{% include 'partials/charxml/_inv_grid.html.j2' %}
{% endwith %}
{% endif %}

View File

@ -66,8 +66,7 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% if 'attr_c' in inv_item %} {%if inv_item.attr_c|int > 999 %}
{%if inv_item.attr_c|int > 999 %} <br />Count: {{ inv_item.attr_c|numberFormat }}
<br />Count: {{ inv_item.attr_c|numberFormat }}
{% endif %}
{% endif %} {% endif %}

View File

@ -22,8 +22,8 @@ logger = logging.getLogger('alembic.env')
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
config.set_main_option( config.set_main_option(
'sqlalchemy.url', 'sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI') str(current_app.extensions['migrate'].db.get_engine().url).replace(
) '%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,

View File

@ -1,164 +0,0 @@
"""compressss reports
Revision ID: 1164e037907f
Revises: a6e42ef03da7
Create Date: 2023-11-18 01:38:00.127472
"""
from alembic import op
from sqlalchemy.orm.session import Session
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
from sqlalchemy.types import JSON
from sqlalchemy.ext.declarative import declarative_base
import gzip
import json
Base = declarative_base()
# revision identifiers, used by Alembic.
revision = '1164e037907f'
down_revision = 'a6e42ef03da7'
branch_labels = None
depends_on = None
class ReportsUpgradeNew(Base):
__tablename__ = 'reports'
__table_args__ = {'extend_existing': True}
data = sa.Column(
mysql.MEDIUMBLOB(),
nullable=False
)
report_type = sa.Column(
sa.VARCHAR(35),
nullable=False,
primary_key=True,
autoincrement=False
)
date = sa.Column(
sa.Date(),
primary_key=True,
autoincrement=False
)
def save(self):
sa.session.add(self)
sa.session.commit()
sa.session.refresh(self)
class ReportsUpgradeOld(Base):
__tablename__ = 'reports_old'
__table_args__ = {'extend_existing': True}
data = sa.Column(
JSON(),
nullable=False
)
report_type = sa.Column(
sa.VARCHAR(35),
nullable=False,
primary_key=True,
autoincrement=False
)
date = sa.Column(
sa.Date(),
primary_key=True,
autoincrement=False
)
class ReportsDowngradeOld(Base):
__tablename__ = 'reports'
__table_args__ = {'extend_existing': True}
data = sa.Column(
mysql.MEDIUMBLOB(),
nullable=False
)
report_type = sa.Column(
sa.VARCHAR(35),
nullable=False,
primary_key=True,
autoincrement=False
)
date = sa.Column(
sa.Date(),
primary_key=True,
autoincrement=False
)
def save(self):
sa.session.add(self)
sa.session.commit()
sa.session.refresh(self)
class ReportsDowngradeNew(Base):
__tablename__ = 'reports_old'
__table_args__ = {'extend_existing': True}
data = sa.Column(
JSON(),
nullable=False
)
report_type = sa.Column(
sa.VARCHAR(35),
nullable=False,
primary_key=True,
autoincrement=False
)
date = sa.Column(
sa.Date(),
primary_key=True,
autoincrement=False
)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.rename_table('reports', 'reports_old')
bind = op.get_bind()
session = Session(bind=bind)
reports = session.query(ReportsUpgradeOld)
op.create_table('reports',
sa.Column('data', mysql.MEDIUMBLOB(), nullable=False),
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('report_type', 'date')
)
# insert records
new_reports = []
# insert records
for report in reports:
new_reports.append({
"data":gzip.compress(json.dumps(report.data).encode('utf-8')),
"report_type":report.report_type,
"date":report.date
})
op.bulk_insert(ReportsUpgradeNew.__table__, new_reports)
op.drop_table('reports_old')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('reports')
op.create_table('reports',
sa.Column('data', JSON(), nullable=False),
sa.Column('report_type', sa.VARCHAR(length=35), autoincrement=False, nullable=False),
sa.Column('date', sa.Date(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint('report_type', 'date')
)
# ### end Alembic commands ###

View File

@ -77,11 +77,11 @@ def upgrade():
sa.Column('created_at', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False), sa.Column('created_at', mysql.TIMESTAMP(), server_default=sa.text('now()'), nullable=False),
sa.Column('play_key_id', mysql.INTEGER(), nullable=True), sa.Column('play_key_id', mysql.INTEGER(), nullable=True),
sa.Column('mute_expire', mysql.BIGINT(unsigned=True), server_default='0', nullable=False), sa.Column('mute_expire', mysql.BIGINT(unsigned=True), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['play_key_id'], ['play_keys.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name') sa.UniqueConstraint('name')
) )
op.create_foreign_key(None, 'accounts', 'play_keys', ['play_key_id'], ['id'], ondelete='CASCADE')
op.add_column('accounts', sa.Column('active', sa.BOOLEAN(), server_default='1', nullable=False)) op.add_column('accounts', sa.Column('active', sa.BOOLEAN(), server_default='1', nullable=False))
op.add_column('accounts', sa.Column('email_confirmed_at', sa.DateTime(), nullable=True)) op.add_column('accounts', sa.Column('email_confirmed_at', sa.DateTime(), nullable=True))
op.add_column('accounts', sa.Column('email', sa.Unicode(length=255), server_default='', nullable=True)) op.add_column('accounts', sa.Column('email', sa.Unicode(length=255), server_default='', nullable=True))

View File

@ -1,20 +1,61 @@
alembic==1.7.5 alembic==1.7.5
APScheduler==3.8.1
astroid==2.9.1
autopep8==1.6.0 autopep8==1.6.0
bcrypt==3.2.0
blinker==1.4
cffi==1.14.6
click==8.0.1
colorama==0.4.4
cryptography==36.0.0
dnspython==2.1.0
dominate==2.6.0
email-validator==1.1.3 email-validator==1.1.3
Flask==3.0.0 Flask==2.0.1
Flask-APScheduler==1.12.3 Flask-APScheduler==1.12.3
Flask-Assets==2.1.0 Flask-Assets==2.0
Flask-Login==0.5.0
Flask-Mail==0.9.1
flask-marshmallow==0.14.0
Flask-Migrate==3.1.0 Flask-Migrate==3.1.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==2.5.1
Flask-User==1.0.2.2 Flask-User==1.0.2.2
Flask-WTF==1.2.1 Flask-WTF==1.0.0
gunicorn==21.2.0 greenlet==1.1.0
idna==3.3
isort==5.10.1
itsdangerous==2.0.1
Jinja2==3.0.1
lazy-object-proxy==1.7.1
libsass==0.21.0 libsass==0.21.0
Mako==1.2.2
MarkupSafe==2.0.1
marshmallow==3.14.1
marshmallow-sqlalchemy==0.26.1
mccabe==0.6.1
passlib==1.7.4
platformdirs==2.4.1
pycodestyle==2.8.0
pycparser==2.20
pydocstyle==6.1.1
pyflakes==2.4.0
pylama==8.3.3
pylint==2.12.2
PyMySQL==1.0.2 PyMySQL==1.0.2
SQLAlchemy==2.0.23 python-dateutil==2.8.2
pytz==2021.3
pytz-deprecation-shim==0.1.0.post0
six==1.16.0
snowballstemmer==2.2.0
SQLAlchemy==1.4.22
sqlalchemy-datatables==2.0.1 sqlalchemy-datatables==2.0.1
toml==0.10.2
tzdata==2021.5
tzlocal==4.1
visitor==0.1.3
Wand==0.6.7 Wand==0.6.7
webassets==2.0 webassets==2.0
Werkzeug==3.0.1 Werkzeug==2.0.1
wrapt==1.13.3
WTForms==3.0.0 WTForms==3.0.0
xmltodict==0.12.0 xmltodict==0.12.0

16
wsgi.py
View File

@ -1,19 +1,23 @@
from sys import platform
from app import create_app from app import create_app
app = create_app() app = create_app()
@app.shell_context_processor @app.shell_context_processor
def make_shell_context(): def make_shell_context():
"""Extend the Flask shell context.""" """Extend the Flask shell context."""
return {'app': app} 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(): with app.app_context():
app.run(host='0.0.0.0') app.run(host='0.0.0.0')
else:
# Configure production running
if running_under_gunicorn:
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
gunicorn_logger = logging.getLogger('gunicorn.error') gunicorn_logger = logging.getLogger('gunicorn.error')
@ -23,3 +27,7 @@ else:
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
app.logger.addHandler(file_handler) app.logger.addHandler(file_handler)
app.logger.setLevel(gunicorn_logger.level) 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')