init CTFd source
Some checks are pending
Linting / Linting (3.11) (push) Waiting to run
Mirror core-theme / mirror (push) Waiting to run

This commit is contained in:
gkr
2025-12-25 09:39:21 +08:00
commit 2e06f92c64
1047 changed files with 150349 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
from __future__ import print_function
import os
import sys
import dataset
from sqlalchemy_utils import drop_database
from CTFd import config, create_app
from CTFd.utils import string_types
# This is important to allow access to the CTFd application factory
sys.path.append(os.getcwd())
def cast_bool(value):
if value and value.isdigit():
return int(value)
elif value and isinstance(value, string_types):
if value.lower() == "true":
return True
elif value.lower() == "false":
return False
else:
return value
if __name__ == "__main__":
print("/*\\ Migrating your database to 2.0.0 can potentially lose data./*\\")
print(
"""/*\\ Please be sure to back up all data by:
* creating a CTFd export
* creating a dump of your actual database
* and backing up the CTFd source code directory"""
)
print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\")
if input("Run database migrations (Y/N)").lower().strip() == "y":
pass
else:
print("/*\\ Aborting database migrations... /*\\")
print("/*\\ Exiting... /*\\")
exit(1)
db_url = config.Config.SQLALCHEMY_DATABASE_URI
old_data = {}
old_conn = dataset.connect(config.Config.SQLALCHEMY_DATABASE_URI)
tables = old_conn.tables
for table in tables:
old_data[table] = old_conn[table].all()
if "alembic_version" in old_data:
old_data.pop("alembic_version")
print("Current Tables:")
for table in old_data.keys():
print("\t", table)
old_conn.executable.close()
print("DROPPING DATABASE")
drop_database(db_url)
app = create_app()
new_conn = dataset.connect(config.Config.SQLALCHEMY_DATABASE_URI)
print("MIGRATING Challenges")
for challenge in old_data["challenges"]:
hidden = challenge.pop("hidden")
challenge["state"] = "hidden" if hidden else "visible"
new_conn["challenges"].insert(dict(challenge))
del old_data["challenges"]
print("MIGRATING Teams")
for team in old_data["teams"]:
admin = team.pop("admin")
team["type"] = "admin" if admin else "user"
team["hidden"] = bool(team.pop("banned"))
team["banned"] = False
team["verified"] = bool(team.pop("verified"))
new_conn["users"].insert(dict(team))
del old_data["teams"]
print("MIGRATING Pages")
for page in old_data["pages"]:
page["content"] = page.pop("html")
new_conn["pages"].insert(dict(page))
del old_data["pages"]
print("MIGRATING Keys")
for key in old_data["keys"]:
key["challenge_id"] = key.pop("chal")
key["content"] = key.pop("flag")
new_conn["flags"].insert(dict(key))
del old_data["keys"]
print("MIGRATING Tags")
for tag in old_data["tags"]:
tag["challenge_id"] = tag.pop("chal")
tag["value"] = tag.pop("tag")
new_conn["tags"].insert(dict(tag))
del old_data["tags"]
print("MIGRATING Files")
for f in old_data["files"]:
challenge_id = f.pop("chal")
if challenge_id:
f["challenge_id"] = challenge_id
f["type"] = "challenge"
else:
f["page_id"] = None
f["type"] = "page"
new_conn["files"].insert(dict(f))
del old_data["files"]
print("MIGRATING Hints")
for hint in old_data["hints"]:
hint["type"] = "standard"
hint["challenge_id"] = hint.pop("chal")
hint["content"] = hint.pop("hint")
new_conn["hints"].insert(dict(hint))
del old_data["hints"]
print("MIGRATING Unlocks")
for unlock in old_data["unlocks"]:
unlock["user_id"] = unlock.pop(
"teamid"
) # This is intentional as previous CTFds are effectively in user mode
unlock["target"] = unlock.pop("itemid")
unlock["type"] = unlock.pop("model")
new_conn["unlocks"].insert(dict(unlock))
del old_data["unlocks"]
print("MIGRATING Awards")
for award in old_data["awards"]:
award["user_id"] = award.pop(
"teamid"
) # This is intentional as previous CTFds are effectively in user mode
new_conn["awards"].insert(dict(award))
del old_data["awards"]
submissions = []
for solve in old_data["solves"]:
solve.pop("id") # ID of a solve doesn't really matter
solve["challenge_id"] = solve.pop("chalid")
solve["user_id"] = solve.pop("teamid")
solve["provided"] = solve.pop("flag")
solve["type"] = "correct"
solve["model"] = "solve"
submissions.append(solve)
for wrong_key in old_data["wrong_keys"]:
wrong_key.pop("id") # ID of a fail doesn't really matter.
wrong_key["challenge_id"] = wrong_key.pop("chalid")
wrong_key["user_id"] = wrong_key.pop("teamid")
wrong_key["provided"] = wrong_key.pop("flag")
wrong_key["type"] = "incorrect"
wrong_key["model"] = "wrong_key"
submissions.append(wrong_key)
submissions = sorted(submissions, key=lambda k: k["date"])
print("MIGRATING Solves & WrongKeys")
for submission in submissions:
model = submission.pop("model")
if model == "solve":
new_id = new_conn["submissions"].insert(dict(submission))
submission["id"] = new_id
new_conn["solves"].insert(dict(submission))
elif model == "wrong_key":
new_conn["submissions"].insert(dict(submission))
del old_data["solves"]
del old_data["wrong_keys"]
print("MIGRATING Tracking")
for tracking in old_data["tracking"]:
tracking["user_id"] = tracking.pop("team")
new_conn["tracking"].insert(dict(tracking))
del old_data["tracking"]
print("MIGRATING Config")
banned = ["ctf_version"]
workshop_mode = None
hide_scores = None
prevent_registration = None
view_challenges_unregistered = None
view_scoreboard_if_authed = None
challenge_visibility = "private"
registration_visibility = "public"
score_visibility = "public"
account_visibility = "public"
for c in old_data["config"]:
c.pop("id")
if c["key"] == "workshop_mode":
workshop_mode = cast_bool(c["value"])
elif c["key"] == "hide_scores":
hide_scores = cast_bool(c["value"])
elif c["key"] == "prevent_registration":
prevent_registration = cast_bool(c["value"])
elif c["key"] == "view_challenges_unregistered":
view_challenges_unregistered = cast_bool(c["value"])
elif c["key"] == "view_scoreboard_if_authed":
view_scoreboard_if_authed = cast_bool(c["value"])
if c["key"] not in banned:
new_conn["config"].insert(dict(c))
if workshop_mode:
score_visibility = "admins"
account_visibility = "admins"
if hide_scores:
score_visibility = "hidden"
if prevent_registration:
registration_visibility = "private"
if view_challenges_unregistered:
challenge_visibility = "public"
if view_scoreboard_if_authed:
score_visibility = "private"
new_conn["config"].insert({"key": "user_mode", "value": "users"})
new_conn["config"].insert(
{"key": "challenge_visibility", "value": challenge_visibility}
)
new_conn["config"].insert(
{"key": "registration_visibility", "value": registration_visibility}
)
new_conn["config"].insert({"key": "score_visibility", "value": score_visibility})
new_conn["config"].insert(
{"key": "account_visibility", "value": account_visibility}
)
del old_data["config"]
manual = []
not_created = []
print("MIGRATING extra tables")
for table in old_data.keys():
print("MIGRATING", table)
new_conn.create_table(table, primary_id=False)
data = old_data[table]
ran = False
for row in data:
new_conn[table].insert(dict(row))
ran = True
else: # We finished inserting
if ran:
manual.append(table)
if ran is False:
not_created.append(table)
print("Migration completed.")
print(
"The following tables require manual setting of primary keys and manual inspection"
)
for table in manual:
print("\t", table)
print(
"For example you can use the following commands if you know that the PRIMARY KEY for the table is `id`:"
)
for table in manual:
print("\t", "ALTER TABLE `{table}` ADD PRIMARY KEY(id)".format(table=table))
print(
"The following tables were not created because they were empty and must be manually recreated (e.g. app.db.create_all()"
)
for table in not_created:
print("\t", table)

45
migrations/alembic.ini Normal file
View File

@@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

96
migrations/env.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import with_statement # noqa: I001
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name, disable_existing_loggers=False)
logger = logging.getLogger("alembic.env")
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
"sqlalchemy.url",
str(current_app.extensions["migrate"].db.engine.url).replace("%", "%%"),
)
target_metadata = current_app.extensions["migrate"].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info("No changes in schema detected.")
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions["migrate"].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,47 @@
"""Add table for comments
Revision ID: 0366ba6575ca
Revises: 1093835a1051
Create Date: 2020-08-14 00:46:54.161120
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "0366ba6575ca"
down_revision = "1093835a1051"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"comments",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.Column("author_id", sa.Integer(), nullable=True),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.Column("page_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["author_id"], ["users.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["page_id"], ["pages.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("comments")
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""Add format to Pages
Revision ID: 07dfbe5e1edc
Revises: 75e8ab9a0014
Create Date: 2021-06-15 19:57:37.410152
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "07dfbe5e1edc"
down_revision = "75e8ab9a0014"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("pages", sa.Column("format", sa.String(length=80), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("pages", "format")
# ### end Alembic commands ###

View File

@@ -0,0 +1,34 @@
"""Add Tokens table to store user access tokens
Revision ID: 080d29b15cd3
Revises: b295b033364d
Create Date: 2019-11-03 18:21:04.827015
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "080d29b15cd3"
down_revision = "b295b033364d"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"tokens",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=32), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=True),
sa.Column("expiration", sa.DateTime(), nullable=True),
sa.Column("value", sa.String(length=128), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("value"),
)
def downgrade():
op.drop_table("tokens")

View File

@@ -0,0 +1,23 @@
"""Add language column to Users table
Revision ID: 0def790057c1
Revises: 46a278193a94
Create Date: 2023-04-19 00:56:54.592584
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "0def790057c1"
down_revision = "46a278193a94"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("users", sa.Column("language", sa.String(length=32), nullable=True))
def downgrade():
op.drop_column("users", "language")

View File

@@ -0,0 +1,70 @@
"""Add default email templates
Revision ID: 1093835a1051
Revises: a03403986a32
Create Date: 2020-02-15 01:32:10.959373
"""
from alembic import op
from sqlalchemy.sql import column, table
from CTFd.models import db
from CTFd.utils.email import (
DEFAULT_PASSWORD_RESET_BODY,
DEFAULT_PASSWORD_RESET_SUBJECT,
DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_BODY,
DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_SUBJECT,
DEFAULT_USER_CREATION_EMAIL_BODY,
DEFAULT_USER_CREATION_EMAIL_SUBJECT,
DEFAULT_VERIFICATION_EMAIL_BODY,
DEFAULT_VERIFICATION_EMAIL_SUBJECT,
)
# revision identifiers, used by Alembic.
revision = "1093835a1051"
down_revision = "a03403986a32"
branch_labels = None
depends_on = None
configs_table = table(
"config", column("id", db.Integer), column("key", db.Text), column("value", db.Text)
)
def get_config(key):
connection = op.get_bind()
return connection.execute(
configs_table.select().where(configs_table.c.key == key).limit(1)
).fetchone()
def set_config(key, value):
connection = op.get_bind()
connection.execute(configs_table.insert().values(key=key, value=value))
def upgrade():
# Only run if this instance already been setup before
if bool(get_config("setup")) is True:
for k, v in [
("password_reset_body", DEFAULT_PASSWORD_RESET_BODY),
("password_reset_subject", DEFAULT_PASSWORD_RESET_SUBJECT),
(
"successful_registration_email_body",
DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_BODY,
),
(
"successful_registration_email_subject",
DEFAULT_SUCCESSFUL_REGISTRATION_EMAIL_SUBJECT,
),
("user_creation_email_body", DEFAULT_USER_CREATION_EMAIL_BODY),
("user_creation_email_subject", DEFAULT_USER_CREATION_EMAIL_SUBJECT),
("verification_email_body", DEFAULT_VERIFICATION_EMAIL_BODY),
("verification_email_subject", DEFAULT_VERIFICATION_EMAIL_SUBJECT),
]:
if get_config(k) is None:
set_config(k, v)
def downgrade():
pass

View File

@@ -0,0 +1,45 @@
"""Convert rating values to votes
Revision ID: 24ad6790bc3c
Revises: 55623b100da8
Create Date: 2025-09-03 05:19:54.151054
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "24ad6790bc3c"
down_revision = "55623b100da8"
branch_labels = None
depends_on = None
def upgrade():
# Convert 5-star ratings to upvote/downvote system
# Values 1-2 become -1 (downvote)
# Values 3-5 become 1 (upvote)
connection = op.get_bind()
# Update ratings with values 1 or 2 to -1 (downvote)
connection.execute(sa.text("UPDATE ratings SET value = -1 WHERE value IN (1, 2)"))
# Update ratings with values 3, 4, or 5 to 1 (upvote)
connection.execute(sa.text("UPDATE ratings SET value = 1 WHERE value IN (3, 4, 5)"))
def downgrade():
# This downgrade is not reversible since we lose the original 5-star granularity
# We can only convert back to a simplified 5-star system
# -1 (downvote) becomes 1 (1 star)
# 1 (upvote) becomes 5 (5 stars)
connection = op.get_bind()
# Convert downvotes (-1) to 1-star ratings
connection.execute(sa.text("UPDATE ratings SET value = 1 WHERE value = -1"))
# Convert upvotes (1) to 5-star ratings
connection.execute(sa.text("UPDATE ratings SET value = 5 WHERE value = 1"))

View File

@@ -0,0 +1,42 @@
"""Add ratings table
Revision ID: 364b4efa1686
Revises: 662d728ad7da
Create Date: 2025-08-17 07:07:41.484323
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "364b4efa1686"
down_revision = "662d728ad7da"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"ratings",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("value", sa.Integer(), nullable=True),
sa.Column("review", sa.String(length=2000), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "challenge_id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("ratings")
# ### end Alembic commands ###

View File

@@ -0,0 +1,46 @@
"""Enable millisecond precision in MySQL datetime
Revision ID: 46a278193a94
Revises: 4d3c1b59d011
Create Date: 2022-11-01 23:27:44.620893
"""
from alembic import op # noqa: I001
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = "46a278193a94"
down_revision = "4d3c1b59d011"
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
url = str(bind.engine.url)
if url.startswith("mysql"):
get_columns = "SELECT `TABLE_NAME`, `COLUMN_NAME` FROM `information_schema`.`COLUMNS` WHERE `table_schema`=DATABASE() AND `DATA_TYPE`='datetime' AND `COLUMN_TYPE`='datetime';"
conn = op.get_bind()
columns = conn.execute(get_columns).fetchall()
for table_name, column_name in columns:
op.alter_column(
table_name=table_name,
column_name=column_name,
type_=mysql.DATETIME(fsp=6),
)
def downgrade():
bind = op.get_bind()
url = str(bind.engine.url)
if url.startswith("mysql"):
get_columns = "SELECT `TABLE_NAME`, `COLUMN_NAME` FROM `information_schema`.`COLUMNS` WHERE `table_schema`=DATABASE() AND `DATA_TYPE`='datetime' AND `COLUMN_TYPE`='datetime(6)';"
conn = op.get_bind()
columns = conn.execute(get_columns).fetchall()
for table_name, column_name in columns:
op.alter_column(
table_name=table_name,
column_name=column_name,
type_=mysql.DATETIME(fsp=0),
)

View File

@@ -0,0 +1,32 @@
"""Add next_id to Challenges table
Revision ID: 4d3c1b59d011
Revises: 6012fe8de495
Create Date: 2022-04-07 03:53:27.554190
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4d3c1b59d011"
down_revision = "6012fe8de495"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("challenges", sa.Column("next_id", sa.Integer(), nullable=True))
op.create_foreign_key(
None, "challenges", "challenges", ["next_id"], ["id"], ondelete="SET NULL"
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "challenges", type_="foreignkey")
op.drop_column("challenges", "next_id")
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Add type to awards
Revision ID: 4e4d5a9ea000
Revises: 8369118943a1
Create Date: 2019-04-07 19:37:17.872128
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "4e4d5a9ea000"
down_revision = "8369118943a1"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"awards",
sa.Column(
"type", sa.String(length=80), nullable=True, server_default="standard"
),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("awards", "type")
# ### end Alembic commands ###

View File

@@ -0,0 +1,24 @@
"""Add attribution to Challenges
Revision ID: 4fe3eeed9a9d
Revises: a02c5bf43407
Create Date: 2024-09-07 01:02:28.997761
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4fe3eeed9a9d"
down_revision = "a02c5bf43407"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("challenges", sa.Column("attribution", sa.Text(), nullable=True))
def downgrade():
op.drop_column("challenges", "attribution")

View File

@@ -0,0 +1,28 @@
"""Add target column to Tracking
Revision ID: 55623b100da8
Revises: 5c98d9253f56
Create Date: 2025-08-26 23:30:29.170546
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "55623b100da8"
down_revision = "5c98d9253f56"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("tracking", sa.Column("target", sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("tracking", "target")
# ### end Alembic commands ###

View File

@@ -0,0 +1,23 @@
"""Add sha1sum field to Files
Revision ID: 5c4996aeb2cb
Revises: 9e6f6578ca84
Create Date: 2024-01-07 13:09:08.843903
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "5c4996aeb2cb"
down_revision = "9e6f6578ca84"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("files", sa.Column("sha1sum", sa.String(length=40), nullable=True))
def downgrade():
op.drop_column("files", "sha1sum")

View File

@@ -0,0 +1,119 @@
"""Rename core-beta to core
Revision ID: 5c98d9253f56
Revises: 364b4efa1686
Create Date: 2025-08-24 16:23:18.795064
"""
from alembic import op
from sqlalchemy.sql import column, table
from CTFd.models import db
# revision identifiers, used by Alembic.
revision = "5c98d9253f56"
down_revision = "364b4efa1686"
branch_labels = None
depends_on = None
configs_table = table(
"config", column("id", db.Integer), column("key", db.Text), column("value", db.Text)
)
pages_table = table("pages", column("id", db.Integer), column("content", db.Text))
def upgrade():
connection = op.get_bind()
# Define theme transformations: old_theme -> new_theme
theme_transformations = {
"core-beta": "core",
"hacker-beta-theme": "hacker-theme",
"learning-beta-theme": "learning-theme",
"learning": "learning-theme",
}
# Find the ctf_theme config entry
theme_config = connection.execute(
configs_table.select().where(configs_table.c.key == "ctf_theme")
).fetchone()
# Update ctf_theme config
if (
theme_config
and theme_config.value
and theme_config.value in theme_transformations
):
new_value = theme_transformations[theme_config.value]
connection.execute(
configs_table.update()
.where(configs_table.c.id == theme_config.id)
.values(value=new_value)
)
# Update pages content for all theme transformations
for old_theme, new_theme in theme_transformations.items():
old_path = f"themes/{old_theme}/static"
new_path = f"themes/{new_theme}/static"
pages_with_old_theme = connection.execute(
pages_table.select().where(pages_table.c.content.like(f"%{old_path}%"))
).fetchall()
for page in pages_with_old_theme:
if page.content:
new_content = page.content.replace(old_path, new_path)
connection.execute(
pages_table.update()
.where(pages_table.c.id == page.id)
.values(content=new_content)
)
def downgrade():
connection = op.get_bind()
# Define reverse theme transformations: new_theme -> old_theme
reverse_theme_transformations = {
"core": "core-beta",
"hacker-theme": "hacker-beta-theme",
"learning-theme": "learning-beta-theme",
}
# Find the ctf_theme config entry
theme_config = connection.execute(
configs_table.select().where(configs_table.c.key == "ctf_theme")
).fetchone()
# Restore ctf_theme config
if (
theme_config
and theme_config.value
and theme_config.value in reverse_theme_transformations
):
new_value = reverse_theme_transformations[theme_config.value]
connection.execute(
configs_table.update()
.where(configs_table.c.id == theme_config.id)
.values(value=new_value)
)
# Restore pages content for all theme transformations
for new_theme, old_theme in reverse_theme_transformations.items():
new_path = f"themes/{new_theme}/static"
old_path = f"themes/{old_theme}/static"
pages_with_new_theme = connection.execute(
pages_table.select().where(pages_table.c.content.like(f"%{new_path}%"))
).fetchall()
for page in pages_with_new_theme:
if page.content and old_path not in page.content:
new_content = page.content.replace(new_path, old_path)
connection.execute(
pages_table.update()
.where(pages_table.c.id == page.id)
.values(content=new_content)
)

View File

@@ -0,0 +1,28 @@
"""Add connection_info column to Challenges
Revision ID: 6012fe8de495
Revises: ef87d69ec29a
Create Date: 2021-07-30 03:50:54.219124
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6012fe8de495"
down_revision = "ef87d69ec29a"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("challenges", sa.Column("connection_info", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("challenges", "connection_info")
# ### end Alembic commands ###

View File

@@ -0,0 +1,43 @@
"""Add Solutions table
Revision ID: 62bf576b2cd3
Revises: a49ad66aa0f1
Create Date: 2025-07-28 20:04:45.082529
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "62bf576b2cd3"
down_revision = "a49ad66aa0f1"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"solutions",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("state", sa.String(length=80), nullable=False),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("challenge_id"),
)
op.add_column("files", sa.Column("solution_id", sa.Integer(), nullable=True))
op.create_foreign_key(None, "files", "solutions", ["solution_id"], ["id"])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "files", type_="foreignkey")
op.drop_column("files", "solution_id")
op.drop_table("solutions")
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""Add change_password to Users
Revision ID: 662d728ad7da
Revises: f73a96c97449
Create Date: 2025-08-11 16:38:48.891702
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "662d728ad7da"
down_revision = "f73a96c97449"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("users", sa.Column("change_password", sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("users", "change_password")
# ### end Alembic commands ###

View File

@@ -0,0 +1,36 @@
"""Add dynamic scoring columns to Challenges table
Revision ID: 67ebab6de598
Revises: 24ad6790bc3c
Create Date: 2025-09-11 08:57:33.156731
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "67ebab6de598"
down_revision = "24ad6790bc3c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("challenges", sa.Column("initial", sa.Integer(), nullable=True))
op.add_column("challenges", sa.Column("minimum", sa.Integer(), nullable=True))
op.add_column("challenges", sa.Column("decay", sa.Integer(), nullable=True))
op.add_column(
"challenges", sa.Column("function", sa.String(length=32), nullable=True)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("challenges", "function")
op.drop_column("challenges", "decay")
op.drop_column("challenges", "minimum")
op.drop_column("challenges", "initial")
# ### end Alembic commands ###

View File

@@ -0,0 +1,53 @@
"""Add Fields and FieldEntries tables
Revision ID: 75e8ab9a0014
Revises: 0366ba6575ca
Create Date: 2020-08-19 00:36:17.579497
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "75e8ab9a0014"
down_revision = "0366ba6575ca"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"fields",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("field_type", sa.String(length=80), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("required", sa.Boolean(), nullable=True),
sa.Column("public", sa.Boolean(), nullable=True),
sa.Column("editable", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"field_entries",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("value", sa.JSON(), nullable=True),
sa.Column("field_id", sa.Integer(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["field_id"], ["fields.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("field_entries")
op.drop_table("fields")
# ### end Alembic commands ###

View File

@@ -0,0 +1,249 @@
"""Initial Revision
Revision ID: 8369118943a1
Revises:
Create Date: 2018-11-05 01:06:24.495010
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "8369118943a1"
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"challenges",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=80), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("max_attempts", sa.Integer(), nullable=True),
sa.Column("value", sa.Integer(), nullable=True),
sa.Column("category", sa.String(length=80), nullable=True),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("state", sa.String(length=80), nullable=False),
sa.Column("requirements", sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"config",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("key", sa.Text(), nullable=True),
sa.Column("value", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"pages",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sa.String(length=80), nullable=True),
sa.Column("route", sa.String(length=128), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("draft", sa.Boolean(), nullable=True),
sa.Column("hidden", sa.Boolean(), nullable=True),
sa.Column("auth_required", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("route"),
)
op.create_table(
"teams",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("oauth_id", sa.Integer(), nullable=True),
sa.Column("name", sa.String(length=128), nullable=True),
sa.Column("email", sa.String(length=128), nullable=True),
sa.Column("password", sa.String(length=128), nullable=True),
sa.Column("secret", sa.String(length=128), nullable=True),
sa.Column("website", sa.String(length=128), nullable=True),
sa.Column("affiliation", sa.String(length=128), nullable=True),
sa.Column("country", sa.String(length=32), nullable=True),
sa.Column("bracket", sa.String(length=32), nullable=True),
sa.Column("hidden", sa.Boolean(), nullable=True),
sa.Column("banned", sa.Boolean(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
sa.UniqueConstraint("id", "oauth_id"),
sa.UniqueConstraint("oauth_id"),
)
op.create_table(
"dynamic_challenge",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("initial", sa.Integer(), nullable=True),
sa.Column("minimum", sa.Integer(), nullable=True),
sa.Column("decay", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["id"], ["challenges.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"files",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("location", sa.Text(), nullable=True),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("page_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["challenge_id"], ["challenges.id"]),
sa.ForeignKeyConstraint(["page_id"], ["pages.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"flags",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("data", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(["challenge_id"], ["challenges.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"hints",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("cost", sa.Integer(), nullable=True),
sa.Column("requirements", sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(["challenge_id"], ["challenges.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"tags",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("value", sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(["challenge_id"], ["challenges.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"users",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("oauth_id", sa.Integer(), nullable=True),
sa.Column("name", sa.String(length=128), nullable=True),
sa.Column("password", sa.String(length=128), nullable=True),
sa.Column("email", sa.String(length=128), nullable=True),
sa.Column("type", sa.String(length=80), nullable=True),
sa.Column("secret", sa.String(length=128), nullable=True),
sa.Column("website", sa.String(length=128), nullable=True),
sa.Column("affiliation", sa.String(length=128), nullable=True),
sa.Column("country", sa.String(length=32), nullable=True),
sa.Column("bracket", sa.String(length=32), nullable=True),
sa.Column("hidden", sa.Boolean(), nullable=True),
sa.Column("banned", sa.Boolean(), nullable=True),
sa.Column("verified", sa.Boolean(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.Column("created", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"]),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
sa.UniqueConstraint("id", "oauth_id"),
sa.UniqueConstraint("oauth_id"),
)
op.create_table(
"awards",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.Column("name", sa.String(length=80), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.Column("value", sa.Integer(), nullable=True),
sa.Column("category", sa.String(length=80), nullable=True),
sa.Column("icon", sa.Text(), nullable=True),
sa.Column("requirements", sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"]),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"notifications",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"]),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"submissions",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.Column("ip", sa.String(length=46), nullable=True),
sa.Column("provided", sa.Text(), nullable=True),
sa.Column("type", sa.String(length=32), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"tracking",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("type", sa.String(length=32), nullable=True),
sa.Column("ip", sa.String(length=46), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"unlocks",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.Column("target", sa.Integer(), nullable=True),
sa.Column("date", sa.DateTime(), nullable=True),
sa.Column("type", sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"]),
sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"solves",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("team_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["id"], ["submissions.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["team_id"], ["teams.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("challenge_id", "team_id"),
sa.UniqueConstraint("challenge_id", "user_id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("solves")
op.drop_table("unlocks")
op.drop_table("tracking")
op.drop_table("submissions")
op.drop_table("notifications")
op.drop_table("awards")
op.drop_table("users")
op.drop_table("tags")
op.drop_table("hints")
op.drop_table("flags")
op.drop_table("files")
op.drop_table("dynamic_challenge")
op.drop_table("teams")
op.drop_table("pages")
op.drop_table("config")
op.drop_table("challenges")
# ### end Alembic commands ###

View File

@@ -0,0 +1,51 @@
"""Add Brackets table
Revision ID: 9889b8c53673
Revises: 5c4996aeb2cb
Create Date: 2024-01-25 03:17:52.734753
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = "9889b8c53673"
down_revision = "5c4996aeb2cb"
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
"brackets",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=255), nullable=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("type", sa.String(length=80), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.add_column("teams", sa.Column("bracket_id", sa.Integer(), nullable=True))
op.create_foreign_key(
None, "teams", "brackets", ["bracket_id"], ["id"], ondelete="SET NULL"
)
op.drop_column("teams", "bracket")
op.add_column("users", sa.Column("bracket_id", sa.Integer(), nullable=True))
op.create_foreign_key(
None, "users", "brackets", ["bracket_id"], ["id"], ondelete="SET NULL"
)
op.drop_column("users", "bracket")
def downgrade():
op.add_column(
"users", sa.Column("bracket", mysql.VARCHAR(length=32), nullable=True)
)
op.drop_constraint(None, "users", type_="foreignkey")
op.drop_column("users", "bracket_id")
op.add_column(
"teams", sa.Column("bracket", mysql.VARCHAR(length=32), nullable=True)
)
op.drop_constraint(None, "teams", type_="foreignkey")
op.drop_column("teams", "bracket_id")
op.drop_table("brackets")

View File

@@ -0,0 +1,23 @@
"""Add description column to tokens table
Revision ID: 9e6f6578ca84
Revises: 0def790057c1
Create Date: 2023-06-21 23:22:34.179636
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "9e6f6578ca84"
down_revision = "0def790057c1"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("tokens", sa.Column("description", sa.Text(), nullable=True))
def downgrade():
op.drop_column("tokens", "description")

View File

@@ -0,0 +1,26 @@
"""Add link_target to Pages
Revision ID: a02c5bf43407
Revises: 9889b8c53673
Create Date: 2024-02-01 13:11:53.076825
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a02c5bf43407"
down_revision = "9889b8c53673"
branch_labels = None
depends_on = None
def upgrade():
op.add_column(
"pages", sa.Column("link_target", sa.String(length=80), nullable=True)
)
def downgrade():
op.drop_column("pages", "link_target")

View File

@@ -0,0 +1,46 @@
"""add theme code injections to configs
Revision ID: a03403986a32
Revises: 080d29b15cd3
Create Date: 2020-02-13 01:10:16.430424
"""
from alembic import op # noqa: I001
from sqlalchemy.sql import column, table
from CTFd.models import db
# revision identifiers, used by Alembic.
revision = "a03403986a32"
down_revision = "080d29b15cd3"
branch_labels = None
depends_on = None
configs_table = table(
"config", column("id", db.Integer), column("key", db.Text), column("value", db.Text)
)
def upgrade():
connection = op.get_bind()
css = connection.execute(
configs_table.select().where(configs_table.c.key == "css").limit(1)
).fetchone()
if css and css.value:
new_css = "<style>\n" + css.value + "\n</style>"
config = connection.execute(
configs_table.select().where(configs_table.c.key == "theme_header").limit(1)
).fetchone()
if config:
# Do not overwrite existing theme_header value
pass
else:
connection.execute(
configs_table.insert().values(key="theme_header", value=new_css)
)
def downgrade():
pass

View File

@@ -0,0 +1,23 @@
"""add title to hint
Revision ID: a49ad66aa0f1
Revises: 4fe3eeed9a9d
Create Date: 2025-02-10 14:45:00.933880
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a49ad66aa0f1"
down_revision = "4fe3eeed9a9d"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("hints", sa.Column("title", sa.String(length=80), nullable=True))
def downgrade():
op.drop_column("hints", "title")

View File

@@ -0,0 +1,298 @@
"""Add ondelete cascade to foreign keys
Revision ID: b295b033364d
Revises: b5551cd26764
Create Date: 2019-05-03 19:26:57.746887
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "b295b033364d"
down_revision = "b5551cd26764"
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
url = str(bind.engine.url)
if url.startswith("mysql"):
op.drop_constraint("awards_ibfk_1", "awards", type_="foreignkey")
op.drop_constraint("awards_ibfk_2", "awards", type_="foreignkey")
op.create_foreign_key(
"awards_ibfk_1", "awards", "teams", ["team_id"], ["id"], ondelete="CASCADE"
)
op.create_foreign_key(
"awards_ibfk_2", "awards", "users", ["user_id"], ["id"], ondelete="CASCADE"
)
op.drop_constraint("files_ibfk_1", "files", type_="foreignkey")
op.create_foreign_key(
"files_ibfk_1",
"files",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("flags_ibfk_1", "flags", type_="foreignkey")
op.create_foreign_key(
"flags_ibfk_1",
"flags",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("hints_ibfk_1", "hints", type_="foreignkey")
op.create_foreign_key(
"hints_ibfk_1",
"hints",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("tags_ibfk_1", "tags", type_="foreignkey")
op.create_foreign_key(
"tags_ibfk_1",
"tags",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("team_captain_id", "teams", type_="foreignkey")
op.create_foreign_key(
"team_captain_id",
"teams",
"users",
["captain_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("tracking_ibfk_1", "tracking", type_="foreignkey")
op.create_foreign_key(
"tracking_ibfk_1",
"tracking",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("unlocks_ibfk_1", "unlocks", type_="foreignkey")
op.drop_constraint("unlocks_ibfk_2", "unlocks", type_="foreignkey")
op.create_foreign_key(
"unlocks_ibfk_1",
"unlocks",
"teams",
["team_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
"unlocks_ibfk_2",
"unlocks",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)
elif url.startswith("postgres"):
op.drop_constraint("awards_team_id_fkey", "awards", type_="foreignkey")
op.drop_constraint("awards_user_id_fkey", "awards", type_="foreignkey")
op.create_foreign_key(
"awards_team_id_fkey",
"awards",
"teams",
["team_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
"awards_user_id_fkey",
"awards",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("files_challenge_id_fkey", "files", type_="foreignkey")
op.create_foreign_key(
"files_challenge_id_fkey",
"files",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("flags_challenge_id_fkey", "flags", type_="foreignkey")
op.create_foreign_key(
"flags_challenge_id_fkey",
"flags",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("hints_challenge_id_fkey", "hints", type_="foreignkey")
op.create_foreign_key(
"hints_challenge_id_fkey",
"hints",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("tags_challenge_id_fkey", "tags", type_="foreignkey")
op.create_foreign_key(
"tags_challenge_id_fkey",
"tags",
"challenges",
["challenge_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("team_captain_id", "teams", type_="foreignkey")
op.create_foreign_key(
"team_captain_id",
"teams",
"users",
["captain_id"],
["id"],
ondelete="SET NULL",
)
op.drop_constraint("tracking_user_id_fkey", "tracking", type_="foreignkey")
op.create_foreign_key(
"tracking_user_id_fkey",
"tracking",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("unlocks_team_id_fkey", "unlocks", type_="foreignkey")
op.drop_constraint("unlocks_user_id_fkey", "unlocks", type_="foreignkey")
op.create_foreign_key(
"unlocks_team_id_fkey",
"unlocks",
"teams",
["team_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
"unlocks_user_id_fkey",
"unlocks",
"users",
["user_id"],
["id"],
ondelete="CASCADE",
)
def downgrade():
bind = op.get_bind()
url = str(bind.engine.url)
if url.startswith("mysql"):
op.drop_constraint("unlocks_ibfk_1", "unlocks", type_="foreignkey")
op.drop_constraint("unlocks_ibfk_2", "unlocks", type_="foreignkey")
op.create_foreign_key("unlocks_ibfk_1", "unlocks", "teams", ["team_id"], ["id"])
op.create_foreign_key("unlocks_ibfk_2", "unlocks", "users", ["user_id"], ["id"])
op.drop_constraint("tracking_ibfk_1", "tracking", type_="foreignkey")
op.create_foreign_key(
"tracking_ibfk_1", "tracking", "users", ["user_id"], ["id"]
)
op.drop_constraint("team_captain_id", "teams", type_="foreignkey")
op.create_foreign_key(
"team_captain_id", "teams", "users", ["captain_id"], ["id"]
)
op.drop_constraint("tags_ibfk_1", "tags", type_="foreignkey")
op.create_foreign_key(
"tags_ibfk_1", "tags", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("hints_ibfk_1", "hints", type_="foreignkey")
op.create_foreign_key(
"hints_ibfk_1", "hints", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("flags_ibfk_1", "flags", type_="foreignkey")
op.create_foreign_key(
"flags_ibfk_1", "flags", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("files_ibfk_1", "files", type_="foreignkey")
op.create_foreign_key(
"files_ibfk_1", "files", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("awards_ibfk_1", "awards", type_="foreignkey")
op.drop_constraint("awards_ibfk_2", "awards", type_="foreignkey")
op.create_foreign_key("awards_ibfk_1", "awards", "teams", ["team_id"], ["id"])
op.create_foreign_key("awards_ibfk_2", "awards", "users", ["user_id"], ["id"])
elif url.startswith("postgres"):
op.drop_constraint("unlocks_team_id_fkey", "unlocks", type_="foreignkey")
op.drop_constraint("unlocks_user_id_fkey", "unlocks", type_="foreignkey")
op.create_foreign_key(
"unlocks_team_id_fkey", "unlocks", "teams", ["team_id"], ["id"]
)
op.create_foreign_key(
"unlocks_user_id_fkey", "unlocks", "users", ["user_id"], ["id"]
)
op.drop_constraint("tracking_user_id_fkey", "tracking", type_="foreignkey")
op.create_foreign_key(
"tracking_user_id_fkey", "tracking", "users", ["user_id"], ["id"]
)
op.drop_constraint("team_captain_id", "teams", type_="foreignkey")
op.create_foreign_key(
"team_captain_id", "teams", "users", ["captain_id"], ["id"]
)
op.drop_constraint("tags_challenge_id_fkey", "tags", type_="foreignkey")
op.create_foreign_key(
"tags_challenge_id_fkey", "tags", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("hints_challenge_id_fkey", "hints", type_="foreignkey")
op.create_foreign_key(
"hints_challenge_id_fkey", "hints", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("flags_challenge_id_fkey", "flags", type_="foreignkey")
op.create_foreign_key(
"flags_challenge_id_fkey", "flags", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("files_challenge_id_fkey", "files", type_="foreignkey")
op.create_foreign_key(
"files_challenge_id_fkey", "files", "challenges", ["challenge_id"], ["id"]
)
op.drop_constraint("awards_team_id_fkey", "awards", type_="foreignkey")
op.drop_constraint("awards_user_id_fkey", "awards", type_="foreignkey")
op.create_foreign_key(
"awards_team_id_fkey", "awards", "teams", ["team_id"], ["id"]
)
op.create_foreign_key(
"awards_user_id_fkey", "awards", "users", ["user_id"], ["id"]
)

View File

@@ -0,0 +1,57 @@
"""Add captain column to Teams
Revision ID: b5551cd26764
Revises: 4e4d5a9ea000
Create Date: 2019-04-12 00:29:08.021141
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.sql import column, table
from CTFd.models import db
# revision identifiers, used by Alembic.
revision = "b5551cd26764"
down_revision = "4e4d5a9ea000"
branch_labels = None
depends_on = None
teams_table = table("teams", column("id", db.Integer), column("captain_id", db.Integer))
users_table = table("users", column("id", db.Integer), column("team_id", db.Integer))
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("teams", sa.Column("captain_id", sa.Integer(), nullable=True))
bind = op.get_bind()
url = str(bind.engine.url)
if url.startswith("sqlite") is False:
op.create_foreign_key(
"team_captain_id", "teams", "users", ["captain_id"], ["id"]
)
connection = op.get_bind()
for team in connection.execute(teams_table.select()):
users = connection.execute(
users_table.select()
.where(users_table.c.team_id == team.id)
.order_by(users_table.c.id)
.limit(1)
)
for user in users:
connection.execute(
teams_table.update()
.where(teams_table.c.id == team.id)
.values(captain_id=user.id)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("team_captain_id", "teams", type_="foreignkey")
op.drop_column("teams", "captain_id")
# ### end Alembic commands ###

View File

@@ -0,0 +1,46 @@
"""Add topics and challenge_topics tables
Revision ID: ef87d69ec29a
Revises: 07dfbe5e1edc
Create Date: 2021-07-29 23:22:39.345426
"""
from alembic import op # noqa: I001
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ef87d69ec29a"
down_revision = "07dfbe5e1edc"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"topics",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("value", sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("value"),
)
op.create_table(
"challenge_topics",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("challenge_id", sa.Integer(), nullable=True),
sa.Column("topic_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["challenge_id"], ["challenges.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(["topic_id"], ["topics.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("challenge_topics")
op.drop_table("topics")
# ### end Alembic commands ###

View File

@@ -0,0 +1,30 @@
"""Add logic column to Challenges
Revision ID: f73a96c97449
Revises: 62bf576b2cd3
Create Date: 2025-08-08 21:49:23.417694
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "f73a96c97449"
down_revision = "62bf576b2cd3"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"challenges", sa.Column("logic", sa.String(length=80), nullable=False)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("challenges", "logic")
# ### end Alembic commands ###