* 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> |