Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tags): Add tags on bills (WIP) #1313

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
68 changes: 52 additions & 16 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 @@ -135,7 +135,8 @@ class EditProjectForm(FlaskForm):
_("New private code"),
description=_("Enter a new code if you want to change it"),
)
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
contact_email = StringField(_("Email"), validators=[
DataRequired(), Email()])
project_history = BooleanField(_("Enable project history"))
ip_recording = BooleanField(_("Use IP tracking for project history"))
currency_helper = CurrencyConverter()
Expand Down Expand Up @@ -228,7 +229,8 @@ class ImportProjectForm(FlaskForm):
"File",
validators=[
FileRequired(),
FileAllowed(["json", "JSON", "csv", "CSV"], "Incorrect file format"),
FileAllowed(["json", "JSON", "csv", "CSV"],
"Incorrect file format"),
],
description=_("Compatible with Cospend"),
)
Expand Down Expand Up @@ -349,9 +351,11 @@ class ResetPasswordForm(FlaskForm):


class BillForm(FlaskForm):
date = DateField(_("When?"), validators=[DataRequired()], default=datetime.now)
date = DateField(_("When?"), validators=[
DataRequired()], default=datetime.now)
what = StringField(_("What?"), validators=[DataRequired()])
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
payer = SelectField(_("Who paid?"), validators=[
DataRequired()], coerce=int)
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
currency_helper = CurrencyConverter()
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
Expand All @@ -373,23 +377,48 @@ class BillForm(FlaskForm):
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))

def parse_hashtags(self, project, what):
"""Handles the hashtags which can be optionally specified in the 'what'
field, using the `grocery #hash #otherhash` syntax.

Returns: the new "what" field (with hashtags stripped-out) and the list
of tags.
"""

hashtags = findall(r"#(\w+)", what)

if not hashtags:
return what, []

for tag in hashtags:
what = what.replace(f"#{tag}", "")

return what, hashtags

def export(self, project):
return Bill(
"""This is triggered on bill creation.
"""
what, hashtags = self.parse_hashtags(project, self.what.data)

bill = Bill(
amount=float(self.amount.data),
date=self.date.data,
external_link=self.external_link.data,
original_currency=str(self.original_currency.data),
owers=Person.query.get_by_ids(self.payed_for.data, project),
payer_id=self.payer.data,
project_default_currency=project.default_currency,
what=self.what.data,
what=what,
bill_type=self.bill_type.data,
)
bill.set_tags(hashtags, project)
return bill

def save(self, bill, project):
what, hashtags = self.parse_hashtags(project, self.what.data)
bill.payer_id = self.payer.data
bill.amount = self.amount.data
bill.what = self.what.data
bill.what = what
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data
bill.date = self.date.data
Expand All @@ -398,19 +427,22 @@ def save(self, bill, project):
bill.converted_amount = self.currency_helper.exchange_currency(
bill.amount, bill.original_currency, project.default_currency
)
bill.set_tags(hashtags, project)
return bill

def fill(self, bill, project):
self.payer.data = bill.payer_id
self.amount.data = bill.amount
self.what.data = bill.what
hashtags = ' '.join([f'#{tag.name}' for tag in bill.tags])
self.what.data = bill.what.strip() + f' {hashtags}'
self.bill_type.data = bill.bill_type
self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency
self.date.data = bill.date
self.payed_for.data = [int(ower.id) for ower in bill.owers]

self.original_currency.label = Label("original_currency", _("Currency"))
self.original_currency.label = Label(
"original_currency", _("Currency"))
self.original_currency.description = _(
"Project default: %(currency)s",
currency=render_localized_currency(
Expand Down Expand Up @@ -439,10 +471,13 @@ def validate_original_currency(self, field):


class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
name = StringField(_("Name"), validators=[
DataRequired()], filters=[strip_filter])

weight_validators = [NumberRange(min=0.1, message=_("Weights should be positive"))]
weight = CommaDecimalField(_("Weight"), default=1, validators=weight_validators)
weight_validators = [NumberRange(
min=0.1, message=_("Weights should be positive"))]
weight = CommaDecimalField(
_("Weight"), default=1, validators=weight_validators)
submit = SubmitField(_("Add"))

def __init__(self, project, edit=False, *args, **kwargs):
Expand All @@ -461,7 +496,8 @@ def validate_name(self, field):
Person.activated,
).all()
): # NOQA
raise ValidationError(_("This project already have this participant"))
raise ValidationError(
_("This project already have this participant"))

def save(self, project, person):
# if the user is already bound to the project, just reactivate him
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 ###
Loading
Loading