mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
feat(tags): Add tags on bills
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.
This commit is contained in:
parent
eb6e156c32
commit
14cc9b96d3
4 changed files with 132 additions and 10 deletions
|
@ -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
|
||||
|
@ -39,7 +39,7 @@ from wtforms.validators import (
|
|||
)
|
||||
|
||||
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,
|
||||
|
@ -389,6 +389,12 @@ class BillForm(FlaskForm):
|
|||
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
|
||||
|
|
|
@ -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 ###
|
|
@ -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
|
||||
|
@ -14,7 +15,6 @@ from itsdangerous import (
|
|||
URLSafeSerializer,
|
||||
URLSafeTimedSerializer,
|
||||
)
|
||||
import sqlalchemy
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy_continuum import make_versioned, version_class
|
||||
|
@ -649,6 +649,29 @@ billowers = db.Table(
|
|||
)
|
||||
|
||||
|
||||
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):
|
||||
|
@ -688,6 +711,7 @@ class Bill(db.Model):
|
|||
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)
|
||||
|
@ -790,3 +814,4 @@ sqlalchemy.orm.configure_mappers()
|
|||
PersonVersion = version_class(Person)
|
||||
ProjectVersion = version_class(Project)
|
||||
BillVersion = version_class(Bill)
|
||||
# TagVersion = version_class(Tag)
|
||||
|
|
|
@ -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,
|
||||
|
@ -33,8 +35,6 @@ from flask import (
|
|||
)
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue