git - alex wennerberg
    1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
from flask import Flask, render_template, request, g
import sqlite3
from urllib.parse import quote as url_quote
import time
import logging

app = Flask(__name__)

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

@app.before_request
def before_request():
    g.start_time = time.time()

@app.after_request
def after_request(response):
    if hasattr(g, 'start_time'):
        duration_ms = (time.time() - g.start_time) * 1000
        logger.info(f"{request.method} {request.path} - {response.status_code} - {duration_ms:.2f}ms")
    return response

def get_db():
    db = sqlite3.connect('3cb.db')
    db.row_factory = sqlite3.Row
    db.set_trace_callback(lambda stmt: logger.debug(f"SQLite: {stmt}"))
    return db

def execute_query(db, query, params=()):
    """Execute a query with logging of query text and execution time."""
    start_time = time.time()
    result = db.execute(query, params)
    duration_ms = (time.time() - start_time) * 1000

    # Format query for logging (remove extra whitespace/newlines)
    formatted_query = ' '.join(query.split())
    logger.info(f"SQL Query ({duration_ms:.2f}ms): {formatted_query} | Params: {params}")

    return result

@app.route('/')
def index():
    db = get_db()
    rounds = db.execute("select r.id, r.fileid, r.date, count(distinct d.player) as player_count from round r left join deck d on r.id = d.round group by r.id, r.fileid, r.date order by r.id desc").fetchall()
    db.close()
    subtitle = "rounds"
    return render_template('index.html', rounds=rounds, subtitle=subtitle)

@app.route('/round/<int:id>')
def round_detail(id):
    db = get_db()
    round_data = db.execute("select * from round where id = ?", (id,)).fetchone()
    matches = db.execute("""
        select *
        from round_score
        where round = ?
        order by final_score desc, prelim_score desc
    """, (id,)).fetchall()
    banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
    db.close()
    subtitle = "round " + str(id)
    return render_template('matches.html', round=round_data, matches=matches, banned_cards=banned_cards, subtitle=subtitle)

@app.route('/card')
def card():
    name = request.args.get('name')
    db = get_db()
    matches = db.execute("""
        select *
        from round_score
        where card1 = ?1 or card2 = ?1 or card3 = ?1
        order by round desc
    """, (name,)).fetchall()
    banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
    db.close()
    subtitle = name
    return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)

@app.route('/player')
def player():
    name = request.args.get('name')
    db = get_db()
    matches = db.execute("""
        select *
        from round_score
        where player = ?
        order by round desc
    """, (name,)).fetchall()
    banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
    db.close()
    subtitle = "player " + name
    return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)

@app.route('/deck')
def deck():
    cards = sorted([request.args.get('c1'), request.args.get('c2'), request.args.get('c3')])
    db = get_db()
    matches = db.execute("""
        select *
        from round_score
        where
        card1 = ?1 and card2 = ?2 and card3 = ?3 or
        card1 = ?1 and card2 = ?3 and card3 = ?2 or
        card1 = ?2 and card2 = ?1 and card3 = ?3 or
        card1 = ?2 and card2 = ?3 and card3 = ?1 or
        card1 = ?3 and card2 = ?1 and card3 = ?2 or
        card1 = ?3 and card2 = ?2 and card3 = ?1
        order by round desc
    """, (cards[0], cards[1], cards[2])).fetchall()
    banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
    db.close()
    subtitle = "; ".join(cards)
    return render_template('matches.html', matches=matches, banned_cards=banned_cards, subtitle=subtitle)

@app.route('/players')
def players():
    db = get_db()
    players = db.execute("""
      select 
        d.player,
        count(distinct d.round) as rounds_played,
        sum(case when r.rank = 1 then 1 else 0 end) as rounds_won,
        min(ro.date) as first_played
      from deck d
      left join rank r on d.round = r.round and d.player = r.player
      left join round ro on d.round = ro.id
      group by d.player
      order by rounds_played desc
    """).fetchall()
    db.close()
    subtitle = "players"
    return render_template('players.html', players=players, subtitle=subtitle)

@app.route('/banned')
def banned():
    db = get_db()
    banned_cards = db.execute("select name from ban order by name").fetchall()
    db.close()
    subtitle = "banned cards"
    return render_template('banned.html', banned_cards=banned_cards, subtitle=subtitle)

@app.route('/cards')
def cards():
    db = get_db()
    cards = db.execute("""
        SELECT card, COUNT(*) as play_count 
        FROM card 
        WHERE card IS NOT NULL 
        GROUP BY card 
        ORDER BY play_count DESC, card ASC
    """).fetchall()
    banned_cards = [row["name"] for row in db.execute("select name from ban").fetchall()]
    db.close()
    subtitle = "all cards"
    return render_template('cards.html', cards=cards, banned_cards=banned_cards, subtitle=subtitle)

# Template filter for url encoding
@app.template_filter('url_encode')
def url_encode_filter(s):
    return url_quote(str(s))

# Template filter for mana symbols using Mana font
@app.template_filter('mana_symbols')
def mana_symbols_filter(s):
    if not s or s == 'N/A':
        return s

    import re

    # Parse mana symbols like {W}, {U}, {1}, {2/U}, etc.
    def replace_symbol(match):
        symbol = match.group(1).lower()

        # Handle hybrid mana (e.g., {W/U}, {2/W})
        if '/' in symbol:
            parts = symbol.split('/')
            # Check if first part is a number (e.g., 2/W for hybrid)
            if parts[0].isdigit():
                return f'<i class="ms ms-{parts[1]} ms-cost ms-shadow"></i>'
            else:
                return f'<i class="ms ms-{parts[0]}{parts[1]} ms-cost ms-shadow"></i>'
        # Handle Phyrexian mana (e.g., {W/P})
        elif 'p' in symbol:
            color = symbol.replace('/p', '').replace('p', '')
            return f'<i class="ms ms-{color} ms-p ms-cost ms-shadow"></i>'
        # Regular mana
        else:
            return f'<i class="ms ms-{symbol} ms-cost ms-shadow"></i>'

    # Replace all mana symbols
    result = re.sub(r'\{([^}]+)\}', replace_symbol, s)

    return result

create_mview = """CREATE table round_score as
select deck.round, deck.player,
card1,
card2,
card3,
m1.cost as card1_cost,
m1.card_type as card1_type,
m2.cost as card2_cost,
m2.card_type as card2_type,
m3.cost as card3_cost,
m3.card_type as card3_type,
min(match.group_name) as prelim_group,
sum(case when match.group_name != 'final' then match.score else 0 end) as prelim_score,
sum(case when match.group_name = 'final' then match.score else null end) as final_score,
rank.rank,
count(*) as num_players
from deck join match
on deck.round = match.round and deck.player = match.player
join rank
on deck.round = rank.round and deck.player = rank.player
left join mtg m1 on deck.card1 = m1.name
left join mtg m2 on deck.card2 = m2.name
left join mtg m3 on deck.card3 = m3.name
group by 1,2
"""

def init_db():
    """Initialize the materialized view"""
    db = get_db()
    db.execute("drop table if exists round_score")
    db.execute(create_mview)
    db.close()

if __name__ == '__main__':
    # setup "materialized view"
    init_db()
    app.run(debug=False)