* Add config/modules/. * Add hook-aware post-login homepage. * Add flexible character module property database table, plus AuthManager interface. * Add Config.modules. * Modules are imported dynamically with a _provided pseudo-module that contains their Flask blueprint and a config reference. * Add stubs for Map, Members, and Intel modules. * Implement starting code for Campaigns module. * Update sample config. * Update database schema with foreign key constraints (unused for now). * Restructure header HTML and desktop CSS slightly to cleanly display large navigation bars. * Dynamically generate navigation bar. * Properly align character options popup with portrait.master
| @@ -3,8 +3,11 @@ __pycache__/ | |||||
| venv/ | venv/ | ||||
| config/* | config/* | ||||
| config/modules/* | |||||
| data/* | data/* | ||||
| logs/ | logs/ | ||||
| !config/config.yml.sample | |||||
| !config/*.sample | |||||
| !config/modules | |||||
| !config/modules/*.sample | |||||
| !data/schema.sql | !data/schema.sql | ||||
| @@ -24,8 +24,8 @@ Database.path = str(basepath / "data" / "db.sqlite3") | |||||
| eve = EVE(config) | eve = EVE(config) | ||||
| auth = AuthManager(config, eve) | auth = AuthManager(config, eve) | ||||
| catch_exceptions = make_error_catcher(app, "error.mako") | |||||
| route_restricted = make_route_restricter( | |||||
| app.catch_exceptions = make_error_catcher(app, "error.mako") | |||||
| app.route_restricted = make_route_restricter( | |||||
| auth, lambda: redirect(url_for("index"), 303)) | auth, lambda: redirect(url_for("index"), 303)) | ||||
| MakoTemplates(app) | MakoTemplates(app) | ||||
| @@ -38,21 +38,25 @@ def prepare_request(): | |||||
| g.auth = auth | g.auth = auth | ||||
| g.config = config | g.config = config | ||||
| g.eve = eve | g.eve = eve | ||||
| g.modules = config.modules | |||||
| g.version = calefaction.__version__ | g.version = calefaction.__version__ | ||||
| app.before_request(Database.pre_hook) | app.before_request(Database.pre_hook) | ||||
| app.teardown_appcontext(Database.post_hook) | app.teardown_appcontext(Database.post_hook) | ||||
| @app.route("/") | @app.route("/") | ||||
| @catch_exceptions | |||||
| @app.catch_exceptions | |||||
| def index(): | def index(): | ||||
| success, _ = try_func(auth.is_authenticated) | success, _ = try_func(auth.is_authenticated) | ||||
| if success: | if success: | ||||
| return render_template("home.mako") | |||||
| module = config.get("modules.home") | |||||
| if module: | |||||
| return config.modules[module].home() | |||||
| return render_template("default_home.mako") | |||||
| return render_template("landing.mako") | return render_template("landing.mako") | ||||
| @app.route("/login", methods=["GET", "POST"]) | @app.route("/login", methods=["GET", "POST"]) | ||||
| @catch_exceptions | |||||
| @app.catch_exceptions | |||||
| def login(): | def login(): | ||||
| code = request.args.get("code") | code = request.args.get("code") | ||||
| state = request.args.get("state") | state = request.args.get("state") | ||||
| @@ -65,7 +69,7 @@ def login(): | |||||
| return redirect(url_for("index"), 303) | return redirect(url_for("index"), 303) | ||||
| @app.route("/logout", methods=["GET", "POST"]) | @app.route("/logout", methods=["GET", "POST"]) | ||||
| @catch_exceptions | |||||
| @app.catch_exceptions | |||||
| def logout(): | def logout(): | ||||
| if request.method == "GET": | if request.method == "GET": | ||||
| return render_template("logout.mako") | return render_template("logout.mako") | ||||
| @@ -75,8 +79,8 @@ def logout(): | |||||
| return redirect(url_for("index"), 303) | return redirect(url_for("index"), 303) | ||||
| @app.route("/settings/style/<style>", methods=["POST"]) | @app.route("/settings/style/<style>", methods=["POST"]) | ||||
| @catch_exceptions | |||||
| @route_restricted | |||||
| @app.catch_exceptions | |||||
| @app.route_restricted | |||||
| def set_style(style): | def set_style(style): | ||||
| if not auth.set_character_style(style): | if not auth.set_character_style(style): | ||||
| abort(404) | abort(404) | ||||
| @@ -10,7 +10,7 @@ from .exceptions import AccessDeniedError | |||||
| __all__ = ["AuthManager"] | __all__ = ["AuthManager"] | ||||
| _SCOPES = ["publicData", "characterAssetsRead"] # ... | |||||
| _SCOPES = [] # ... | |||||
| class AuthManager: | class AuthManager: | ||||
| """Authentication manager. Handles user access and management.""" | """Authentication manager. Handles user access and management.""" | ||||
| @@ -196,10 +196,20 @@ class AuthManager: | |||||
| self._invalidate_session() | self._invalidate_session() | ||||
| raise AccessDeniedError() | raise AccessDeniedError() | ||||
| def _update_prop_cache(self, module, prop, value): | |||||
| """Update the value of a character module property in the cache.""" | |||||
| if hasattr(g, "_character_modprops"): | |||||
| propcache = g._character_modprops | |||||
| else: | |||||
| propcache = g._character_modprops = {module: {}} | |||||
| if module not in propcache: | |||||
| propcache[module] = {} | |||||
| propcache[module][prop] = value | |||||
| def get_character_id(self): | def get_character_id(self): | ||||
| """Return the character ID associated with the current session. | """Return the character ID associated with the current session. | ||||
| Returns None if the session is invalid or is not associated with a | |||||
| Return None if the session is invalid or is not associated with a | |||||
| character. | character. | ||||
| """ | """ | ||||
| if not self._check_session(): | if not self._check_session(): | ||||
| @@ -212,7 +222,7 @@ class AuthManager: | |||||
| def get_character_prop(self, prop): | def get_character_prop(self, prop): | ||||
| """Look up a property for the current session's character. | """Look up a property for the current session's character. | ||||
| Returns None if the session is invalid, is not associated with a | |||||
| Return None if the session is invalid, is not associated with a | |||||
| character, or the property has no non-default value. | character, or the property has no non-default value. | ||||
| """ | """ | ||||
| cid = self.get_character_id() | cid = self.get_character_id() | ||||
| @@ -240,6 +250,40 @@ class AuthManager: | |||||
| delattr(g, "_character_props") | delattr(g, "_character_props") | ||||
| return True | return True | ||||
| def get_character_modprop(self, module, prop): | |||||
| """Look up a module property for the current session's character. | |||||
| Return None if the session is invalid, is not associated with a | |||||
| character, or the property has no non-default value. | |||||
| """ | |||||
| cid = self.get_character_id() | |||||
| if not cid: | |||||
| return None | |||||
| if hasattr(g, "_character_modprops"): | |||||
| propcache = g._character_modprops | |||||
| if module in propcache and prop in propcache[module]: | |||||
| return propcache[module][prop] | |||||
| value = g.db.get_character_modprop(cid, module, prop) | |||||
| self._update_prop_cache(module, prop, value) | |||||
| return value | |||||
| def set_character_modprop(self, module, prop, value): | |||||
| """Update a module property for the current session's character. | |||||
| Return whether successful. | |||||
| """ | |||||
| cid = self.get_character_id() | |||||
| if not cid: | |||||
| return False | |||||
| self._debug("Setting module %s property %s to %s for char id=%d", | |||||
| module, prop, value, cid) | |||||
| g.db.set_character_modprop(cid, module, prop, value) | |||||
| self._update_prop_cache(module, prop, value) | |||||
| return True | |||||
| def is_authenticated(self): | def is_authenticated(self): | ||||
| """Return whether the user has permission to access this site. | """Return whether the user has permission to access this site. | ||||
| @@ -2,14 +2,39 @@ | |||||
| import yaml | import yaml | ||||
| from .module import Module | |||||
| __all__ = ["Config"] | __all__ = ["Config"] | ||||
| class _ModuleIndex(list): | |||||
| """List class that supports attribute access to its elements by key.""" | |||||
| def __init__(self): | |||||
| super().__init__() | |||||
| self._index = {} | |||||
| def __getitem__(self, key): | |||||
| try: | |||||
| return super().__getitem__(key) | |||||
| except TypeError: | |||||
| return super().__getitem__(self._index[key]) | |||||
| def __getattr__(self, attr): | |||||
| return self[self._index[attr]] | |||||
| def append(self, key, value): | |||||
| self._index[key] = len(self) | |||||
| super().append(value) | |||||
| class Config: | class Config: | ||||
| """Stores application-wide configuration info.""" | """Stores application-wide configuration info.""" | ||||
| def __init__(self, confdir): | def __init__(self, confdir): | ||||
| self._dir = confdir | |||||
| self._filename = confdir / "config.yml" | self._filename = confdir / "config.yml" | ||||
| self._data = None | self._data = None | ||||
| self._modules = _ModuleIndex() | |||||
| self._load() | self._load() | ||||
| def _load(self): | def _load(self): | ||||
| @@ -17,6 +42,10 @@ class Config: | |||||
| with self._filename.open("rb") as fp: | with self._filename.open("rb") as fp: | ||||
| self._data = yaml.load(fp) | self._data = yaml.load(fp) | ||||
| self._modules = _ModuleIndex() | |||||
| for name in self.get("modules.enabled", []): | |||||
| self._modules.append(name, Module(self, name)) | |||||
| def get(self, key="", default=None): | def get(self, key="", default=None): | ||||
| """Acts like a dict lookup in the config file. | """Acts like a dict lookup in the config file. | ||||
| @@ -31,12 +60,32 @@ class Config: | |||||
| return obj | return obj | ||||
| @property | @property | ||||
| def modules(self): | |||||
| """Return a list-like object (a _ModuleIndex) of loaded modules.""" | |||||
| return self._modules | |||||
| @property | |||||
| def scheme(self): | def scheme(self): | ||||
| """Return the site's configured scheme, either "http" or "https".""" | """Return the site's configured scheme, either "http" or "https".""" | ||||
| return "https" if self.get("site.https") else "http" | return "https" if self.get("site.https") else "http" | ||||
| def install(self, app): | def install(self, app): | ||||
| """Install relevant config parameters into the application.""" | |||||
| """Install relevant config into the application, including modules.""" | |||||
| app.config["SERVER_NAME"] = self.get("site.canonical") | app.config["SERVER_NAME"] = self.get("site.canonical") | ||||
| app.config["PREFERRED_URL_SCHEME"] = self.scheme | app.config["PREFERRED_URL_SCHEME"] = self.scheme | ||||
| app.secret_key = self.get("auth.session_key") | app.secret_key = self.get("auth.session_key") | ||||
| for module in self.modules: | |||||
| module.install(app) | |||||
| def load_module_config(self, name): | |||||
| """Load and return a module config file. | |||||
| Returns a YAML parse of {confdir}/modules/{name}.yml, or None. | |||||
| """ | |||||
| filename = self._dir / "modules" / (name + ".yml") | |||||
| try: | |||||
| with filename.open("rb") as fp: | |||||
| return yaml.load(fp) | |||||
| except FileNotFoundError: | |||||
| return None | |||||
| @@ -203,3 +203,17 @@ class Database: | |||||
| """Drop any authentication info for the given character.""" | """Drop any authentication info for the given character.""" | ||||
| with self._conn as conn: | with self._conn as conn: | ||||
| conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,)) | conn.execute("DELETE FROM auth WHERE auth_character = ?", (cid,)) | ||||
| def set_character_modprop(self, cid, module, prop, value): | |||||
| """Add or update a character module property.""" | |||||
| with self._conn as conn: | |||||
| conn.execute("""INSERT OR REPLACE INTO character_prop | |||||
| (cprop_character, cprop_module, cprop_key, cprop_value) | |||||
| VALUES (?, ?, ?, ?)""", (cid, module, prop, value)) | |||||
| def get_character_modprop(self, cid, module, prop): | |||||
| """Return the value of a character module property, or None.""" | |||||
| query = """SELECT cprop_value FROM character_prop | |||||
| WHERE cprop_character = ? AND cprop_module = ? AND cprop_key = ?""" | |||||
| res = self._conn.execute(query, (cid, module, prop)).fetchall() | |||||
| return res[0][0] if res else None | |||||
| @@ -0,0 +1,53 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| import importlib | |||||
| import sys | |||||
| from types import ModuleType | |||||
| from flask import Blueprint | |||||
| from . import baseLogger | |||||
| __all__ = ["Module"] | |||||
| class Module: | |||||
| """Handles common operations for Calefaction's modular components.""" | |||||
| def __init__(self, config, name): | |||||
| self._config = config | |||||
| self._name = name | |||||
| self._logger = baseLogger.getChild("module").getChild(name) | |||||
| self._module = None | |||||
| self._blueprint = None | |||||
| def __getattr__(self, attr): | |||||
| return getattr(self._module, attr) | |||||
| def _import(self, app): | |||||
| """Set up the environment for the module, then import it.""" | |||||
| base = "calefaction.modules" | |||||
| fullname = base + "." + self._name | |||||
| self._blueprint = bp = Blueprint(self._name, fullname) | |||||
| bp.rroute = lambda *a, **kw: (lambda f: bp.route(*a, **kw)( | |||||
| app.catch_exceptions(app.route_restricted(f)))) | |||||
| provided = ModuleType(base + "._provided") | |||||
| provided.__package__ = base | |||||
| provided.app = app | |||||
| provided.blueprint = bp | |||||
| provided.config = self._config.load_module_config(self._name) | |||||
| sys.modules[provided.__name__] = provided | |||||
| self._module = importlib.import_module(fullname) | |||||
| del sys.modules[provided.__name__] | |||||
| def install(self, app): | |||||
| """Load this module and register it with the application.""" | |||||
| self._import(app) | |||||
| app.register_blueprint(self._blueprint) | |||||
| def navitem(self): | |||||
| """Return a navigation bar HTML snippet for this module, or None.""" | |||||
| if hasattr(self._module, "navitem"): | |||||
| return self._module.navitem() | |||||
| @@ -0,0 +1,35 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| from flask import abort, g | |||||
| from flask_mako import render_template | |||||
| from ._provided import blueprint, config | |||||
| def get_current(): | |||||
| """Return the name of the currently selected campaign, or None.""" | |||||
| if not config["enabled"]: | |||||
| return None | |||||
| setting = g.auth.get_character_modprop("campaigns", "current") | |||||
| if not setting or setting not in config["enabled"]: | |||||
| return config["enabled"][0] | |||||
| return setting | |||||
| def home(): | |||||
| return render_template("campaigns/campaign.mako", current=get_current()) | |||||
| def navitem(): | |||||
| current = get_current() | |||||
| if current: | |||||
| result = render_template("campaigns/navitem.mako", current=current) | |||||
| return result.decode("utf8") | |||||
| @blueprint.rroute("/campaign") | |||||
| def campaign(): | |||||
| return home() | |||||
| @blueprint.rroute("/settings/campaign/<campaign>", methods=["POST"]) | |||||
| def set_campaign(campaign): | |||||
| if campaign not in config["enabled"]: | |||||
| abort(404) | |||||
| g.auth.set_character_modprop("campaigns", "current", campaign) | |||||
| return "", 204 | |||||
| @@ -0,0 +1,6 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # ... | |||||
| def navitem(): | |||||
| return "Intel" | |||||
| @@ -0,0 +1,6 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # ... | |||||
| def navitem(): | |||||
| return "Map" | |||||
| @@ -0,0 +1,6 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # ... | |||||
| def navitem(): | |||||
| return "Members" | |||||
| @@ -20,12 +20,15 @@ corp: | |||||
| modules: | modules: | ||||
| # Most site functionality comes from optional modules selected based on your | # Most site functionality comes from optional modules selected based on your | ||||
| # corp's needs. They are located in the calefaction/modules/ directory. The | |||||
| # corp's needs. They are located in the calefaction/modules/ directory. Their | |||||
| # order below determines how they appear in the navigation menu. | # order below determines how they appear in the navigation menu. | ||||
| # List of enabled modules: | # List of enabled modules: | ||||
| - campaigns | |||||
| - map | |||||
| - members | |||||
| enabled: | |||||
| - campaigns | |||||
| - map | |||||
| - members | |||||
| # Module to show on the home page after users log in: | |||||
| home: campaigns | |||||
| auth: | auth: | ||||
| # Secure session signing key. Never share with anyone. Can generate with | # Secure session signing key. Never share with anyone. Can generate with | ||||
| @@ -44,6 +47,7 @@ auth: | |||||
| style: | style: | ||||
| # Default stylesheet from static/styles/*.css: | # Default stylesheet from static/styles/*.css: | ||||
| default: null | default: null | ||||
| # List of enabled stylesheets: | |||||
| enabled: | enabled: | ||||
| - amarr | - amarr | ||||
| - caldari | - caldari | ||||
| @@ -0,0 +1,15 @@ | |||||
| # This is a sample config file for Calefaction's Campaigns module. | |||||
| # Copy this to campaigns.yml and modify it to set up the module. | |||||
| # You must restart the server after making any changes. | |||||
| # List of active campaigns, in your preferred order: | |||||
| enabled: | |||||
| - Foo | |||||
| - Bar | |||||
| campaigns: | |||||
| # ... | |||||
| Foo: | |||||
| a: b | |||||
| Bar: | |||||
| a: b | |||||
| @@ -1,14 +1,5 @@ | |||||
| -- Schema for Calefaction's internal database | -- Schema for Calefaction's internal database | ||||
| DROP TABLE IF EXISTS session; | |||||
| CREATE TABLE session ( | |||||
| session_id INTEGER PRIMARY KEY AUTOINCREMENT, | |||||
| session_character INTEGER DEFAULT NULL, | |||||
| session_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||||
| session_touched TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |||||
| ); | |||||
| DROP TABLE IF EXISTS character; | DROP TABLE IF EXISTS character; | ||||
| CREATE TABLE character ( | CREATE TABLE character ( | ||||
| @@ -17,11 +8,36 @@ CREATE TABLE character ( | |||||
| character_style TEXT DEFAULT NULL | character_style TEXT DEFAULT NULL | ||||
| ); | ); | ||||
| DROP TABLE IF EXISTS session; | |||||
| CREATE TABLE session ( | |||||
| session_id INTEGER PRIMARY KEY AUTOINCREMENT, | |||||
| session_character INTEGER DEFAULT NULL, | |||||
| session_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||||
| session_touched TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |||||
| FOREIGN KEY (session_character) REFERENCES character (character_id) | |||||
| ON DELETE SET NULL ON UPDATE CASCADE | |||||
| ); | |||||
| DROP TABLE IF EXISTS auth; | DROP TABLE IF EXISTS auth; | ||||
| CREATE TABLE auth ( | CREATE TABLE auth ( | ||||
| auth_character INTEGER PRIMARY KEY, | auth_character INTEGER PRIMARY KEY, | ||||
| auth_token BLOB, | auth_token BLOB, | ||||
| auth_refresh BLOB, | auth_refresh BLOB, | ||||
| auth_token_expiry TIMESTAMP | |||||
| auth_token_expiry TIMESTAMP, | |||||
| FOREIGN KEY (auth_character) REFERENCES character (character_id) | |||||
| ON DELETE CASCADE ON UPDATE CASCADE | |||||
| ); | |||||
| DROP TABLE IF EXISTS character_prop; | |||||
| CREATE TABLE character_prop ( | |||||
| cprop_character INTEGER, | |||||
| cprop_module TEXT, | |||||
| cprop_key TEXT, | |||||
| cprop_value TEXT, | |||||
| UNIQUE (cprop_character, cprop_module, cprop_key), | |||||
| FOREIGN KEY (cprop_character) REFERENCES character (character_id) | |||||
| ON DELETE CASCADE ON UPDATE CASCADE | |||||
| ); | ); | ||||
| @@ -28,6 +28,10 @@ h2 { | |||||
| margin: 0.5em 0; | margin: 0.5em 0; | ||||
| } | } | ||||
| .understate { | |||||
| font-weight: normal; | |||||
| } | |||||
| #container { | #container { | ||||
| display: flex; | display: flex; | ||||
| flex: 1; | flex: 1; | ||||
| @@ -66,10 +70,6 @@ header > div, footer > div { | |||||
| margin: 0 auto; | margin: 0 auto; | ||||
| } | } | ||||
| header > div > div { | |||||
| display: inline-block; | |||||
| } | |||||
| @media (min-width: 800px) { | @media (min-width: 800px) { | ||||
| main, header > div, footer > div { | main, header > div, footer > div { | ||||
| max-width: 1000px; | max-width: 1000px; | ||||
| @@ -81,17 +81,30 @@ header > div > div { | |||||
| padding: 0.5em 1.5em; | padding: 0.5em 1.5em; | ||||
| } | } | ||||
| header > div { | |||||
| display: table; | |||||
| } | |||||
| footer > div { | footer > div { | ||||
| padding: 0.5em 1.5em; | padding: 0.5em 1.5em; | ||||
| } | } | ||||
| header > div > .left { | |||||
| margin: 0.5em 1.5em 0.5em 0; | |||||
| header > div > div { | |||||
| display: table-row; | |||||
| } | |||||
| header > div > div > div { | |||||
| display: table-cell; | |||||
| } | } | ||||
| header > div > .right { | |||||
| margin: 0.5em 0; | |||||
| float: right; | |||||
| header .left { | |||||
| padding: 0.5em 1.5em 0.5em 0; | |||||
| } | |||||
| header .right { | |||||
| padding: 0.5em 0; | |||||
| text-align: right; | |||||
| white-space: nowrap; | |||||
| } | } | ||||
| } | } | ||||
| @@ -105,19 +118,38 @@ header > div > div { | |||||
| padding: 0.5em 1em; | padding: 0.5em 1em; | ||||
| } | } | ||||
| header > div > div { | |||||
| margin: 0.5em 1em; | |||||
| header > div > div > div { | |||||
| display: inline-block; | |||||
| } | } | ||||
| header > div > .left { | |||||
| margin-bottom: 0em; | |||||
| header .left { | |||||
| margin: 0.5em 1em 0em 1em; | |||||
| } | |||||
| header .right { | |||||
| margin: 0.25em 1em 0.5em 1em; | |||||
| } | } | ||||
| } | } | ||||
| header nav { | header nav { | ||||
| display: inline-block; | display: inline-block; | ||||
| margin-left: 1.5em; | |||||
| vertical-align: middle; | vertical-align: middle; | ||||
| padding: 0.25em 0; | |||||
| } | |||||
| header nav > ul { | |||||
| display: inline-block; | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| } | |||||
| header nav > ul > li { | |||||
| display: inline-block; | |||||
| } | |||||
| header nav > ul > li:not(:last-child):after { | |||||
| content: " \007c"; | |||||
| color: #777; | |||||
| } | } | ||||
| header .spacer { | header .spacer { | ||||
| @@ -146,6 +178,7 @@ footer ul li:not(:last-child):after { | |||||
| #corp-masthead { | #corp-masthead { | ||||
| color: #EAEAEA; | color: #EAEAEA; | ||||
| margin-right: 1.5em; | |||||
| } | } | ||||
| #corp-masthead:hover { | #corp-masthead:hover { | ||||
| @@ -189,6 +222,11 @@ footer ul li:not(:last-child):after { | |||||
| } | } | ||||
| #character-portrait { | #character-portrait { | ||||
| position: relative; | |||||
| display: inline-block; | |||||
| } | |||||
| #character-portrait img { | |||||
| height: 32px; | height: 32px; | ||||
| width: 32px; | width: 32px; | ||||
| margin-right: 0.25em; | margin-right: 0.25em; | ||||
| @@ -15,7 +15,7 @@ $(function() { | |||||
| // Toggle character options on click: | // Toggle character options on click: | ||||
| var charopts = $("#character-options"); | var charopts = $("#character-options"); | ||||
| charopts.hide(); | charopts.hide(); | ||||
| $("#character-portrait").click(function() { | |||||
| $("#character-portrait img").click(function() { | |||||
| if (charopts.is(":visible")) { | if (charopts.is(":visible")) { | ||||
| charopts.hide(); | charopts.hide(); | ||||
| $(document).off("mouseup.charopts"); | $(document).off("mouseup.charopts"); | ||||
| @@ -23,7 +23,7 @@ $(function() { | |||||
| charopts.show(); | charopts.show(); | ||||
| $(document).on("mouseup.charopts", function(e) { | $(document).on("mouseup.charopts", function(e) { | ||||
| if (!overlaps(e.target, charopts) && | if (!overlaps(e.target, charopts) && | ||||
| !overlaps(e.target, $("#character-portrait"))) { | |||||
| !overlaps(e.target, $("#character-portrait img"))) { | |||||
| charopts.hide(); | charopts.hide(); | ||||
| $(document).off("mouseup.charopts"); | $(document).off("mouseup.charopts"); | ||||
| } | } | ||||
| @@ -23,18 +23,20 @@ | |||||
| <%block name="header"> | <%block name="header"> | ||||
| <header class="styled-border"> | <header class="styled-border"> | ||||
| <div> | <div> | ||||
| <div class="left"> | |||||
| <%block name="lefthead"> | |||||
| <a id="corp-masthead" title="Home" href="${url_for('index')}"> | |||||
| <img alt="Logo" src="${g.eve.image.corp(g.config.get('corp.id'), 256)}"/> | |||||
| <h1>${g.config.get("corp.name") | h}</h1> | |||||
| </a> | |||||
| </%block> | |||||
| </div> | |||||
| <div class="right"> | |||||
| <%block name="righthead"> | |||||
| <img class="spacer" alt="" src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="/> | |||||
| </%block> | |||||
| <div> | |||||
| <div class="left"> | |||||
| <%block name="lefthead"> | |||||
| <a id="corp-masthead" title="Home" href="${url_for('index')}"> | |||||
| <img alt="Logo" src="${g.eve.image.corp(g.config.get('corp.id'), 256)}"/> | |||||
| <h1>${g.config.get("corp.name") | h}</h1> | |||||
| </a> | |||||
| </%block> | |||||
| </div> | |||||
| <div class="right"> | |||||
| <%block name="righthead"> | |||||
| <img class="spacer" alt="" src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="/> | |||||
| </%block> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </header> | </header> | ||||
| @@ -2,28 +2,37 @@ | |||||
| <%block name="lefthead"> | <%block name="lefthead"> | ||||
| ${parent.lefthead()} | ${parent.lefthead()} | ||||
| <nav> | <nav> | ||||
| Campaign: XYZ | Map | Intel | Members... | |||||
| <ul> | |||||
| % for module in g.modules: | |||||
| <% navitem = module.navitem() %> | |||||
| % if navitem: | |||||
| <li>${navitem}</li> | |||||
| % endif | |||||
| % endfor | |||||
| </ul> | |||||
| </nav> | </nav> | ||||
| </%block> | </%block> | ||||
| <%block name="righthead"> | <%block name="righthead"> | ||||
| <img id="character-portrait" class="styled-border" alt="Portrait" src="${g.eve.image.character(g.auth.get_character_id(), 256)}"/> | |||||
| <div id="character-options" class="styled-border"> | |||||
| <div id="style-options"> | |||||
| <% cur_style = g.auth.get_character_prop("style") or g.config.get("style.default") %> | |||||
| % for style in g.config.get("style.enabled"): | |||||
| <% | |||||
| stitle = style.title() | |||||
| url = url_for('staticv', filename='images/style/{}.png'.format(style)) | |||||
| %> | |||||
| <form action="${url_for('set_style', style=style)}" method="post"${' class="cur"' if style == cur_style else ''}> | |||||
| <input type="submit" title="${stitle}" value="${stitle}" data-style="${style}"${' disabled' if style == cur_style else ''} | |||||
| style="background-image: url('${url}')"> | |||||
| </form> | |||||
| % endfor | |||||
| <div id="character-portrait"> | |||||
| <img class="styled-border" alt="Portrait" src="${g.eve.image.character(g.auth.get_character_id(), 256)}"/> | |||||
| <div id="character-options" class="styled-border"> | |||||
| <div id="style-options"> | |||||
| <% cur_style = g.auth.get_character_prop("style") or g.config.get("style.default") %> | |||||
| % for style in g.config.get("style.enabled"): | |||||
| <% | |||||
| stitle = style.title() | |||||
| url = url_for('staticv', filename='images/style/{}.png'.format(style)) | |||||
| %> | |||||
| <form action="${url_for('set_style', style=style)}" method="post"${' class="cur"' if style == cur_style else ''}> | |||||
| <input type="submit" title="${stitle | h}" value="${stitle | h}" data-style="${style | h}"${' disabled' if style == cur_style else ''} | |||||
| style="background-image: url('${url}')"> | |||||
| </form> | |||||
| % endfor | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <span id="character-summary"> | <span id="character-summary"> | ||||
| ${g.auth.get_character_prop("name")} | |||||
| ${g.auth.get_character_prop("name") | h} | |||||
| <span class="sep">[</span><a id="logout" title="Log out" href="${url_for('logout')}">log out</a><span class="sep">]</span> | <span class="sep">[</span><a id="logout" title="Log out" href="${url_for('logout')}">log out</a><span class="sep">]</span> | ||||
| </span> | </span> | ||||
| </%block> | </%block> | ||||
| @@ -0,0 +1,7 @@ | |||||
| <%inherit file="../_default.mako"/> | |||||
| %if current: | |||||
| <h2><span class="understate">Campaign:</span> ${current} <!-- select ... --></h2> | |||||
| <p>Hello! ...</p> | |||||
| %else: | |||||
| <p>No campaigns currently.</p> | |||||
| %endif | |||||
| @@ -0,0 +1 @@ | |||||
| Campaign: <a href="${url_for('campaigns.campaign')}">${current}</a> | |||||
| @@ -0,0 +1,2 @@ | |||||
| <%inherit file="_default.mako"/> | |||||
| <p>Hi, ${g.auth.get_character_prop("name") | h}!</p> | |||||
| @@ -1,2 +0,0 @@ | |||||
| <%inherit file="_default.mako"/> | |||||
| <p>Hi, ${g.auth.get_character_prop("name")}!</p> | |||||