Browse Source

Add structure and supporting code for modules; add Campaigns module.

* 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
Ben Kurtovic 7 years ago
parent
commit
dfa695bb83
21 changed files with 385 additions and 73 deletions
  1. +4
    -1
      .gitignore
  2. +12
    -8
      app.py
  3. +47
    -3
      calefaction/auth.py
  4. +50
    -1
      calefaction/config.py
  5. +14
    -0
      calefaction/database.py
  6. +53
    -0
      calefaction/module.py
  7. +35
    -0
      calefaction/modules/campaigns.py
  8. +6
    -0
      calefaction/modules/intel.py
  9. +6
    -0
      calefaction/modules/map.py
  10. +6
    -0
      calefaction/modules/members.py
  11. +8
    -4
      config/config.yml.sample
  12. +15
    -0
      config/modules/campaigns.yml.sample
  13. +26
    -10
      data/schema.sql
  14. +52
    -14
      static/main.css
  15. +2
    -2
      static/main.js
  16. +14
    -12
      templates/_base.mako
  17. +25
    -16
      templates/_default.mako
  18. +7
    -0
      templates/campaigns/campaign.mako
  19. +1
    -0
      templates/campaigns/navitem.mako
  20. +2
    -0
      templates/default_home.mako
  21. +0
    -2
      templates/home.mako

+ 4
- 1
.gitignore View File

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

+ 12
- 8
app.py View File

@@ -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)


+ 47
- 3
calefaction/auth.py View File

@@ -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.




+ 50
- 1
calefaction/config.py View File

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

+ 14
- 0
calefaction/database.py View File

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

+ 53
- 0
calefaction/module.py View File

@@ -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()

+ 35
- 0
calefaction/modules/campaigns.py View File

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

+ 6
- 0
calefaction/modules/intel.py View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-

# ...

def navitem():
return "Intel"

+ 6
- 0
calefaction/modules/map.py View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-

# ...

def navitem():
return "Map"

+ 6
- 0
calefaction/modules/members.py View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-

# ...

def navitem():
return "Members"

+ 8
- 4
config/config.yml.sample View File

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


+ 15
- 0
config/modules/campaigns.yml.sample View File

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

+ 26
- 10
data/schema.sql View File

@@ -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
); );

+ 52
- 14
static/main.css View File

@@ -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;


+ 2
- 2
static/main.js View File

@@ -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");
} }


+ 14
- 12
templates/_base.mako View File

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


+ 25
- 16
templates/_default.mako View File

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


+ 7
- 0
templates/campaigns/campaign.mako View File

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

+ 1
- 0
templates/campaigns/navitem.mako View File

@@ -0,0 +1 @@
Campaign: <a href="${url_for('campaigns.campaign')}">${current}</a>

+ 2
- 0
templates/default_home.mako View File

@@ -0,0 +1,2 @@
<%inherit file="_default.mako"/>
<p>Hi, ${g.auth.get_character_prop("name") | h}!</p>

+ 0
- 2
templates/home.mako View File

@@ -1,2 +0,0 @@
<%inherit file="_default.mako"/>
<p>Hi, ${g.auth.get_character_prop("name")}!</p>

Loading…
Cancel
Save