Skip to content

Commit

Permalink
feat: wip: add activity interval and initial support for minimize hab…
Browse files Browse the repository at this point in the history
…its.
  • Loading branch information
codito committed Feb 3, 2024
1 parent 7200d46 commit bcf459d
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 20 deletions.
47 changes: 41 additions & 6 deletions habito/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,57 @@

from habito import models as models

EXAMPLES = """
Examples:
@click.command()
\b
habito add "Write every day" 700 --units words
habito add "Cycle twice a week" 2 --units rounds --interval 7
habito add "Wake up before 6am" 6 --units am --minimize
habito add "Walk to work" 1 --units times
"""


@click.command(epilog=EXAMPLES)
@click.argument("name", nargs=-1)
@click.argument("quantum", type=click.FLOAT)
@click.option("--units", "-u", default="units", help="Units of data.")
def add(name, quantum, units):
"""Add a habit."""
@click.option(
"--interval",
"-i",
type=click.INT,
default=1,
help="Check-in interval in days. Default: 1 day.",
)
@click.option(
"--minimize",
is_flag=True,
default=False,
help=(
"Treat QUANTUM as upper bound. "
"Any lesser value will be considered successful check-in."
),
)
def add(name, quantum, units, interval, minimize):
# noqa
"""Add a habit NAME with QUANTUM goal."""
habit_name = " ".join(name)
models.Habit.add(
name=habit_name,
created_date=datetime.now(),
quantum=quantum,
units=units,
frequency=interval,
minimize=minimize,
magica="",
)

msg_unit = click.style("{0} {1}".format(quantum, units), fg="green")
msg_name = click.style("{0}".format(habit_name), fg="green")
click.echo("You have commited to {0} of {1} every day!".format(msg_unit, msg_name))
msg_neg = "<" if minimize else ""
msg_unit = click.style(f"{msg_neg}{quantum} {units}", fg="green")
msg_name = click.style(f"{habit_name}", fg="green")
msg_interval = click.style(f"{interval} days", fg="green")
click.echo(
"You have commited to {0} of {1} every {2}!".format(
msg_unit, msg_name, msg_interval
)
)
34 changes: 32 additions & 2 deletions habito/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from playhouse.sqlite_ext import SqliteExtDatabase
from playhouse.migrate import SqliteMigrator, migrate

DB_VERSION = 2
DB_VERSION = 3
db = SqliteExtDatabase(None, pragmas=(("foreign_keys", "on"),), regexp_function=True)
logger = logging.getLogger("habito.models")

Expand Down Expand Up @@ -131,6 +131,7 @@ class Habit(BaseModel):
quantum (float): Amount for the habit.
frequency (int): Data input frequency in numbers of days. (Default: 1)
units (str): Units of the quantum.
minimize (bool): Treat quantum as upper bound if True. (Default: False)
magica (str): Why is this habit interesting?
active (bool): True if the habit is active
Expand All @@ -141,6 +142,7 @@ class Habit(BaseModel):
frequency = IntegerField(default=1)
quantum = DoubleField()
units = CharField()
minimize = BooleanField(default=False)
magica = TextField()
active = BooleanField(default=True)

Expand Down Expand Up @@ -350,7 +352,7 @@ def _migration_1(self):
logger.debug("Migration #1: DB version updated to 1.")

# Update summaries
for h in Habit.select():
for h in Habit.select(Habit.id, Habit.created_date):
activities = (
Activity.select()
.where(Activity.for_habit == h)
Expand Down Expand Up @@ -382,3 +384,31 @@ def _migration_2(self):
Config.insert(name="version", value="2").on_conflict("replace").execute()
logger.debug("Migration #2: DB version updated to 2.")
return 0

def _migration_3(self):
"""Apply migration #3.
Add support for minimize habits.
"""
cols = reflection.introspect(self._db).columns
if "minimize" not in cols["habit"]:
with self._db.transaction():
# Not using `migrator.add_column` because of complexity. It
# ends up dropping the table and recreating it, which fails for
# us since `summary` has FK constraint on `habit`.
#
# migrate(
# migrator.add_column(
# table="habit",
# column_name="minimize",
# field=BooleanField(default=False),
# )
# )
self._db.execute_sql(
"ALTER TABLE habit ADD COLUMN minimize INTEGER DEFAULT 0"
)
logger.debug("Migration #3: Add minimize column to habit table.")

Config.insert(name="version", value="3").on_conflict("replace").execute()
logger.debug("Migration #3: DB version updated to 3.")
return 0
27 changes: 26 additions & 1 deletion tests/commands/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,31 @@ class HabitoAddTestCase(HabitoCommandTestCase):
def test_habito_add_should_add_a_habit(self):
result = self._run_command(habito.commands.add, ["dummy habit", "10.01"])

habit = models.Habit.get()
assert result.exit_code == 0
assert models.Habit.get().name == "dummy habit"
assert habit.name == "dummy habit"
assert habit.quantum == 10.01
assert habit.units == "units"
assert habit.frequency == 1
assert models.Summary.get().streak == 0

def test_habito_add_with_interval(self):
result = self._run_command(
habito.commands.add, ["dummy habit", "10.01", "-u", "words", "-i", 2]
)

habit = models.Habit.get()
assert result.exit_code == 0
assert habit.name == "dummy habit"
assert habit.frequency == 2
assert habit.units == "words"

def test_habito_add_invert_habit(self):
result = self._run_command(
habito.commands.add, ["dummy habit", "10.01", "--minimize"]
)

habit = models.Habit.get()
assert result.exit_code == 0
assert habit.name == "dummy habit"
assert habit.minimize is True
55 changes: 55 additions & 0 deletions tests/sql/02.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- DB Version: 2
CREATE TABLE IF NOT EXISTS "habit" (
"id" INTEGER NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"created_date" DATE NOT NULL,
"frequency" INTEGER NOT NULL,
"quantum" REAL NOT NULL,
"units" VARCHAR(255) NOT NULL,
"magica" TEXT NOT NULL,
"active" INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS "activity" (
"id" INTEGER NOT NULL PRIMARY KEY,
"for_habit_id" INTEGER NOT NULL,
"quantum" REAL NOT NULL,
"update_date" DATETIME NOT NULL,
FOREIGN KEY ("for_habit_id") REFERENCES "habit" ("id")
);

CREATE TABLE IF NOT EXISTS "config" (
"id" INTEGER NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"value" VARCHAR(255) NOT NULL
);

CREATE TABLE IF NOT EXISTS "summary" (
"id" INTEGER NOT NULL PRIMARY KEY,
"for_habit_id" INTEGER NOT NULL,
"target" REAL NOT NULL,
"target_date" DATE NOT NULL,
"streak" INTEGER NOT NULL,
FOREIGN KEY ("for_habit_id") REFERENCES "habit" ("id")
);

CREATE INDEX "activitymodel_for_habit_id" ON "activity" ("for_habit_id");
CREATE UNIQUE INDEX "config_name" ON "config" ("name");
CREATE INDEX "summary_for_habit_id" ON "summary" ("for_habit_id");

-- Insert test data
INSERT INTO "habit" ("id", "name", "created_date", "frequency", "quantum", "units", "magica", "active")
VALUES (1, "habit 1", date('now'), 1, 10.0, "units", "sample pledge", 1),
(2, "habit 2", date('now'), 1, 10.0, "units", "sample pledge2", 1);

INSERT INTO "activity" ("id", "for_habit_id", "quantum", "update_date")
VALUES (1, 1, 11.0, date('now', '-1 days')),
(2, 1, 10.0, date('now', '-2 days')),
(3, 2, 11.0, date('now', '-1 days')),
(4, 2, 10.0, date('now', '-3 days'));

INSERT INTO summary ("id", "for_habit_id", "target", "target_date", "streak")
VALUES (1, 1, 0.0, date('now'), 2),
(2, 2, 0.0, date('now'), 1);

INSERT INTO config VALUES(2,'version','2');
43 changes: 32 additions & 11 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ def test_get_version_returns_one_if_key_doesnot_exist(self):
assert version == 1

def test_get_version_returns_version_if_config_exists(self):
self._setup_db_exist_config_version()
self._setup_db_exist_config_version_latest()

version = self.migration.get_version()

assert version == 2
assert version == 3

# Migration scenario: DB doesn't exist
def test_execute_list_result_db_doesnot_exist(self):
Expand All @@ -131,38 +131,48 @@ def test_execute_list_result_db_exist_without_config(self):

result = self.migration.execute(list_only=True)

assert result == {1: -1, 2: -1}
assert result == {1: -1, 2: -1, 3: -1}

def test_execute_run_result_db_exist_without_config(self):
self._setup_db_exist_no_config()

result = self.migration.execute()

assert result == {1: 0, 2: 0}
assert result == {1: 0, 2: 0, 3: 0}
self._verify_row_counts_for_version_2()
self._verify_summary_for_version_2()

def test_execute_migration_1_to_2_is_idempotent(self):
def test_execute_migration_is_idempotent(self):
self._setup_db_exist_no_config()
result = self.migration.execute()
models.Config.update(value="1").where(models.Config.name == "version").execute()

result = self.migration.execute()

assert result == {1: 0, 2: 0}
assert result == {1: 0, 2: 0, 3: 0}
self._verify_row_counts_for_version_2()
self._verify_summary_for_version_2()
self._verify_version_3()

# Migration scenario: DB is at version 2
def test_execute_migration_2_to_3_is_idempotent(self):
self._setup_db_exist_config_version_two()

result = self.migration.execute()

assert result == {2: 0, 3: 0}
self._verify_version_3()

# Migration scenario: DB is at version 3 (latest)
def test_execute_list_result_db_exist_with_config(self):
self._setup_db_exist_config_version()
self._setup_db_exist_config_version_latest()

result = self.migration.execute(list_only=True)

assert result == {}

def test_execute_run_result_db_exist_with_config(self):
self._setup_db_exist_config_version()
self._setup_db_exist_config_version_latest()

result = self.migration.execute()

Expand All @@ -183,10 +193,17 @@ def _setup_db_exist_config_version_doesnot_exist(self):
"""
models.db.create_tables([models.Config])

def _setup_db_exist_config_version(self):
def _setup_db_exist_config_version_two(self):
"""DB version 2 setup."""
models.db.create_tables([models.Config])
models.Config.create(name="version", value="2")
with open("tests/sql/02.sql", "r") as f:
script = f.read()
for s in script.split(";"):
models.db.execute_sql(s + ";")

def _setup_db_exist_config_version_latest(self):
"""DB version 2 setup."""
models.db.create_tables([models.Habit, models.Config])
models.Config.create(name="version", value="3")

# Validations for DB states
def _verify_row_counts_for_version_2(self):
Expand All @@ -203,6 +220,10 @@ def _verify_summary_for_version_2(self):
assert s1 == 2
assert s2 == 1

def _verify_version_3(self):
for h in models.Habit.select():
assert h.minimize is False


class HabitTests(HabitoTestCase):
def setUp(self):
Expand Down

0 comments on commit bcf459d

Please sign in to comment.