Compare commits

..

13 Commits

Author SHA1 Message Date
8fc221d649 Added build yaml and versioning change for requirements.txt 2024-01-03 12:54:28 -08:00
ae5b4d5c2e updated requirements 2024-01-03 12:32:19 -08:00
aronwk-aaron
09096fe1c4 bump action versions 2023-12-31 01:57:16 -06:00
Aaron Kimbrell
9bfa55ac8e
Merge pull request #81 from Ramen2X/main
fix readme typo (exmaple -> example)
2023-12-18 10:11:00 -06:00
Ramen2X
1dee96c04f
fix typo (exmaple -> example) 2023-12-17 16:42:01 -05:00
aronwk-aaron
d005b497e6 compress the reports :D 2023-11-18 05:46:50 -06:00
aronwk-aaron
259efc81fd fix: don't show count of 1 on items
fix: tooltips on lazy loaded items
2023-11-18 01:13:05 -06:00
aronwk-aaron
b9fc039a7e LAZY LOADING
updated a bunch of packages
2023-11-18 00:41:35 -06:00
aronwk-aaron
abc8af89c5 update packages 2023-11-17 22:51:12 -06:00
Aaron Kimbrell
5ae2769ad2
Merge pull request #78 from DarkflameUniverse/captcha
feat: add recaptcha support
2023-11-17 18:29:41 -06:00
aronwk-aaron
b7e48bb656 extend the recaptha validator to make it optional 2023-11-17 17:40:01 -06:00
aronwk-aaron
7633053490 it works 2023-11-17 16:38:01 -06:00
aronwk-aaron
e6c452d000 feat: add recaptcha support 2023-11-17 00:55:18 -06:00
26 changed files with 534 additions and 501 deletions

View File

@ -0,0 +1,39 @@
name: Docker Build
on:
push:
branches:
- main
workflow_dispatch:
# container:
# image: catthehacker/ubuntu:act-latest
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Log in to Gitea Registry
run: echo "${{ secrets.TOKEN }}"| docker login -u zebra --password-stdin git.izebra.net
##run: docker login -u zebra -p "${{ secrets.TOKEN }}" git.izebra.net
- name: Checkout code
uses: actions/checkout@v2
- name: Build Docker image
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
push: true
tags: git.izebra.net/mirrored_repos/nexusdash:latest # Adjust the Gitea registry URL and team name
- name: Push Docker image to Gitea
run: docker push git.izebra.net/git.izebra.net/mirrored_repos/nexusdash:latest # Adjust the Gitea registry URL and team name
# Add any additional steps you need for deployment or other tasks

View File

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

View File

@ -98,7 +98,7 @@ docker run -d \
### Environmental Variables
Please Reference `app/settings_exmaple.py` to see all the variables
Please Reference `app/settings_example.py` to see all the variables
* Required:
* APP_SECRET_KEY (Must be provided)

View File

@ -5,7 +5,6 @@ from flask_assets import Environment
from webassets import Bundle
import time
from app.models import db, migrate, PlayKey
from app.schemas import ma
from app.forms import CustomUserManager
from flask_user import user_registered, current_user, user_logged_in
from flask_wtf.csrf import CSRFProtect
@ -128,8 +127,6 @@ def register_extensions(app):
"""
db.init_app(app)
migrate.init_app(app, db)
ma.init_app(app)
scheduler.init_app(app)
scheduler.start()
@ -204,19 +201,23 @@ def register_settings(app):
# Load environment specific settings
app.config['TESTING'] = False
app.config['DEBUG'] = False
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
"pool_pre_ping": True,
"pool_size": 10,
"max_overflow": 2,
"pool_recycle": 300,
"pool_pre_ping": True,
"pool_use_lifo": True
}
# always pull these two from the env
app.config['SECRET_KEY'] = os.getenv(
'APP_SECRET_KEY',
app.config['APP_SECRET_KEY']
)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
'APP_DATABASE_URI',
app.config['APP_DATABASE_URI']
)
# try to get overides, otherwise just use what we have already
app.config['USER_ENABLE_REGISTER'] = os.getenv(
'USER_ENABLE_REGISTER',
app.config['USER_ENABLE_REGISTER']
@ -241,14 +242,6 @@ def register_settings(app):
'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(
'MAIL_SERVER',
app.config['MAIL_SERVER']
@ -312,6 +305,42 @@ def register_settings(app):
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):

View File

@ -19,16 +19,12 @@ from app.models import (
db,
Friends
)
from app.schemas import AccountSchema
from app import gm_level, log_audit
from app.forms import EditGMLevelForm, EditEmailForm
from sqlalchemy import or_
accounts_blueprint = Blueprint('accounts', __name__)
account_schema = AccountSchema()
@accounts_blueprint.route('/', methods=['GET'])
@login_required
@gm_level(3)

View File

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

View File

@ -1,17 +1,15 @@
from flask_wtf import FlaskForm
from flask_wtf import FlaskForm, Recaptcha, RecaptchaField
from flask import current_app
from flask_user.forms import (
unique_email_validator,
password_validator,
unique_username_validator
LoginForm,
RegisterForm
)
from flask_user import UserManager
from wtforms.widgets import TextArea, NumberInput
from wtforms import (
StringField,
HiddenField,
PasswordField,
BooleanField,
SubmitField,
validators,
@ -36,35 +34,19 @@ def validate_play_key(form, field):
field.data = PlayKey.key_is_valid(key_string=field.data)
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):
def customize(self, app):
self.RegisterFormClass = CustomRegisterForm
self.LoginFormClass = CustomLoginForm
class CustomRegisterForm(FlaskForm):
"""Registration form"""
next = HiddenField()
reg_next = HiddenField()
# Login Info
email = StringField(
'E-Mail',
validators=[
Optional(),
validators.Email('Invalid email address'),
unique_email_validator,
]
)
username = StringField(
'Username',
validators=[
DataRequired(),
unique_username_validator,
]
)
class CustomRegisterForm(RegisterForm):
play_key_id = StringField(
'Play Key',
validators=[
@ -72,21 +54,14 @@ class CustomRegisterForm(FlaskForm):
validate_play_key,
]
)
recaptcha = RecaptchaField(
validators=[CustomRecaptcha()]
)
password = PasswordField('Password', validators=[
DataRequired(),
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 CustomLoginForm(LoginForm):
recaptcha = RecaptchaField(
validators=[CustomRecaptcha()]
)
class CreatePlayKeyForm(FlaskForm):

View File

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

View File

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

View File

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

View File

@ -4,8 +4,7 @@ from app.models import CharacterInfo, Account, CharacterXML, Reports
from app.luclient import get_lot_name
from app import gm_level, scheduler
from sqlalchemy.orm import load_only
import datetime
import xmltodict
import xmltodict, gzip, json, datetime
reports_blueprint = Blueprint('reports', __name__)
@ -44,6 +43,8 @@ def index():
@gm_level(3)
def items_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "items").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/items/by_date.html.j2', data=data, date=date)
@ -62,6 +63,8 @@ def items_graph(start, end):
datasets = []
# get stuff ready
for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y"))
for key in entry.data:
items[key] = get_lot_name(key)
@ -104,6 +107,8 @@ def items_graph(start, end):
@gm_level(3)
def currency_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "currency").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/currency/by_date.html.j2', data=data, date=date)
@ -121,6 +126,8 @@ def currency_graph(start, end):
datasets = []
# get stuff ready
for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters:
data = []
@ -155,6 +162,8 @@ def currency_graph(start, end):
@gm_level(3)
def uscore_by_date(date):
data = Reports.query.filter(Reports.date == date).filter(Reports.report_type == "uscore").first().data
data = gzip.decompress(data)
data = json.loads(data.decode('utf-8'))
return render_template('reports/uscore/by_date.html.j2', data=data, date=date)
@ -172,6 +181,8 @@ def uscore_graph(start, end):
datasets = []
# get stuff ready
for entry in entries:
entry.data = gzip.decompress(entry.data)
entry.data = json.loads(entry.data.decode('utf-8'))
labels.append(entry.date.strftime("%m/%d/%Y"))
for character in characters:
data = []
@ -260,7 +271,7 @@ def gen_item_report():
current_app.logger.error(f"REPORT::ITEMS - {e}")
new_report = Reports(
data=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="items",
date=date
)
@ -308,7 +319,7 @@ def gen_currency_report():
current_app.logger.error(f"REPORT::CURRENCY - {e}")
new_report = Reports(
data=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="currency",
date=date
)
@ -356,7 +367,7 @@ def gen_uscore_report():
current_app.logger.error(f"REPORT::U-SCORE - {e}")
new_report = Reports(
data=report_data,
data=gzip.compress(json.dumps(report_data).encode('utf-8')),
report_type="uscore",
date=date
)

View File

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

View File

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

View File

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

View File

@ -94,6 +94,24 @@
$(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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@
<span class="inventory-count text-bold">
{%if inv_item.attr_c|int > 999 %}
+999
{% else %}
{% elif inv_item.attr_c|int > 1 %}
{{ inv_item.attr_c }}
{% endif %}
</span>

View File

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

View File

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

View File

@ -1,38 +1,44 @@
urllib3=1.23
alembic==1.7.5
APScheduler==3.8.1
astroid==2.9.1
autopep8==1.6.0
backports.zoneinfo==0.2.1
bcrypt==3.2.0
blinker==1.4
blinker==1.7.0
cffi==1.14.6
click==8.0.1
click==8.1.7
colorama==0.4.4
cryptography==36.0.0
dnspython==2.1.0
dominate==2.6.0
email-validator==1.1.3
Flask==2.0.1
Flask==3.0.0
Flask-APScheduler==1.12.3
Flask-Assets==2.0
Flask-Login==0.5.0
Flask-Assets==2.1.0
Flask-Login==0.6.3
Flask-Mail==0.9.1
flask-marshmallow==0.14.0
flask-marshmallow==0.15.0
Flask-Migrate==3.1.0
Flask-SQLAlchemy==2.5.1
Flask-SQLAlchemy==3.1.1
Flask-User==1.0.2.2
Flask-WTF==1.0.0
Flask-WTF==1.2.1
greenlet==1.1.0
gunicorn==21.2.0
idna==3.3
importlib-metadata==6.8.0
importlib-resources==6.1.1
isort==5.10.1
itsdangerous==2.0.1
Jinja2==3.0.1
itsdangerous==2.1.2
Jinja2==3.1.2
lazy-object-proxy==1.7.1
libsass==0.21.0
Mako==1.2.2
MarkupSafe==2.0.1
MarkupSafe==2.1.3
marshmallow==3.14.1
marshmallow-sqlalchemy==0.26.1
mccabe==0.6.1
packaging==23.2
passlib==1.7.4
platformdirs==2.4.1
pycodestyle==2.8.0
@ -47,15 +53,17 @@ pytz==2021.3
pytz-deprecation-shim==0.1.0.post0
six==1.16.0
snowballstemmer==2.2.0
SQLAlchemy==1.4.22
SQLAlchemy==2.0.23
sqlalchemy-datatables==2.0.1
toml==0.10.2
typing_extensions==4.8.0
tzdata==2021.5
tzlocal==4.1
visitor==0.1.3
Wand==0.6.7
webassets==2.0
Werkzeug==2.0.1
Werkzeug==3.0.1
wrapt==1.13.3
WTForms==3.0.0
xmltodict==0.12.0
zipp==3.17.0