Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Format Python with black before push (matches CI: . --exclude '/migrations/').
# If black rewrites files, abort so the changes can be committed first.
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"

run_black() {
if command -v pipenv >/dev/null 2>&1 && [[ -f Pipfile ]]; then
pipenv run black "$@"
elif command -v black >/dev/null 2>&1; then
black "$@"
else
echo "pre-push: black not found (install black, or use pipenv with black available)" >&2
exit 1
fi
}

echo "pre-push: running black..."
run_black . --exclude '/migrations/'

if ! git diff --quiet -- '*.py'; then
echo "" >&2
echo "pre-push: black reformatted Python files. Commit them and push again:" >&2
git --no-pager diff --stat -- '*.py' >&2
exit 1
fi

echo "pre-push: black ok"
4 changes: 3 additions & 1 deletion apps/blog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ def send_blogpost_webhook(self):
# Only format username as a link if the user is authenticated
if self.user and self.user.is_authenticated:
profile_path = reverse("user-profile", args=[self.get_display_name()])
username_display = f"[{self.get_display_name()}](<{settings.SITE_URL}{profile_path}>)"
username_display = (
f"[{self.get_display_name()}](<{settings.SITE_URL}{profile_path}>)"
)
else:
username_display = f'"{self.get_display_name()}"'

Expand Down
4 changes: 3 additions & 1 deletion apps/blog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,9 @@ def item_categories(self, item):

# "Enclosure" stuff so we can use the preview images in the rss feeds
def item_enclosure_url(self, item):
if getattr(item, "featured_image", None) and getattr(item.featured_image, "url", None):
if getattr(item, "featured_image", None) and getattr(
item.featured_image, "url", None
):
return self._absolute_url(item.featured_image.url)
return None

Expand Down
1 change: 0 additions & 1 deletion apps/bookclub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from django.db import models
from django.contrib.auth import get_user_model


User = get_user_model()
logger = logging.getLogger(__name__)

Expand Down
6 changes: 2 additions & 4 deletions apps/bot_manager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ def get_tables():
"""Get list of tables in Fops database"""
with get_fops_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
cur.execute("""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blackfmt] reported by reviewdog 🐶

Suggested change
cur.execute("""
cur.execute(
"""

SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
"""
)
""")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blackfmt] reported by reviewdog 🐶

Suggested change
""")
"""
)

return [row["table_name"] for row in cur.fetchall()]


Expand Down
12 changes: 6 additions & 6 deletions apps/tanks_manager/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,12 @@ def _editor_data(site):
"tankBottomOffset": site.tank_bottom_offset,
"character_name": site.character_name,
"character_url": site.character_url,
"stage_background_url": site.stage_background.url
if site.stage_background
else "",
"stage_foreground_url": site.stage_foreground.url
if site.stage_foreground
else "",
"stage_background_url": (
site.stage_background.url if site.stage_background else ""
),
"stage_foreground_url": (
site.stage_foreground.url if site.stage_foreground else ""
),
},
"logs": [
{"date": g.date, "text": g.text}
Expand Down
75 changes: 72 additions & 3 deletions apps/vore_day_rumble/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,32 @@ class Meta:
def __str__(self):
return self.display_name

def prey(self):
"""
Full stack of previous prey!! >:3 Counts if you get double-ate too!
"""
latest = (
self.matches_won.select_related("contestant_a", "contestant_b")
.order_by("-round_number", "-position")
.first()
)
if not latest:
return []

if latest.winner_id == latest.contestant_a_id:
loser = latest.contestant_b
my_side, loser_side = "a", "b"
else:
loser = latest.contestant_a
my_side, loser_side = "b", "a"

out = []
if loser:
out.append(loser)
out.extend(latest.previous_prey(loser_side))
out.extend(latest.previous_prey(my_side))
return out


class Match(models.Model):
"""One matchup in a round. Round 1 gets rebuilt whenever someone signs up."""
Expand Down Expand Up @@ -168,6 +194,51 @@ def live_vote_counts(self):
)
return agg["left"] or 0, agg["right"] or 0

def previous_prey(self, side):
"""
Contestants this side already ate on the way here.
Beating someone also absorbs their prey stack.
Newest elimination first.
side: "a" or "b"
"""
if side == "a":
contestant = self.contestant_a
pos = self.position * 2
elif side == "b":
contestant = self.contestant_b
pos = self.position * 2 + 1
else:
raise ValueError(f"side must be 'a' or 'b', got {side!r}")

if not contestant or self.round_number <= 1:
return []

out = []
for r in range(self.round_number - 1, 0, -1):
feeder = (
Match.objects.select_related("contestant_a", "contestant_b", "winner")
.filter(round_number=r, position=pos)
.first()
)
if not feeder or feeder.winner_id != contestant.id:
break

if feeder.winner_id == feeder.contestant_a_id:
loser = feeder.contestant_b
loser_side = "b"
pos = feeder.position * 2
else:
loser = feeder.contestant_a
loser_side = "a"
pos = feeder.position * 2 + 1

if loser:
out.append(loser)
# Inherit everyone the loser had already eaten
out.extend(feeder.previous_prey(loser_side))

return out


class MatchVote(models.Model):
"""One anonymous site vote per snowsune account, per match."""
Expand All @@ -179,9 +250,7 @@ class MatchVote(models.Model):
(CHOICE_B, "Contestant B"),
]

match = models.ForeignKey(
Match, on_delete=models.CASCADE, related_name="votes"
)
match = models.ForeignKey(Match, on_delete=models.CASCADE, related_name="votes")
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
Expand Down
21 changes: 20 additions & 1 deletion apps/vore_day_rumble/static/vore_day_rumble/css/rumble.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,32 @@
text-align: center;
}

#rumble-bracket {
.rumble-bracket-scroll {
width: 100%;
max-width: 100%;
margin: 0.5rem 0 1rem;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
touch-action: pan-x pan-y;
}

#rumble-bracket {
width: 100%;
background: transparent;
color: var(--text-color);
}

@media (max-width: 768px) {
.rumble-bracket-scroll {
/* Full-bleed within the content column so swipes aren't cramped */
margin-left: -0.5rem;
margin-right: -0.5rem;
padding: 0 0.5rem 0.35rem;
}
}

#rumble-bracket:empty {
display: flex;
align-items: center;
Expand Down
10 changes: 10 additions & 0 deletions apps/vore_day_rumble/static/vore_day_rumble/js/bracket.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ if (dataEl && wrapper) {
const sidebar = css.getPropertyValue("--sidebar-bg").trim() || "#eee";

const pfpSize = 44;
const matchMaxWidth = 220;
const matchHorMargin = 16;
const roundCount = data.rounds.length;
// Keep rounds readable on narrow screens; parent .rumble-bracket-scroll pans horizontally
const minBracketWidth = roundCount * (matchMaxWidth + matchHorMargin * 2);
if (window.matchMedia("(max-width: 768px)").matches) {
wrapper.style.minWidth = `${minBracketWidth}px`;
}

createBracket(data, wrapper, {
width: "100%",
Expand All @@ -26,6 +34,8 @@ if (dataEl && wrapper) {
verticalScrollMode: "native",
roundTitlesFontSize: 0,
roundTitlesVerticalPadding: 0,
matchMaxWidth,
matchHorMargin,
rootBgColor: tile,
rootBorderColor: "transparent",
wrapperBorderColor: "transparent",
Expand Down
65 changes: 27 additions & 38 deletions apps/vore_day_rumble/static/vore_day_rumble/js/voting.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
if (!root) return;

const voteUrl = root.dataset.voteUrl;
const statusUrl = root.dataset.statusUrl;
const streamUrl = root.dataset.streamUrl;
const loggedIn = root.dataset.loggedIn === "1";
const csrfInput = root.querySelector("[name=csrfmiddlewaretoken]");
const csrf = csrfInput ? csrfInput.value : "";
const csrf = root.querySelector("[name=csrfmiddlewaretoken]")?.value || "";

const countdownEl = document.getElementById("rumble-vote-countdown");
const hintEl = document.getElementById("rumble-vote-hint");
Expand All @@ -16,18 +15,19 @@

let endsAtUnix = null;
let clockSkewMs = 0;
let pollTimer = null;
let countdownTimer = null;
let source = null;

function applyStatus(data) {
if (!data || data.voting_open === false) {
if (countdownEl) countdownEl.textContent = "Voting closed! Refresh for the next match!";
if (countdownEl) {
countdownEl.textContent = "Voting closed! Refresh for the next match!";
}
buttons.forEach((btn) => {
btn.disabled = true;
});
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
if (source) {
source.close();
source = null;
}
return;
}
Expand Down Expand Up @@ -64,16 +64,15 @@
const s = seconds % 60;
if (m >= 60) {
const h = Math.floor(m / 60);
const rm = m % 60;
return `Closes in ${h}h ${rm}m`;
return `Closes in ${h}h ${m % 60}m`;
}
return `Closes in ${m}:${String(s).padStart(2, "0")}`;
}

function tickCountdown() {
if (!countdownEl || endsAtUnix == null) return;
const nowUnix = Math.floor((Date.now() - clockSkewMs) / 1000);
const remaining = endsAtUnix - nowUnix;
const remaining =
endsAtUnix - Math.floor((Date.now() - clockSkewMs) / 1000);
countdownEl.textContent = formatRemaining(remaining);
if (remaining <= 0) {
buttons.forEach((btn) => {
Expand All @@ -82,20 +81,15 @@
}
}

async function pollStatus() {
try {
const resp = await fetch(`${statusUrl}?_=${Date.now()}`, {
headers: { Accept: "application/json" },
credentials: "same-origin",
cache: "no-store",
});
if (!resp.ok) return;
const data = await resp.json();
applyStatus(data);
tickCountdown();
} catch (_) {
/* ignore transient poll errors */
}
function startStream() {
if (!streamUrl || typeof EventSource === "undefined") return;
source = new EventSource(streamUrl);
source.addEventListener("status", (event) => {
try {
applyStatus(JSON.parse(event.data));
tickCountdown();
} catch (_) {}
});
}

async function castVote(choice) {
Expand All @@ -104,8 +98,7 @@
btn.disabled = true;
});
try {
const body = new URLSearchParams();
body.set("choice", choice);
const body = new URLSearchParams({ choice });
const resp = await fetch(voteUrl, {
method: "POST",
headers: {
Expand All @@ -115,13 +108,11 @@
},
credentials: "same-origin",
cache: "no-store",
body: body.toString(),
body,
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) {
if (hintEl) {
hintEl.textContent = data.error || "Could not cast vote :<";
}
if (hintEl) hintEl.textContent = data.error || "Could not cast vote :<";
buttons.forEach((btn) => {
btn.disabled = false;
});
Expand All @@ -145,12 +136,10 @@
if (boot) {
try {
applyStatus(JSON.parse(boot.textContent));
} catch (_) {
/* ignore bad boot JSON */
}
} catch (_) {}
}

tickCountdown();
countdownTimer = setInterval(tickCountdown, 1000);
pollTimer = setInterval(pollStatus, 4000);
setInterval(tickCountdown, 1000);
startStream();
})();
6 changes: 4 additions & 2 deletions apps/vore_day_rumble/templates/vore_day_rumble/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ <h1>Vixi's Vore Day Rumble</h1>
class="rumble-now-voting"
id="rumble-now-voting"
data-vote-url="{% url 'vore_day_rumble:cast_vote' %}"
data-status-url="{% url 'vore_day_rumble:vote_status' %}"
data-stream-url="{% url 'vore_day_rumble:vote_stream' %}"
data-logged-in="{% if request.user.is_authenticated %}1{% else %}0{% endif %}"
>
{% csrf_token %}
Expand Down Expand Up @@ -122,6 +122,8 @@ <h2>Now voting</h2>
{% endif %}

<h2>Nom-nom Bracket</h2>
<div id="rumble-bracket"></div>
<div class="rumble-bracket-scroll">
<div id="rumble-bracket"></div>
</div>
</section>
{% endblock %}
Loading
Loading