Skip to content

Commit

Permalink
feat(tags): Add tags on bills
Browse files Browse the repository at this point in the history
Tags can now be added in the description of a bill, using a hashtag
symbol (`#tagname`).

There is no way to "manage" the tags, for simplicity, they are part of
the "what" field, and are parsed via a regular expression.

Statistics have been updated to include tags per month.

Under the hood, a new `tag` table has been added.
  • Loading branch information
almet committed May 30, 2024
1 parent eb6e156 commit 14cc9b9
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 10 deletions.
12 changes: 9 additions & 3 deletions ihatemoney/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime
import decimal
from re import match
from datetime import datetime
from re import findall, match
from types import SimpleNamespace

import email_validator
Expand Down Expand Up @@ -39,7 +39,7 @@
)

from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project, Tag
from ihatemoney.utils import (
em_surround,
eval_arithmetic_expression,
Expand Down Expand Up @@ -389,6 +389,12 @@ def export(self, project):
def save(self, bill, project):
bill.payer_id = self.payer.data
bill.amount = self.amount.data
# Get the list of tags from the 'what' field
hashtags = findall(r"#(\w+)", self.what.data)
if hashtags:
bill.tags = [Tag(name=tag) for tag in hashtags]
for tag in hashtags:
self.what.data = self.what.data.replace(f"#{tag}", "")
bill.what = self.what.data
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data
Expand Down
91 changes: 91 additions & 0 deletions ihatemoney/migrations/versions/d53fe61e5521_add_a_tags_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Add a tags table
Revision ID: d53fe61e5521
Revises: 7a9b38559992
Create Date: 2024-05-16 00:32:19.566457
"""

# revision identifiers, used by Alembic.
revision = 'd53fe61e5521'
down_revision = '7a9b38559992'

from alembic import op
import sqlalchemy as sa


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('billtags_version',
sa.Column('bill_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('tag_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('bill_id', 'tag_id', 'transaction_id')
)
with op.batch_alter_table('billtags_version', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_billtags_version_end_transaction_id'), ['end_transaction_id'], unique=False)
batch_op.create_index(batch_op.f('ix_billtags_version_operation_type'), ['operation_type'], unique=False)
batch_op.create_index(batch_op.f('ix_billtags_version_transaction_id'), ['transaction_id'], unique=False)

op.create_table('tag',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('project_id', sa.String(length=64), nullable=True),
sa.Column('name', sa.UnicodeText(), nullable=True),
sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
sa.PrimaryKeyConstraint('id'),
sqlite_autoincrement=True
)
op.create_table('billtags',
sa.Column('bill_id', sa.Integer(), nullable=False),
sa.Column('tag_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['bill_id'], ['bill.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ),
sa.PrimaryKeyConstraint('bill_id', 'tag_id'),
sqlite_autoincrement=True
)
with op.batch_alter_table('bill_version', schema=None) as batch_op:
batch_op.alter_column('bill_type',
existing_type=sa.TEXT(),
type_=sa.Enum('EXPENSE', 'REIMBURSEMENT', name='billtype'),
existing_nullable=True,
autoincrement=False)

with op.batch_alter_table('billowers', schema=None) as batch_op:
batch_op.alter_column('bill_id',
existing_type=sa.INTEGER(),
nullable=False)
batch_op.alter_column('person_id',
existing_type=sa.INTEGER(),
nullable=False)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('billowers', schema=None) as batch_op:
batch_op.alter_column('person_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.alter_column('bill_id',
existing_type=sa.INTEGER(),
nullable=True)

with op.batch_alter_table('bill_version', schema=None) as batch_op:
batch_op.alter_column('bill_type',
existing_type=sa.Enum('EXPENSE', 'REIMBURSEMENT', name='billtype'),
type_=sa.TEXT(),
existing_nullable=True,
autoincrement=False)

op.drop_table('billtags')
op.drop_table('tag')
with op.batch_alter_table('billtags_version', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_billtags_version_transaction_id'))
batch_op.drop_index(batch_op.f('ix_billtags_version_operation_type'))
batch_op.drop_index(batch_op.f('ix_billtags_version_end_transaction_id'))

op.drop_table('billtags_version')
# ### end Alembic commands ###
31 changes: 28 additions & 3 deletions ihatemoney/models.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from collections import defaultdict
import datetime
from enum import Enum
import itertools
from collections import defaultdict
from enum import Enum

import sqlalchemy
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from debts import settle
Expand All @@ -14,7 +15,6 @@
URLSafeSerializer,
URLSafeTimedSerializer,
)
import sqlalchemy
from sqlalchemy import orm
from sqlalchemy.sql import func
from sqlalchemy_continuum import make_versioned, version_class
Expand Down Expand Up @@ -649,6 +649,29 @@ def __repr__(self):
)


class Tag(db.Model):
__versionned__ = {}

__table_args__ = {"sqlite_autoincrement": True}
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.String(64), db.ForeignKey("project.id"))
# bills = db.relationship("Bill", backref="tags")

name = db.Column(db.UnicodeText)

def __str__(self):
return self.name


# We need to manually define a join table for m2m relations
billtags = db.Table(
"billtags",
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
sqlite_autoincrement=True,
)


class Bill(db.Model):
class BillQuery(BaseQuery):
def get(self, project, id):
Expand Down Expand Up @@ -688,6 +711,7 @@ def delete(self, project, id):
what = db.Column(db.UnicodeText)
bill_type = db.Column(db.Enum(BillType))
external_link = db.Column(db.UnicodeText)
tags = db.relationship(Tag, secondary=billtags)

original_currency = db.Column(db.String(3))
converted_amount = db.Column(db.Float)
Expand Down Expand Up @@ -790,3 +814,4 @@ def __repr__(self):
PersonVersion = version_class(Person)
ProjectVersion = version_class(Project)
BillVersion = version_class(Bill)
# TagVersion = version_class(Tag)
8 changes: 4 additions & 4 deletions ihatemoney/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
The blueprint for the web interface.
Contains all the interaction logic with the end user (except forms which
are directly handled in the forms module.
are directly handled in the forms module).
Basically, this blueprint takes care of the authentication and provides
some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview)
"""
import datetime
from functools import wraps
import hashlib
import json
import os
from functools import wraps
from urllib.parse import urlparse, urlunparse

import qrcode
import qrcode.image.svg
from flask import (
Blueprint,
Response,
Expand All @@ -33,8 +35,6 @@
)
from flask_babel import gettext as _
from flask_mail import Message
import qrcode
import qrcode.image.svg
from sqlalchemy_continuum import Operation
from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash
Expand Down

0 comments on commit 14cc9b9

Please sign in to comment.