3cb-data

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.alexwennerberg.com/3cb-data.git
Log | Files | Refs | README | LICENSE

commit c4d72ba7bb45fb4d3c7517026252cd137fcca8ed
parent 1864d2082f14e064d2bd027ce86902e22e3b00fe
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Mon,  6 Oct 2025 14:31:34 -0400

python rewrite

Diffstat:
M.gitignore | 1+
DGemfile.lock | 59-----------------------------------------------------------
Aapp.py | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adeploy.sh | 3+++
Dmain.rb | 81-------------------------------------------------------------------------------
Arequirements.txt | 2++
Rpublic/style.css -> static/style.css | 0
Atemplates/banned.html | 19+++++++++++++++++++
Atemplates/footer.html | 7+++++++
Atemplates/header.html | 15+++++++++++++++
Atemplates/index.html | 21+++++++++++++++++++++
Atemplates/matches.html | 29+++++++++++++++++++++++++++++
Atemplates/players.html | 23+++++++++++++++++++++++
Dviews/card.erb | 3---
Dviews/footer.erb | 6------
Dviews/header.erb | 14--------------
Dviews/index.erb | 20--------------------
Dviews/matches.erb | 28----------------------------
Dviews/player.erb | 15---------------
Dviews/players.erb | 23-----------------------
Dviews/round.erb | 21---------------------
Dviews/submission.erb | 11-----------
22 files changed, 222 insertions(+), 281 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -6,3 +6,4 @@ allcards.txt .venv __pycache__ _site +venv/ diff --git a/Gemfile.lock b/Gemfile.lock @@ -1,59 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - base64 (0.2.0) - logger (1.6.4) - mustermann (3.0.3) - ruby2_keywords (~> 0.0.1) - nio4r (2.7.4) - puma (6.5.0) - nio4r (~> 2.0) - rack (3.1.8) - rack-protection (4.1.1) - base64 (>= 0.1.0) - logger (>= 1.6.0) - rack (>= 3.0.0, < 4) - rack-session (2.0.0) - rack (>= 3.0.0) - rackup (2.2.1) - rack (>= 3) - ruby2_keywords (0.0.5) - sinatra (4.1.1) - logger (>= 1.6.0) - mustermann (~> 3.0) - rack (>= 3.0.0, < 4) - rack-protection (= 4.1.1) - rack-session (>= 2.0.0, < 3) - tilt (~> 2.0) - sqlite3 (2.4.1-aarch64-linux-gnu) - sqlite3 (2.4.1-aarch64-linux-musl) - sqlite3 (2.4.1-arm-linux-gnu) - sqlite3 (2.4.1-arm-linux-musl) - sqlite3 (2.4.1-arm64-darwin) - sqlite3 (2.4.1-x86-linux-gnu) - sqlite3 (2.4.1-x86-linux-musl) - sqlite3 (2.4.1-x86_64-darwin) - sqlite3 (2.4.1-x86_64-linux-gnu) - sqlite3 (2.4.1-x86_64-linux-musl) - tilt (2.5.0) - -PLATFORMS - aarch64-linux-gnu - aarch64-linux-musl - arm-linux-gnu - arm-linux-musl - arm64-darwin - x86-linux-gnu - x86-linux-musl - x86_64-darwin - x86_64-linux-gnu - x86_64-linux-musl - -DEPENDENCIES - puma - rackup - sinatra - sqlite3 - -BUNDLED WITH - 2.5.9 diff --git a/app.py b/app.py @@ -0,0 +1,101 @@ +from flask import Flask, render_template, request +import sqlite3 +from urllib.parse import quote as url_quote + +app = Flask(__name__) + +def get_db(): + db = sqlite3.connect('3cb.db') + db.row_factory = sqlite3.Row + return db + +@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) + +# Template filter for url encoding +@app.template_filter('url_encode') +def url_encode_filter(s): + return url_quote(str(s)) + +if __name__ == '__main__': + app.run(debug=True) +\ No newline at end of file diff --git a/deploy.sh b/deploy.sh @@ -0,0 +1,3 @@ +scp main.rb alpine@fishbb.org:~ +scp -r views alpine@fishbb.org:~ +ssh alpine@fishbb.org "doas rc-service 3cm restart" diff --git a/main.rb b/main.rb @@ -1,81 +0,0 @@ -require 'sinatra' -require 'sqlite3' -require 'erb' -include ERB::Util - -db = SQLite3::Database.new "3cb.db" -db.results_as_hash = true -set :server_settings, :timeout => 5 - -get '/' do - @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" - @subtitle = "rounds" - erb :index -end - -get '/round/:id' do |id| - @round = db.execute("select * from round where id = ?", id)[0] - @matches = db.execute "select * from round_score where round = ? order by final_score desc,prelim_score desc", id - @banned_cards = db.execute("select name from ban").map { |row| row["name"] } - @subtitle = "round " + id - erb :matches -end - -get '/card' do - name = params[:name] - @matches = db.execute "select * from round_score where - card1 = ?1 or - card2 = ?1 or - card3 = ?1 order by round desc", name - @banned_cards = db.execute("select name from ban").map { |row| row["name"] } - @subtitle = name - erb :matches -end - -get '/player' do - name = params[:name] - @matches = db.execute "select * from round_score where player = ? order by round desc", name - @banned_cards = db.execute("select name from ban").map { |row| row["name"] } - @subtitle = "player " + name - erb :matches # rename round -> matches -end - -get '/deck' do - cards = [params[:c1], params[:c2], params[:c3]].sort - # i am lazy - @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]]) - @banned_cards = db.execute("select name from ban").map { |row| row["name"] } - @subtitle = cards.join("; ") - erb :matches -end - -get '/players' do - @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 - " - @subtitle = "players" - erb :players -end - -get '/banned' do - @banned_cards = db.execute("select name from ban order by name") - @subtitle = "banned cards" - erb :banned -end diff --git a/requirements.txt b/requirements.txt @@ -0,0 +1 @@ +Flask==2.3.3 +\ No newline at end of file diff --git a/public/style.css b/static/style.css diff --git a/templates/banned.html b/templates/banned.html @@ -0,0 +1,18 @@ +{% include 'header.html' %} + +<div class="banned-cards-grid"> +{% for card in banned_cards %} + <div class="banned-card"> + <div class="card-image"> + <img src="https://gatherer.wizards.com/Handlers/Image.ashx?name={{ card.name|url_encode }}&type=card" + alt="{{ card.name }}" + onerror="this.src='https://cards.scryfall.io/normal/front/0/0/00000000-0000-0000-0000-000000000000.jpg'"> + </div> + <div class="card-name"> + <a href="/card?name={{ card.name|url_encode }}">{{ card.name }}</a> + </div> + </div> +{% endfor %} +</div> + +{% include 'footer.html' %} +\ No newline at end of file diff --git a/templates/footer.html b/templates/footer.html @@ -0,0 +1,6 @@ +<footer> + Made by aw for <a href="https://sites.google.com/view/3cb-metashape/home">3 Card Blind Metashape</a>. <a href="https://git.sr.ht/~aw/3cb-data">Source</a> +</footer> +</main> +</body> +</html> +\ No newline at end of file diff --git a/templates/header.html b/templates/header.html @@ -0,0 +1,14 @@ +<html> +<html dir="ltr" lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1" /> +<meta name="description" content="Data analysis for 3 card blind metashape" /> +<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}"> +<title>3 card blind data analysis</title> +</head> +<body> + <header> + <b><a href="/">3cb-data</a></b> &gt; <b>{{ subtitle }}</b> + </header> +<main> +\ No newline at end of file diff --git a/templates/index.html b/templates/index.html @@ -0,0 +1,20 @@ +{% include 'header.html' %} +<p><a href="/players">View all players</a> | <a href="/banned">View banned cards</a></p> +<form action="/card" method="get" class="card-search"> + <input type="text" name="name" placeholder="Search for a card..." required> + <input type="submit" value="Search"> +</form> +<div class="rounds-list"> +{% for round in rounds %} + <div class="round-item"> + <a href="/round/{{ round.id }}">Round {{ "%02d"|format(round.id) }}</a> + <span class="round-meta"> + {% if round.date %} + {{ round.date }} + {% endif %} +[{{ round.player_count }} players] + </span> + </div> +{% endfor %} +</div> +{% include 'footer.html' %} +\ No newline at end of file diff --git a/templates/matches.html b/templates/matches.html @@ -0,0 +1,28 @@ +{% include 'header.html' %} + {% if round %} + <a href='https://docs.google.com/spreadsheets/d/{{ round.fileid }}/'>Google Sheet</a>{% endif %} +{% if not matches %} + <p>This card has never been played, or may be spelled wrong!</p> +{% else %} +<div class="flextable"> + {% for match in matches %} + <div class="deck-box" + {% if match.rank == 1 %} style="background:#c5ffc5" {% elif match.final_score %} style="background:#ffffc5"{% endif %}> + <div style="width:160px"> + <b><a href='/player?name={{ match.player }}'>{{ match.player }}</a></b><br> + <a href="/round/{{ match.round }}">Round {{ match.round }}</a><br> + Rank: &nbsp;&nbsp;{{ match.rank }}<br> + Groups{{ match.group_name }}: {{ match.prelim_score }} +{% if match.final_score %}<br> + Finals: {{ match.final_score }} +{% endif %}<br> + <a href="/deck?c1={{ match.card1 }}&c2={{ match.card2 }}&c3={{ match.card3 }}">deck</a><br> +</div> +<div> + {% for card in [match.card1, match.card2, match.card3] %}<div style="position: relative; display: inline-block;"><a href="/card?name={{ card }}"><img alt='{{ card }}' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact={{ card|url_encode }}&format=image&version=medium" /></a>{% if card in banned_cards %}<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.7); color: white; font-weight: bold; font-size: 20px; padding: 5px 10px; border-radius: 5px;">BANNED</div>{% endif %}</div>{% endfor %} +</div> +</div> +{% endfor %} +</div> +{% endif %} +{% include 'footer.html' %} +\ No newline at end of file diff --git a/templates/players.html b/templates/players.html @@ -0,0 +1,22 @@ +{% include 'header.html' %} +<table class="players-table"> + <thead> + <tr> + <th>Player</th> + <th>Rounds</th> + <th>Wins</th> + <th>First Played</th> + </tr> + </thead> + <tbody> + {% for player in players %} + <tr> + <td><a href="/player?name={{ player.player|url_encode }}">{{ player.player }}</a></td> + <td>{{ player.rounds_played }}</td> + <td>{{ player.rounds_won }}</td> + <td>{{ player.first_played }}</td> + </tr> + {% endfor %} + </tbody> +</table> +{% include 'footer.html' %} +\ No newline at end of file diff --git a/views/card.erb b/views/card.erb @@ -1,3 +0,0 @@ -<%= erb :header %> -<%= erb :footer%> - diff --git a/views/footer.erb b/views/footer.erb @@ -1,6 +0,0 @@ -<footer> - Made by aw for <a href="https://sites.google.com/view/3cb-metashape/home">3 Card Blind Metashape</a>. <a href="https://git.sr.ht/~aw/3cb-data">Source</a> -</footer> -</main> -</body> -</html> diff --git a/views/header.erb b/views/header.erb @@ -1,14 +0,0 @@ -<html> -<html dir="ltr" lang="en"> -<head> -<meta charset="utf-8"> -<meta name="viewport" content="width=device-width, initial-scale=1" /> -<meta name="description" content="Data analysis for 3 card blind metashape" /> -<link rel="stylesheet" type="text/css" href="/style.css"> -<title>3 card blind data analysis</title> -</head> -<body> - <header> - <b><a href="/">3cb-data</a></b> &gt; <b><%= @subtitle %></b> - </header> -<main> diff --git a/views/index.erb b/views/index.erb @@ -1,20 +0,0 @@ -<%= erb :header %> -<p><a href="/players">View all players</a> | <a href="/banned">View banned cards</a></p> -<form action="/card" method="get" class="card-search"> - <input type="text" name="name" placeholder="Search for a card..." required> - <input type="submit" value="Search"> -</form> -<div class="rounds-list"> -<% @rounds.each do |round| %> - <div class="round-item"> - <a href="/round/<%= round["id"] %>">Round <%= round["id"].to_s.rjust(2, "0") %></a> - <span class="round-meta"> - <% if round["date"] %> - <%= round["date"] %> - <% end %> -[<%= round["player_count"] %> players] - </span> - </div> -<% end %> -</div> -<%= erb :footer %> diff --git a/views/matches.erb b/views/matches.erb @@ -1,28 +0,0 @@ -<%= erb :header %> - <% if @round %> - <a href='https://docs.google.com/spreadsheets/d/<%= @round["fileid"] %>/'>Google Sheet</a><% end %> -<% if @matches.empty? %> - <p>This card has never been played, or may be spelled wrong!</p> -<% else %> -<div class="flextable"> - <% @matches.each do |match| %> - <div class="deck-box" - <% if match["rank"] == 1 %> style="background:#c5ffc5" <% elsif match["final_score"] %> style="background:#ffffc5"<% end %>> - <div style="width:160px"> - <b><a href='/player?name=<%= match["player"] %>'><%= match["player"] %></a></b><br> - <a href="/round/<%= match["round"]%>">Round <%= match["round"] %></a><br> - Rank: &nbsp;&nbsp;<%= match["rank"] %><br> - Groups<%= match["group_name"] %>: <%= match["prelim_score"] %> -<% if match["final_score"] %><br> - Finals: <%= match["final_score"] %> -<% end %><br> - <a href="/deck?c1=<%= match["card1"] %>&c2=<%= match["card2"] %>&c3=<%= match["card3"]%>">deck</a><br> -</div> -<div> - <% [match["card1"], match["card2"], match["card3"]].each do |card| %><div style="position: relative; display: inline-block;"><a href="/card?name=<%= card %>"><img alt='<%= card %>' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= url_encode(card) %>&format=image&version=medium" /></a><% if @banned_cards.include?(card) %><div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.7); color: white; font-weight: bold; font-size: 20px; padding: 5px 10px; border-radius: 5px;">BANNED</div><% end %></div><% end %> -</div> -</div> -<% end %> -</div> -<% end %> -<%= erb :footer %> diff --git a/views/player.erb b/views/player.erb @@ -1,15 +0,0 @@ -<%= erb :header %> -Submissions: -<table> -<% @matches.each do |match| %> - <tr> - <td><%= match["round"] %></td> - <td><%= match["prelim_score"] %></td> - <td><%= match["final_score"] %></td> - <td><%= match["card1"] %></td> - <td><%= match["card2"] %></td> - <td><%= match["card3"] %></td> - </tr> -<% end %> -</table> -<%= erb :footer%> diff --git a/views/players.erb b/views/players.erb @@ -1,22 +0,0 @@ -<%= erb :header %> -<table class="players-table"> - <thead> - <tr> - <th>Player</th> - <th>Rounds</th> - <th>Wins</th> - <th>First Played</th> - </tr> - </thead> - <tbody> - <% @players.each do |player| %> - <tr> - <td><a href="/player?name=<%= url_encode(player["player"]) %>"><%= player["player"] %></a></td> - <td><%= player["rounds_played"] %></td> - <td><%= player["rounds_won"] %></td> - <td><%= player["first_played"] %></td> - </tr> - <% end %> - </tbody> -</table> -<%= erb :footer %> -\ No newline at end of file diff --git a/views/round.erb b/views/round.erb @@ -1,21 +0,0 @@ -<%= erb :header %> - <% if @round %> - <a href='https://docs.google.com/spreadsheets/d/<%= @round["fileid"] %>/'>Google Sheet</a><% end %> -<div class="flextable"> - <% @matches.each do |match| %> - <div class="deck-box" - <% if match["final_score"] %> style="background:#ffffc5"<% end %>> - <div style="width:160px"> - <b><a href='/player/<%= match["player"] %>'><%= match["player"] %></a></b><br> - <a href="/round/<%= match["round"]%>"><%= match["round"] %></a> - <a href="/deck?card=<%= match["card1"] %>&card=<%= match["card2"] %>card= <%= match["card3"]%>>deck</a><br> - Group <%= match["prelim_group"] %>: <%=match["prelim_score"]%><br> - <% if match["final_score"] %>Finals: &nbsp;<%= match["final_score"] %><% end %> -</div> -<div> - <% [match["card1"], match["card2"], match["card3"]].each do |card| %><img alt='<%= card %>' width=146height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= card %>&format=image&version=small" /><% end %> -</div> -</div> -<% end %> -</div> -<%= erb :footer %> diff --git a/views/submission.erb b/views/submission.erb @@ -1,11 +0,0 @@ -<div class="deck-match"> - <div> - playername<br> - Group A: 43<br> - Finals: 10<br> - </div> - <div> - card123 - deck link - </div> -</div>