Skip to content

Commit 0b302d6

Browse files
committed
feat(tags): Add tags on bills (WIP)
Tags can now be added in the description of a bill, using a hashtag symbol (`#tagname`), and it will be stored in a specific `tag` table.
1 parent eb6e156 commit 0b302d6

File tree

4 files changed

+132
-10
lines changed

4 files changed

+132
-10
lines changed

ihatemoney/forms.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from datetime import datetime
21
import decimal
3-
from re import match
2+
from datetime import datetime
3+
from re import findall, match
44
from types import SimpleNamespace
55

66
import email_validator
@@ -39,7 +39,7 @@
3939
)
4040

4141
from ihatemoney.currency_convertor import CurrencyConverter
42-
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
42+
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project, Tag
4343
from ihatemoney.utils import (
4444
em_surround,
4545
eval_arithmetic_expression,
@@ -389,6 +389,12 @@ def export(self, project):
389389
def save(self, bill, project):
390390
bill.payer_id = self.payer.data
391391
bill.amount = self.amount.data
392+
# Get the list of tags from the 'what' field
393+
hashtags = findall(r"#(\w+)", self.what.data)
394+
if hashtags:
395+
bill.tags = [Tag(name=tag) for tag in hashtags]
396+
for tag in hashtags:
397+
self.what.data = self.what.data.replace(f"#{tag}", "")
392398
bill.what = self.what.data
393399
bill.bill_type = BillType(self.bill_type.data)
394400
bill.external_link = self.external_link.data
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Add a tags table
2+
3+
Revision ID: d53fe61e5521
4+
Revises: 7a9b38559992
5+
Create Date: 2024-05-16 00:32:19.566457
6+
7+
"""
8+
9+
# revision identifiers, used by Alembic.
10+
revision = 'd53fe61e5521'
11+
down_revision = '7a9b38559992'
12+
13+
from alembic import op
14+
import sqlalchemy as sa
15+
16+
17+
def upgrade():
18+
# ### commands auto generated by Alembic - please adjust! ###
19+
op.create_table('billtags_version',
20+
sa.Column('bill_id', sa.Integer(), autoincrement=False, nullable=False),
21+
sa.Column('tag_id', sa.Integer(), autoincrement=False, nullable=False),
22+
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
23+
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
24+
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
25+
sa.PrimaryKeyConstraint('bill_id', 'tag_id', 'transaction_id')
26+
)
27+
with op.batch_alter_table('billtags_version', schema=None) as batch_op:
28+
batch_op.create_index(batch_op.f('ix_billtags_version_end_transaction_id'), ['end_transaction_id'], unique=False)
29+
batch_op.create_index(batch_op.f('ix_billtags_version_operation_type'), ['operation_type'], unique=False)
30+
batch_op.create_index(batch_op.f('ix_billtags_version_transaction_id'), ['transaction_id'], unique=False)
31+
32+
op.create_table('tag',
33+
sa.Column('id', sa.Integer(), nullable=False),
34+
sa.Column('project_id', sa.String(length=64), nullable=True),
35+
sa.Column('name', sa.UnicodeText(), nullable=True),
36+
sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
37+
sa.PrimaryKeyConstraint('id'),
38+
sqlite_autoincrement=True
39+
)
40+
op.create_table('billtags',
41+
sa.Column('bill_id', sa.Integer(), nullable=False),
42+
sa.Column('tag_id', sa.Integer(), nullable=False),
43+
sa.ForeignKeyConstraint(['bill_id'], ['bill.id'], ),
44+
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ),
45+
sa.PrimaryKeyConstraint('bill_id', 'tag_id'),
46+
sqlite_autoincrement=True
47+
)
48+
with op.batch_alter_table('bill_version', schema=None) as batch_op:
49+
batch_op.alter_column('bill_type',
50+
existing_type=sa.TEXT(),
51+
type_=sa.Enum('EXPENSE', 'REIMBURSEMENT', name='billtype'),
52+
existing_nullable=True,
53+
autoincrement=False)
54+
55+
with op.batch_alter_table('billowers', schema=None) as batch_op:
56+
batch_op.alter_column('bill_id',
57+
existing_type=sa.INTEGER(),
58+
nullable=False)
59+
batch_op.alter_column('person_id',
60+
existing_type=sa.INTEGER(),
61+
nullable=False)
62+
63+
# ### end Alembic commands ###
64+
65+
66+
def downgrade():
67+
# ### commands auto generated by Alembic - please adjust! ###
68+
with op.batch_alter_table('billowers', schema=None) as batch_op:
69+
batch_op.alter_column('person_id',
70+
existing_type=sa.INTEGER(),
71+
nullable=True)
72+
batch_op.alter_column('bill_id',
73+
existing_type=sa.INTEGER(),
74+
nullable=True)
75+
76+
with op.batch_alter_table('bill_version', schema=None) as batch_op:
77+
batch_op.alter_column('bill_type',
78+
existing_type=sa.Enum('EXPENSE', 'REIMBURSEMENT', name='billtype'),
79+
type_=sa.TEXT(),
80+
existing_nullable=True,
81+
autoincrement=False)
82+
83+
op.drop_table('billtags')
84+
op.drop_table('tag')
85+
with op.batch_alter_table('billtags_version', schema=None) as batch_op:
86+
batch_op.drop_index(batch_op.f('ix_billtags_version_transaction_id'))
87+
batch_op.drop_index(batch_op.f('ix_billtags_version_operation_type'))
88+
batch_op.drop_index(batch_op.f('ix_billtags_version_end_transaction_id'))
89+
90+
op.drop_table('billtags_version')
91+
# ### end Alembic commands ###

ihatemoney/models.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from collections import defaultdict
21
import datetime
3-
from enum import Enum
42
import itertools
3+
from collections import defaultdict
4+
from enum import Enum
55

6+
import sqlalchemy
67
from dateutil.parser import parse
78
from dateutil.relativedelta import relativedelta
89
from debts import settle
@@ -14,7 +15,6 @@
1415
URLSafeSerializer,
1516
URLSafeTimedSerializer,
1617
)
17-
import sqlalchemy
1818
from sqlalchemy import orm
1919
from sqlalchemy.sql import func
2020
from sqlalchemy_continuum import make_versioned, version_class
@@ -649,6 +649,29 @@ def __repr__(self):
649649
)
650650

651651

652+
class Tag(db.Model):
653+
__versionned__ = {}
654+
655+
__table_args__ = {"sqlite_autoincrement": True}
656+
id = db.Column(db.Integer, primary_key=True)
657+
project_id = db.Column(db.String(64), db.ForeignKey("project.id"))
658+
# bills = db.relationship("Bill", backref="tags")
659+
660+
name = db.Column(db.UnicodeText)
661+
662+
def __str__(self):
663+
return self.name
664+
665+
666+
# We need to manually define a join table for m2m relations
667+
billtags = db.Table(
668+
"billtags",
669+
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
670+
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
671+
sqlite_autoincrement=True,
672+
)
673+
674+
652675
class Bill(db.Model):
653676
class BillQuery(BaseQuery):
654677
def get(self, project, id):
@@ -688,6 +711,7 @@ def delete(self, project, id):
688711
what = db.Column(db.UnicodeText)
689712
bill_type = db.Column(db.Enum(BillType))
690713
external_link = db.Column(db.UnicodeText)
714+
tags = db.relationship(Tag, secondary=billtags)
691715

692716
original_currency = db.Column(db.String(3))
693717
converted_amount = db.Column(db.Float)
@@ -790,3 +814,4 @@ def __repr__(self):
790814
PersonVersion = version_class(Person)
791815
ProjectVersion = version_class(Project)
792816
BillVersion = version_class(Bill)
817+
# TagVersion = version_class(Tag)

ihatemoney/web.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@
22
The blueprint for the web interface.
33
44
Contains all the interaction logic with the end user (except forms which
5-
are directly handled in the forms module.
5+
are directly handled in the forms module).
66
77
Basically, this blueprint takes care of the authentication and provides
88
some shortcuts to make your life better when coding (see `pull_project`
99
and `add_project_id` for a quick overview)
1010
"""
1111
import datetime
12-
from functools import wraps
1312
import hashlib
1413
import json
1514
import os
15+
from functools import wraps
1616
from urllib.parse import urlparse, urlunparse
1717

18+
import qrcode
19+
import qrcode.image.svg
1820
from flask import (
1921
Blueprint,
2022
Response,
@@ -33,8 +35,6 @@
3335
)
3436
from flask_babel import gettext as _
3537
from flask_mail import Message
36-
import qrcode
37-
import qrcode.image.svg
3838
from sqlalchemy_continuum import Operation
3939
from werkzeug.exceptions import NotFound
4040
from werkzeug.security import check_password_hash

0 commit comments

Comments
 (0)