mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
Use black to refomat the files.
This commit is contained in:
parent
f2a0b9f3f0
commit
f260a2c9e7
17 changed files with 1479 additions and 1075 deletions
|
@ -5,11 +5,10 @@ templates_path = ["_templates"]
|
|||
source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
|
||||
project = u"I hate money"
|
||||
copyright = u"2011, The 'I hate money' team"
|
||||
project = "I hate money"
|
||||
copyright = "2011, The 'I hate money' team"
|
||||
|
||||
version = "1.0"
|
||||
release = "1.0"
|
||||
exclude_patterns = ["_build"]
|
||||
pygments_style = "sphinx"
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ from flask_cors import CORS
|
|||
from wtforms.fields.core import BooleanField
|
||||
|
||||
from ihatemoney.models import db, Project, Person, Bill
|
||||
from ihatemoney.forms import (ProjectForm, EditProjectForm, MemberForm,
|
||||
get_billform_for)
|
||||
from ihatemoney.forms import ProjectForm, EditProjectForm, MemberForm, get_billform_for
|
||||
from werkzeug.security import check_password_hash
|
||||
from functools import wraps
|
||||
|
||||
|
@ -21,6 +20,7 @@ def need_auth(f):
|
|||
|
||||
Return the project if the authorization is good, abort the request with a 401 otherwise
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
auth = request.authorization
|
||||
|
@ -35,25 +35,26 @@ def need_auth(f):
|
|||
return f(*args, project=project, **kwargs)
|
||||
else:
|
||||
# Use Bearer token Auth
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
auth_token = ''
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
auth_token = ""
|
||||
try:
|
||||
auth_token = auth_header.split(" ")[1]
|
||||
except IndexError:
|
||||
abort(401)
|
||||
project_id = Project.verify_token(auth_token, token_type='non_timed_token')
|
||||
project_id = Project.verify_token(auth_token, token_type="non_timed_token")
|
||||
if auth_token and project_id:
|
||||
project = Project.query.get(project_id)
|
||||
if project:
|
||||
kwargs.pop("project_id")
|
||||
return f(*args, project=project, **kwargs)
|
||||
abort(401)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class ProjectsHandler(Resource):
|
||||
def post(self):
|
||||
form = ProjectForm(meta={'csrf': False})
|
||||
form = ProjectForm(meta={"csrf": False})
|
||||
if form.validate():
|
||||
project = form.save()
|
||||
db.session.add(project)
|
||||
|
@ -74,7 +75,7 @@ class ProjectHandler(Resource):
|
|||
return "DELETED"
|
||||
|
||||
def put(self, project):
|
||||
form = EditProjectForm(meta={'csrf': False})
|
||||
form = EditProjectForm(meta={"csrf": False})
|
||||
if form.validate():
|
||||
form.update(project)
|
||||
db.session.commit()
|
||||
|
@ -94,7 +95,8 @@ class APIMemberForm(MemberForm):
|
|||
|
||||
But we want Member.enabled to be togglable via the API.
|
||||
"""
|
||||
activated = BooleanField(false_values=('false', '', 'False'))
|
||||
|
||||
activated = BooleanField(false_values=("false", "", "False"))
|
||||
|
||||
def save(self, project, person):
|
||||
person.activated = self.activated.data
|
||||
|
@ -108,7 +110,7 @@ class MembersHandler(Resource):
|
|||
return project.members
|
||||
|
||||
def post(self, project):
|
||||
form = MemberForm(project, meta={'csrf': False})
|
||||
form = MemberForm(project, meta={"csrf": False})
|
||||
if form.validate():
|
||||
member = Person()
|
||||
form.save(project, member)
|
||||
|
@ -127,7 +129,7 @@ class MemberHandler(Resource):
|
|||
return member
|
||||
|
||||
def put(self, project, member_id):
|
||||
form = APIMemberForm(project, meta={'csrf': False}, edit=True)
|
||||
form = APIMemberForm(project, meta={"csrf": False}, edit=True)
|
||||
if form.validate():
|
||||
member = Person.query.get(member_id, project)
|
||||
form.save(project, member)
|
||||
|
@ -148,7 +150,7 @@ class BillsHandler(Resource):
|
|||
return project.get_bills().all()
|
||||
|
||||
def post(self, project):
|
||||
form = get_billform_for(project, True, meta={'csrf': False})
|
||||
form = get_billform_for(project, True, meta={"csrf": False})
|
||||
if form.validate():
|
||||
bill = Bill()
|
||||
form.save(bill, project)
|
||||
|
@ -168,7 +170,7 @@ class BillHandler(Resource):
|
|||
return bill, 200
|
||||
|
||||
def put(self, project, bill_id):
|
||||
form = get_billform_for(project, True, meta={'csrf': False})
|
||||
form = get_billform_for(project, True, meta={"csrf": False})
|
||||
if form.validate():
|
||||
bill = Bill.query.get(project, bill_id)
|
||||
form.save(bill, project)
|
||||
|
@ -184,10 +186,16 @@ class BillHandler(Resource):
|
|||
return "OK", 200
|
||||
|
||||
|
||||
restful_api.add_resource(ProjectsHandler, '/projects')
|
||||
restful_api.add_resource(ProjectHandler, '/projects/<string:project_id>')
|
||||
restful_api.add_resource(ProjectsHandler, "/projects")
|
||||
restful_api.add_resource(ProjectHandler, "/projects/<string:project_id>")
|
||||
restful_api.add_resource(MembersHandler, "/projects/<string:project_id>/members")
|
||||
restful_api.add_resource(ProjectStatsHandler, "/projects/<string:project_id>/statistics")
|
||||
restful_api.add_resource(MemberHandler, "/projects/<string:project_id>/members/<int:member_id>")
|
||||
restful_api.add_resource(
|
||||
ProjectStatsHandler, "/projects/<string:project_id>/statistics"
|
||||
)
|
||||
restful_api.add_resource(
|
||||
MemberHandler, "/projects/<string:project_id>/members/<int:member_id>"
|
||||
)
|
||||
restful_api.add_resource(BillsHandler, "/projects/<string:project_id>/bills")
|
||||
restful_api.add_resource(BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>")
|
||||
restful_api.add_resource(
|
||||
BillHandler, "/projects/<string:project_id>/bills/<int:bill_id>"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Verbose and documented settings are in conf-templates/ihatemoney.cfg.j2
|
||||
DEBUG = SQLACHEMY_ECHO = False
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/ihatemoney.db'
|
||||
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/ihatemoney.db"
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = "tralala"
|
||||
MAIL_DEFAULT_SENDER = ("Budget manager", "budget@notmyidea.org")
|
||||
|
@ -8,4 +8,4 @@ ACTIVATE_DEMO_PROJECT = True
|
|||
ADMIN_PASSWORD = ""
|
||||
ALLOW_PUBLIC_PROJECT_CREATION = True
|
||||
ACTIVATE_ADMIN_DASHBOARD = False
|
||||
SUPPORTED_LANGUAGES = ['en', 'fr', 'de', 'nl', 'es_419']
|
||||
SUPPORTED_LANGUAGES = ["en", "fr", "de", "nl", "es_419"]
|
||||
|
|
|
@ -2,7 +2,14 @@ from flask_wtf.form import FlaskForm
|
|||
from wtforms.fields.core import SelectField, SelectMultipleField
|
||||
from wtforms.fields.html5 import DateField, DecimalField, URLField
|
||||
from wtforms.fields.simple import PasswordField, SubmitField, TextAreaField, StringField
|
||||
from wtforms.validators import Email, DataRequired, ValidationError, EqualTo, NumberRange, Optional
|
||||
from wtforms.validators import (
|
||||
Email,
|
||||
DataRequired,
|
||||
ValidationError,
|
||||
EqualTo,
|
||||
NumberRange,
|
||||
Optional,
|
||||
)
|
||||
from flask_babel import lazy_gettext as _
|
||||
from flask import request
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
@ -48,7 +55,7 @@ class CommaDecimalField(DecimalField):
|
|||
|
||||
def process_formdata(self, value):
|
||||
if value:
|
||||
value[0] = str(value[0]).replace(',', '.')
|
||||
value[0] = str(value[0]).replace(",", ".")
|
||||
return super(CommaDecimalField, self).process_formdata(value)
|
||||
|
||||
|
||||
|
@ -68,7 +75,7 @@ class CalculatorStringField(StringField):
|
|||
value = str(valuelist[0]).replace(",", ".")
|
||||
|
||||
# avoid exponents to prevent expensive calculations i.e 2**9999999999**9999999
|
||||
if not match(r'^[ 0-9\.\+\-\*/\(\)]{0,200}$', value) or "**" in value:
|
||||
if not match(r"^[ 0-9\.\+\-\*/\(\)]{0,200}$", value) or "**" in value:
|
||||
raise ValueError(Markup(message))
|
||||
|
||||
valuelist[0] = str(eval_arithmetic_expression(value))
|
||||
|
@ -86,9 +93,12 @@ class EditProjectForm(FlaskForm):
|
|||
|
||||
Returns the created instance
|
||||
"""
|
||||
project = Project(name=self.name.data, id=self.id.data,
|
||||
password=generate_password_hash(self.password.data),
|
||||
contact_email=self.contact_email.data)
|
||||
project = Project(
|
||||
name=self.name.data,
|
||||
id=self.id.data,
|
||||
password=generate_password_hash(self.password.data),
|
||||
contact_email=self.contact_email.data,
|
||||
)
|
||||
return project
|
||||
|
||||
def update(self, project):
|
||||
|
@ -108,8 +118,11 @@ class ProjectForm(EditProjectForm):
|
|||
def validate_id(form, field):
|
||||
form.id.data = slugify(field.data)
|
||||
if (form.id.data == "dashboard") or Project.query.get(form.id.data):
|
||||
message = _("A project with this identifier (\"%(project)s\") already exists. "
|
||||
"Please choose a new identifier", project=form.id.data)
|
||||
message = _(
|
||||
'A project with this identifier ("%(project)s") already exists. '
|
||||
"Please choose a new identifier",
|
||||
project=form.id.data,
|
||||
)
|
||||
raise ValidationError(Markup(message))
|
||||
|
||||
|
||||
|
@ -134,10 +147,14 @@ class PasswordReminder(FlaskForm):
|
|||
|
||||
|
||||
class ResetPasswordForm(FlaskForm):
|
||||
password_validators = [DataRequired(),
|
||||
EqualTo('password_confirmation', message=_("Password mismatch"))]
|
||||
password_validators = [
|
||||
DataRequired(),
|
||||
EqualTo("password_confirmation", message=_("Password mismatch")),
|
||||
]
|
||||
password = PasswordField(_("Password"), validators=password_validators)
|
||||
password_confirmation = PasswordField(_("Password confirmation"), validators=[DataRequired()])
|
||||
password_confirmation = PasswordField(
|
||||
_("Password confirmation"), validators=[DataRequired()]
|
||||
)
|
||||
submit = SubmitField(_("Reset password"))
|
||||
|
||||
|
||||
|
@ -146,10 +163,14 @@ class BillForm(FlaskForm):
|
|||
what = StringField(_("What?"), validators=[DataRequired()])
|
||||
payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int)
|
||||
amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()])
|
||||
external_link = URLField(_("External link"), validators=[Optional(
|
||||
)], description=_("A link to an external document, related to this bill"))
|
||||
payed_for = SelectMultipleField(_("For whom?"),
|
||||
validators=[DataRequired()], coerce=int)
|
||||
external_link = URLField(
|
||||
_("External link"),
|
||||
validators=[Optional()],
|
||||
description=_("A link to an external document, related to this bill"),
|
||||
)
|
||||
payed_for = SelectMultipleField(
|
||||
_("For whom?"), validators=[DataRequired()], coerce=int
|
||||
)
|
||||
submit = SubmitField(_("Submit"))
|
||||
submit2 = SubmitField(_("Submit and add a new one"))
|
||||
|
||||
|
@ -159,8 +180,7 @@ class BillForm(FlaskForm):
|
|||
bill.what = self.what.data
|
||||
bill.external_link = self.external_link.data
|
||||
bill.date = self.date.data
|
||||
bill.owers = [Person.query.get(ower, project)
|
||||
for ower in self.payed_for.data]
|
||||
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
|
||||
|
||||
return bill
|
||||
|
||||
|
@ -181,11 +201,10 @@ class BillForm(FlaskForm):
|
|||
|
||||
|
||||
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 = CommaDecimalField(_("Weight"), default=1, validators=weight_validators)
|
||||
submit = SubmitField(_("Add"))
|
||||
|
||||
def __init__(self, project, edit=False, *args, **kwargs):
|
||||
|
@ -196,10 +215,14 @@ class MemberForm(FlaskForm):
|
|||
def validate_name(form, field):
|
||||
if field.data == form.name.default:
|
||||
raise ValidationError(_("User name incorrect"))
|
||||
if (not form.edit and Person.query.filter(
|
||||
if (
|
||||
not form.edit
|
||||
and Person.query.filter(
|
||||
Person.name == field.data,
|
||||
Person.project == form.project,
|
||||
Person.activated == True).all()): # NOQA
|
||||
Person.activated == True,
|
||||
).all()
|
||||
): # NOQA
|
||||
raise ValidationError(_("This project already have this member"))
|
||||
|
||||
def save(self, project, person):
|
||||
|
@ -224,5 +247,6 @@ class InviteForm(FlaskForm):
|
|||
try:
|
||||
email_validator.validate_email(email)
|
||||
except email_validator.EmailNotValidError:
|
||||
raise ValidationError(_("The email %(email)s is not valid",
|
||||
email=email))
|
||||
raise ValidationError(
|
||||
_("The email %(email)s is not valid", email=email)
|
||||
)
|
||||
|
|
|
@ -19,42 +19,51 @@ class GeneratePasswordHash(Command):
|
|||
"""Get password from user and hash it without printing it in clear text."""
|
||||
|
||||
def run(self):
|
||||
password = getpass.getpass(prompt='Password: ')
|
||||
password = getpass.getpass(prompt="Password: ")
|
||||
print(generate_password_hash(password))
|
||||
|
||||
|
||||
class GenerateConfig(Command):
|
||||
def get_options(self):
|
||||
return [
|
||||
Option('config_file', choices=[
|
||||
'ihatemoney.cfg',
|
||||
'apache-vhost.conf',
|
||||
'gunicorn.conf.py',
|
||||
'supervisord.conf',
|
||||
'nginx.conf',
|
||||
]),
|
||||
Option(
|
||||
"config_file",
|
||||
choices=[
|
||||
"ihatemoney.cfg",
|
||||
"apache-vhost.conf",
|
||||
"gunicorn.conf.py",
|
||||
"supervisord.conf",
|
||||
"nginx.conf",
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def gen_secret_key():
|
||||
return ''.join([
|
||||
random.SystemRandom().choice(
|
||||
'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)')
|
||||
for i in range(50)])
|
||||
return "".join(
|
||||
[
|
||||
random.SystemRandom().choice(
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
||||
)
|
||||
for i in range(50)
|
||||
]
|
||||
)
|
||||
|
||||
def run(self, config_file):
|
||||
env = create_jinja_env('conf-templates', strict_rendering=True)
|
||||
template = env.get_template('%s.j2' % config_file)
|
||||
env = create_jinja_env("conf-templates", strict_rendering=True)
|
||||
template = env.get_template("%s.j2" % config_file)
|
||||
|
||||
bin_path = os.path.dirname(sys.executable)
|
||||
pkg_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
print(template.render(
|
||||
print(
|
||||
template.render(
|
||||
pkg_path=pkg_path,
|
||||
bin_path=bin_path,
|
||||
sys_prefix=sys.prefix,
|
||||
secret_key=self.gen_secret_key(),
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class DeleteProject(Command):
|
||||
|
@ -65,14 +74,14 @@ class DeleteProject(Command):
|
|||
|
||||
|
||||
def main():
|
||||
QUIET_COMMANDS = ('generate_password_hash', 'generate-config')
|
||||
QUIET_COMMANDS = ("generate_password_hash", "generate-config")
|
||||
|
||||
exception = None
|
||||
backup_stderr = sys.stderr
|
||||
# Hack to divert stderr for commands generating content to stdout
|
||||
# to avoid confusing the user
|
||||
if len(sys.argv) > 1 and sys.argv[1] in QUIET_COMMANDS:
|
||||
sys.stderr = open(os.devnull, 'w')
|
||||
sys.stderr = open(os.devnull, "w")
|
||||
|
||||
try:
|
||||
app = create_app()
|
||||
|
@ -87,12 +96,12 @@ def main():
|
|||
raise exception
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
manager.add_command('generate_password_hash', GeneratePasswordHash)
|
||||
manager.add_command('generate-config', GenerateConfig)
|
||||
manager.add_command('delete-project', DeleteProject)
|
||||
manager.add_command("db", MigrateCommand)
|
||||
manager.add_command("generate_password_hash", GeneratePasswordHash)
|
||||
manager.add_command("generate-config", GenerateConfig)
|
||||
manager.add_command("delete-project", DeleteProject)
|
||||
manager.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -11,14 +11,16 @@ config = context.config
|
|||
# Interpret the config file for Python logging. This line sets up loggers
|
||||
# basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
logger = logging.getLogger("alembic.env")
|
||||
|
||||
# Add your model's MetaData object here for 'autogenerate' support from myapp
|
||||
# import mymodel target_metadata = mymodel.Base.metadata.
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url',
|
||||
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
config.set_main_option(
|
||||
"sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI")
|
||||
)
|
||||
target_metadata = current_app.extensions["migrate"].db.metadata
|
||||
|
||||
# Other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
|
@ -57,21 +59,25 @@ def run_migrations_online():
|
|||
# when there are no changes to the schema.
|
||||
# reference: https://alembic.readthedocs.io/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
if getattr(config.cmd_opts, "autogenerate", False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
logger.info("No changes in schema detected.")
|
||||
|
||||
engine = engine_from_config(config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions["migrate"].configure_args
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
|
@ -79,6 +85,7 @@ def run_migrations_online():
|
|||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
|
|
|
@ -7,8 +7,8 @@ Create Date: 2016-06-15 09:22:04.069447
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '26d6a218c329'
|
||||
down_revision = 'b9a10d5d63ce'
|
||||
revision = "26d6a218c329"
|
||||
down_revision = "b9a10d5d63ce"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
@ -16,11 +16,11 @@ import sqlalchemy as sa
|
|||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('person', sa.Column('weight', sa.Float(), nullable=True))
|
||||
op.add_column("person", sa.Column("weight", sa.Float(), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('person', 'weight')
|
||||
op.drop_column("person", "weight")
|
||||
### end Alembic commands ###
|
||||
|
|
|
@ -7,8 +7,8 @@ Create Date: 2019-09-28 13:38:09.550747
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6c6fb2b7f229'
|
||||
down_revision = 'a67119aa3ee5'
|
||||
revision = "6c6fb2b7f229"
|
||||
down_revision = "a67119aa3ee5"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
@ -16,11 +16,11 @@ import sqlalchemy as sa
|
|||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('bill', sa.Column('external_link', sa.UnicodeText(), nullable=True))
|
||||
op.add_column("bill", sa.Column("external_link", sa.UnicodeText(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('bill', 'external_link')
|
||||
op.drop_column("bill", "external_link")
|
||||
# ### end Alembic commands ###
|
||||
|
|
|
@ -7,29 +7,29 @@ Create Date: 2018-12-25 18:34:20.220844
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a67119aa3ee5'
|
||||
down_revision = 'afbf27e6ef20'
|
||||
revision = "a67119aa3ee5"
|
||||
down_revision = "afbf27e6ef20"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# Snapshot of the person table
|
||||
person_helper = sa.Table(
|
||||
'person', sa.MetaData(),
|
||||
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.Column('activated', sa.Boolean(), nullable=True),
|
||||
sa.Column('weight', sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
"person",
|
||||
sa.MetaData(),
|
||||
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.Column("activated", sa.Boolean(), nullable=True),
|
||||
sa.Column("weight", sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
person_helper.update()
|
||||
.where(person_helper.c.weight <= 0)
|
||||
.values(weight=1)
|
||||
person_helper.update().where(person_helper.c.weight <= 0).values(weight=1)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ Create Date: 2018-02-19 20:29:26.286136
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'afbf27e6ef20'
|
||||
down_revision = 'b78f8a8bdb16'
|
||||
revision = "afbf27e6ef20"
|
||||
down_revision = "b78f8a8bdb16"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
@ -16,11 +16,11 @@ import sqlalchemy as sa
|
|||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('bill', sa.Column('creation_date', sa.Date(), nullable=True))
|
||||
op.add_column("bill", sa.Column("creation_date", sa.Date(), nullable=True))
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('bill', 'creation_date')
|
||||
op.drop_column("bill", "creation_date")
|
||||
### end Alembic commands ###
|
||||
|
|
|
@ -7,20 +7,21 @@ Create Date: 2017-12-17 11:45:44.783238
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b78f8a8bdb16'
|
||||
down_revision = 'f629c8ef4ab0'
|
||||
revision = "b78f8a8bdb16"
|
||||
down_revision = "f629c8ef4ab0"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
project_helper = sa.Table(
|
||||
'project', sa.MetaData(),
|
||||
sa.Column('id', sa.String(length=64), nullable=False),
|
||||
sa.Column('name', sa.UnicodeText(), nullable=True),
|
||||
sa.Column('password', sa.String(length=128), nullable=True),
|
||||
sa.Column('contact_email', sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
"project",
|
||||
sa.MetaData(),
|
||||
sa.Column("id", sa.String(length=64), nullable=False),
|
||||
sa.Column("name", sa.UnicodeText(), nullable=True),
|
||||
sa.Column("password", sa.String(length=128), nullable=True),
|
||||
sa.Column("contact_email", sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -28,11 +29,9 @@ def upgrade():
|
|||
connection = op.get_bind()
|
||||
for project in connection.execute(project_helper.select()):
|
||||
connection.execute(
|
||||
project_helper.update().where(
|
||||
project_helper.c.name == project.name
|
||||
).values(
|
||||
password=generate_password_hash(project.password)
|
||||
)
|
||||
project_helper.update()
|
||||
.where(project_helper.c.name == project.name)
|
||||
.values(password=generate_password_hash(project.password))
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ Create Date: 2016-05-21 23:21:21.605076
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b9a10d5d63ce'
|
||||
revision = "b9a10d5d63ce"
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
|
@ -16,53 +16,58 @@ import sqlalchemy as sa
|
|||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('project',
|
||||
sa.Column('id', sa.String(length=64), nullable=False),
|
||||
sa.Column('name', sa.UnicodeText(), nullable=True),
|
||||
sa.Column('password', sa.String(length=128), nullable=True),
|
||||
sa.Column('contact_email', sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table(
|
||||
"project",
|
||||
sa.Column("id", sa.String(length=64), nullable=False),
|
||||
sa.Column("name", sa.UnicodeText(), nullable=True),
|
||||
sa.Column("password", sa.String(length=128), nullable=True),
|
||||
sa.Column("contact_email", sa.String(length=128), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table('archive',
|
||||
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')
|
||||
op.create_table(
|
||||
"archive",
|
||||
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"),
|
||||
)
|
||||
op.create_table('person',
|
||||
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.Column('activated', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table(
|
||||
"person",
|
||||
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.Column("activated", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table('bill',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('payer_id', sa.Integer(), nullable=True),
|
||||
sa.Column('amount', sa.Float(), nullable=True),
|
||||
sa.Column('date', sa.Date(), nullable=True),
|
||||
sa.Column('what', sa.UnicodeText(), nullable=True),
|
||||
sa.Column('archive', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['archive'], ['archive.id'], ),
|
||||
sa.ForeignKeyConstraint(['payer_id'], ['person.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
op.create_table(
|
||||
"bill",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("payer_id", sa.Integer(), nullable=True),
|
||||
sa.Column("amount", sa.Float(), nullable=True),
|
||||
sa.Column("date", sa.Date(), nullable=True),
|
||||
sa.Column("what", sa.UnicodeText(), nullable=True),
|
||||
sa.Column("archive", sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["archive"], ["archive.id"]),
|
||||
sa.ForeignKeyConstraint(["payer_id"], ["person.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table('billowers',
|
||||
sa.Column('bill_id', sa.Integer(), nullable=True),
|
||||
sa.Column('person_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['bill_id'], ['bill.id'], ),
|
||||
sa.ForeignKeyConstraint(['person_id'], ['person.id'], )
|
||||
op.create_table(
|
||||
"billowers",
|
||||
sa.Column("bill_id", sa.Integer(), nullable=True),
|
||||
sa.Column("person_id", sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["bill_id"], ["bill.id"]),
|
||||
sa.ForeignKeyConstraint(["person_id"], ["person.id"]),
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('billowers')
|
||||
op.drop_table('bill')
|
||||
op.drop_table('person')
|
||||
op.drop_table('archive')
|
||||
op.drop_table('project')
|
||||
op.drop_table("billowers")
|
||||
op.drop_table("bill")
|
||||
op.drop_table("person")
|
||||
op.drop_table("archive")
|
||||
op.drop_table("project")
|
||||
### end Alembic commands ###
|
||||
|
|
|
@ -7,30 +7,29 @@ Create Date: 2016-06-15 09:40:30.400862
|
|||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f629c8ef4ab0'
|
||||
down_revision = '26d6a218c329'
|
||||
revision = "f629c8ef4ab0"
|
||||
down_revision = "26d6a218c329"
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# Snapshot of the person table
|
||||
person_helper = sa.Table(
|
||||
'person', sa.MetaData(),
|
||||
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.Column('activated', sa.Boolean(), nullable=True),
|
||||
sa.Column('weight', sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['project.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
"person",
|
||||
sa.MetaData(),
|
||||
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.Column("activated", sa.Boolean(), nullable=True),
|
||||
sa.Column("weight", sa.Float(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["project_id"], ["project.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
person_helper.update()
|
||||
.where(person_helper.c.weight == None)
|
||||
.values(weight=1)
|
||||
person_helper.update().where(person_helper.c.weight == None).values(weight=1)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -6,8 +6,12 @@ from flask import g, current_app
|
|||
|
||||
from debts import settle
|
||||
from sqlalchemy import orm
|
||||
from itsdangerous import (TimedJSONWebSignatureSerializer, URLSafeSerializer,
|
||||
BadSignature, SignatureExpired)
|
||||
from itsdangerous import (
|
||||
TimedJSONWebSignatureSerializer,
|
||||
URLSafeSerializer,
|
||||
BadSignature,
|
||||
SignatureExpired,
|
||||
)
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
@ -33,8 +37,8 @@ class Project(db.Model):
|
|||
balance = self.balance
|
||||
for member in self.members:
|
||||
member_obj = member._to_serialize
|
||||
member_obj['balance'] = balance.get(member.id, 0)
|
||||
obj['members'].append(member_obj)
|
||||
member_obj["balance"] = balance.get(member.id, 0)
|
||||
obj["members"].append(member_obj)
|
||||
|
||||
return obj
|
||||
|
||||
|
@ -45,14 +49,14 @@ class Project(db.Model):
|
|||
@property
|
||||
def balance(self):
|
||||
|
||||
balances, should_pay, should_receive = (defaultdict(int)
|
||||
for time in (1, 2, 3))
|
||||
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
|
||||
|
||||
# for each person
|
||||
for person in self.members:
|
||||
# get the list of bills he has to pay
|
||||
bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter(
|
||||
Bill.owers.contains(person))
|
||||
Bill.owers.contains(person)
|
||||
)
|
||||
for bill in bills.all():
|
||||
if person != bill.payer:
|
||||
share = bill.pay_each() * person.weight
|
||||
|
@ -72,18 +76,23 @@ class Project(db.Model):
|
|||
:return: one stat dict per member
|
||||
:rtype list:
|
||||
"""
|
||||
return [{
|
||||
'member': member,
|
||||
'paid': sum([
|
||||
bill.amount
|
||||
for bill in self.get_member_bills(member.id).all()
|
||||
]),
|
||||
'spent': sum([
|
||||
bill.pay_each() * member.weight
|
||||
for bill in self.get_bills().all() if member in bill.owers
|
||||
]),
|
||||
'balance': self.balance[member.id]
|
||||
} for member in self.active_members]
|
||||
return [
|
||||
{
|
||||
"member": member,
|
||||
"paid": sum(
|
||||
[bill.amount for bill in self.get_member_bills(member.id).all()]
|
||||
),
|
||||
"spent": sum(
|
||||
[
|
||||
bill.pay_each() * member.weight
|
||||
for bill in self.get_bills().all()
|
||||
if member in bill.owers
|
||||
]
|
||||
),
|
||||
"balance": self.balance[member.id],
|
||||
}
|
||||
for member in self.active_members
|
||||
]
|
||||
|
||||
@property
|
||||
def uses_weights(self):
|
||||
|
@ -99,22 +108,27 @@ class Project(db.Model):
|
|||
return transactions
|
||||
pretty_transactions = []
|
||||
for transaction in transactions:
|
||||
pretty_transactions.append({
|
||||
'ower': transaction['ower'].name,
|
||||
'receiver': transaction['receiver'].name,
|
||||
'amount': round(transaction['amount'], 2)
|
||||
})
|
||||
pretty_transactions.append(
|
||||
{
|
||||
"ower": transaction["ower"].name,
|
||||
"receiver": transaction["receiver"].name,
|
||||
"amount": round(transaction["amount"], 2),
|
||||
}
|
||||
)
|
||||
return pretty_transactions
|
||||
|
||||
# cache value for better performance
|
||||
members = {person.id: person for person in self.members}
|
||||
settle_plan = settle(self.balance.items()) or []
|
||||
|
||||
transactions = [{
|
||||
'ower': members[ower_id],
|
||||
'receiver': members[receiver_id],
|
||||
'amount': amount
|
||||
} for ower_id, amount, receiver_id in settle_plan]
|
||||
transactions = [
|
||||
{
|
||||
"ower": members[ower_id],
|
||||
"receiver": members[receiver_id],
|
||||
"amount": amount,
|
||||
}
|
||||
for ower_id, amount, receiver_id in settle_plan
|
||||
]
|
||||
|
||||
return prettify(transactions, pretty_output)
|
||||
|
||||
|
@ -140,23 +154,27 @@ class Project(db.Model):
|
|||
|
||||
def get_bills(self):
|
||||
"""Return the list of bills related to this project"""
|
||||
return Bill.query.join(Person, Project)\
|
||||
.filter(Bill.payer_id == Person.id)\
|
||||
.filter(Person.project_id == Project.id)\
|
||||
.filter(Project.id == self.id)\
|
||||
.order_by(Bill.date.desc())\
|
||||
.order_by(Bill.creation_date.desc())\
|
||||
return (
|
||||
Bill.query.join(Person, Project)
|
||||
.filter(Bill.payer_id == Person.id)
|
||||
.filter(Person.project_id == Project.id)
|
||||
.filter(Project.id == self.id)
|
||||
.order_by(Bill.date.desc())
|
||||
.order_by(Bill.creation_date.desc())
|
||||
.order_by(Bill.id.desc())
|
||||
)
|
||||
|
||||
def get_member_bills(self, member_id):
|
||||
"""Return the list of bills related to a specific member"""
|
||||
return Bill.query.join(Person, Project)\
|
||||
.filter(Bill.payer_id == Person.id)\
|
||||
.filter(Person.project_id == Project.id)\
|
||||
.filter(Person.id == member_id)\
|
||||
.filter(Project.id == self.id)\
|
||||
.order_by(Bill.date.desc())\
|
||||
return (
|
||||
Bill.query.join(Person, Project)
|
||||
.filter(Bill.payer_id == Person.id)
|
||||
.filter(Person.project_id == Project.id)
|
||||
.filter(Person.id == member_id)
|
||||
.filter(Project.id == self.id)
|
||||
.order_by(Bill.date.desc())
|
||||
.order_by(Bill.id.desc())
|
||||
)
|
||||
|
||||
def get_pretty_bills(self, export_format="json"):
|
||||
"""Return a list of project's bills with pretty formatting"""
|
||||
|
@ -166,16 +184,18 @@ class Project(db.Model):
|
|||
if export_format == "json":
|
||||
owers = [ower.name for ower in bill.owers]
|
||||
else:
|
||||
owers = ', '.join([ower.name for ower in bill.owers])
|
||||
owers = ", ".join([ower.name for ower in bill.owers])
|
||||
|
||||
pretty_bills.append({
|
||||
"what": bill.what,
|
||||
"amount": round(bill.amount, 2),
|
||||
"date": str(bill.date),
|
||||
"payer_name": Person.query.get(bill.payer_id).name,
|
||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||
"owers": owers
|
||||
})
|
||||
pretty_bills.append(
|
||||
{
|
||||
"what": bill.what,
|
||||
"amount": round(bill.amount, 2),
|
||||
"date": str(bill.date),
|
||||
"payer_name": Person.query.get(bill.payer_id).name,
|
||||
"payer_weight": Person.query.get(bill.payer_id).weight,
|
||||
"owers": owers,
|
||||
}
|
||||
)
|
||||
return pretty_bills
|
||||
|
||||
def remove_member(self, member_id):
|
||||
|
@ -210,12 +230,12 @@ class Project(db.Model):
|
|||
"""
|
||||
if expiration:
|
||||
serializer = TimedJSONWebSignatureSerializer(
|
||||
current_app.config['SECRET_KEY'],
|
||||
expiration)
|
||||
token = serializer.dumps({'project_id': self.id}).decode('utf-8')
|
||||
current_app.config["SECRET_KEY"], expiration
|
||||
)
|
||||
token = serializer.dumps({"project_id": self.id}).decode("utf-8")
|
||||
else:
|
||||
serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
|
||||
token = serializer.dumps({'project_id': self.id})
|
||||
serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
|
||||
token = serializer.dumps({"project_id": self.id})
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
|
@ -226,34 +246,40 @@ class Project(db.Model):
|
|||
:param token: Serialized TimedJsonWebToken
|
||||
"""
|
||||
if token_type == "timed_token":
|
||||
serializer = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
|
||||
serializer = TimedJSONWebSignatureSerializer(
|
||||
current_app.config["SECRET_KEY"]
|
||||
)
|
||||
else:
|
||||
serializer = URLSafeSerializer(current_app.config['SECRET_KEY'])
|
||||
serializer = URLSafeSerializer(current_app.config["SECRET_KEY"])
|
||||
try:
|
||||
data = serializer.loads(token)
|
||||
except SignatureExpired:
|
||||
return None
|
||||
except BadSignature:
|
||||
return None
|
||||
return data['project_id']
|
||||
return data["project_id"]
|
||||
|
||||
def __repr__(self):
|
||||
return "<Project %s>" % self.name
|
||||
|
||||
|
||||
class Person(db.Model):
|
||||
|
||||
class PersonQuery(BaseQuery):
|
||||
|
||||
def get_by_name(self, name, project):
|
||||
return Person.query.filter(Person.name == name)\
|
||||
.filter(Project.id == project.id).one()
|
||||
return (
|
||||
Person.query.filter(Person.name == name)
|
||||
.filter(Project.id == project.id)
|
||||
.one()
|
||||
)
|
||||
|
||||
def get(self, id, project=None):
|
||||
if not project:
|
||||
project = g.project
|
||||
return Person.query.filter(Person.id == id)\
|
||||
.filter(Project.id == project.id).one()
|
||||
return (
|
||||
Person.query.filter(Person.id == id)
|
||||
.filter(Project.id == project.id)
|
||||
.one()
|
||||
)
|
||||
|
||||
query_class = PersonQuery
|
||||
|
||||
|
@ -276,9 +302,11 @@ class Person(db.Model):
|
|||
|
||||
def has_bills(self):
|
||||
"""return if the user do have bills or not"""
|
||||
bills_as_ower_number = db.session.query(billowers)\
|
||||
.filter(billowers.columns.get("person_id") == self.id)\
|
||||
bills_as_ower_number = (
|
||||
db.session.query(billowers)
|
||||
.filter(billowers.columns.get("person_id") == self.id)
|
||||
.count()
|
||||
)
|
||||
return bills_as_ower_number != 0 or len(self.bills) != 0
|
||||
|
||||
def __str__(self):
|
||||
|
@ -290,23 +318,24 @@ class Person(db.Model):
|
|||
|
||||
# We need to manually define a join table for m2m relations
|
||||
billowers = db.Table(
|
||||
'billowers',
|
||||
db.Column('bill_id', db.Integer, db.ForeignKey('bill.id')),
|
||||
db.Column('person_id', db.Integer, db.ForeignKey('person.id')),
|
||||
"billowers",
|
||||
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id")),
|
||||
db.Column("person_id", db.Integer, db.ForeignKey("person.id")),
|
||||
)
|
||||
|
||||
|
||||
class Bill(db.Model):
|
||||
|
||||
class BillQuery(BaseQuery):
|
||||
|
||||
def get(self, project, id):
|
||||
try:
|
||||
return (self.join(Person, Project)
|
||||
.filter(Bill.payer_id == Person.id)
|
||||
.filter(Person.project_id == Project.id)
|
||||
.filter(Project.id == project.id)
|
||||
.filter(Bill.id == id).one())
|
||||
return (
|
||||
self.join(Person, Project)
|
||||
.filter(Bill.payer_id == Person.id)
|
||||
.filter(Person.project_id == Project.id)
|
||||
.filter(Project.id == project.id)
|
||||
.filter(Bill.id == id)
|
||||
.one()
|
||||
)
|
||||
except orm.exc.NoResultFound:
|
||||
return None
|
||||
|
||||
|
@ -356,7 +385,8 @@ class Bill(db.Model):
|
|||
def __repr__(self):
|
||||
return "<Bill of %s from %s for %s>" % (
|
||||
self.amount,
|
||||
self.payer, ", ".join([o.name for o in self.owers])
|
||||
self.payer,
|
||||
", ".join([o.name for o in self.owers]),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -10,8 +10,13 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|||
|
||||
from ihatemoney.api import api
|
||||
from ihatemoney.models import db
|
||||
from ihatemoney.utils import (IhmJSONEncoder, PrefixedWSGI, locale_from_iso,
|
||||
minimal_round, static_include)
|
||||
from ihatemoney.utils import (
|
||||
IhmJSONEncoder,
|
||||
PrefixedWSGI,
|
||||
locale_from_iso,
|
||||
minimal_round,
|
||||
static_include,
|
||||
)
|
||||
from ihatemoney.web import main as web_interface
|
||||
|
||||
from ihatemoney import default_settings
|
||||
|
@ -24,27 +29,27 @@ def setup_database(app):
|
|||
""" Checks if we are migrating from a pre-alembic ihatemoney
|
||||
"""
|
||||
con = db.engine.connect()
|
||||
tables_exist = db.engine.dialect.has_table(con, 'project')
|
||||
alembic_setup = db.engine.dialect.has_table(con, 'alembic_version')
|
||||
tables_exist = db.engine.dialect.has_table(con, "project")
|
||||
alembic_setup = db.engine.dialect.has_table(con, "alembic_version")
|
||||
return tables_exist and not alembic_setup
|
||||
|
||||
sqlalchemy_url = app.config.get('SQLALCHEMY_DATABASE_URI')
|
||||
if sqlalchemy_url.startswith('sqlite:////tmp'):
|
||||
sqlalchemy_url = app.config.get("SQLALCHEMY_DATABASE_URI")
|
||||
if sqlalchemy_url.startswith("sqlite:////tmp"):
|
||||
warnings.warn(
|
||||
'The database is currently stored in /tmp and might be lost at '
|
||||
'next reboot.'
|
||||
"The database is currently stored in /tmp and might be lost at "
|
||||
"next reboot."
|
||||
)
|
||||
|
||||
db.init_app(app)
|
||||
db.app = app
|
||||
|
||||
Migrate(app, db)
|
||||
migrations_path = os.path.join(app.root_path, 'migrations')
|
||||
migrations_path = os.path.join(app.root_path, "migrations")
|
||||
|
||||
if _pre_alembic_db():
|
||||
with app.app_context():
|
||||
# fake the first migration
|
||||
stamp(migrations_path, revision='b9a10d5d63ce')
|
||||
stamp(migrations_path, revision="b9a10d5d63ce")
|
||||
|
||||
# auto-execute migrations on runtime
|
||||
with app.app_context():
|
||||
|
@ -60,38 +65,38 @@ def load_configuration(app, configuration=None):
|
|||
- Otherwise, load the default settings.
|
||||
"""
|
||||
|
||||
env_var_config = os.environ.get('IHATEMONEY_SETTINGS_FILE_PATH')
|
||||
app.config.from_object('ihatemoney.default_settings')
|
||||
env_var_config = os.environ.get("IHATEMONEY_SETTINGS_FILE_PATH")
|
||||
app.config.from_object("ihatemoney.default_settings")
|
||||
if configuration:
|
||||
app.config.from_object(configuration)
|
||||
elif env_var_config:
|
||||
app.config.from_pyfile(env_var_config)
|
||||
else:
|
||||
app.config.from_pyfile('ihatemoney.cfg', silent=True)
|
||||
app.config.from_pyfile("ihatemoney.cfg", silent=True)
|
||||
# Configure custom JSONEncoder used by the API
|
||||
app.config['RESTFUL_JSON'] = {'cls': IhmJSONEncoder}
|
||||
app.config["RESTFUL_JSON"] = {"cls": IhmJSONEncoder}
|
||||
|
||||
|
||||
def validate_configuration(app):
|
||||
|
||||
if app.config['SECRET_KEY'] == default_settings.SECRET_KEY:
|
||||
if app.config["SECRET_KEY"] == default_settings.SECRET_KEY:
|
||||
warnings.warn(
|
||||
"Running a server without changing the SECRET_KEY can lead to"
|
||||
+ " user impersonation. Please update your configuration file.",
|
||||
UserWarning
|
||||
UserWarning,
|
||||
)
|
||||
# Deprecations
|
||||
if 'DEFAULT_MAIL_SENDER' in app.config:
|
||||
if "DEFAULT_MAIL_SENDER" in app.config:
|
||||
# Since flask-mail 0.8
|
||||
warnings.warn(
|
||||
"DEFAULT_MAIL_SENDER is deprecated in favor of MAIL_DEFAULT_SENDER"
|
||||
+ " and will be removed in further version",
|
||||
UserWarning
|
||||
UserWarning,
|
||||
)
|
||||
if 'MAIL_DEFAULT_SENDER' not in app.config:
|
||||
app.config['MAIL_DEFAULT_SENDER'] = default_settings.DEFAULT_MAIL_SENDER
|
||||
if "MAIL_DEFAULT_SENDER" not in app.config:
|
||||
app.config["MAIL_DEFAULT_SENDER"] = default_settings.DEFAULT_MAIL_SENDER
|
||||
|
||||
if "pbkdf2:" not in app.config['ADMIN_PASSWORD'] and app.config['ADMIN_PASSWORD']:
|
||||
if "pbkdf2:" not in app.config["ADMIN_PASSWORD"] and app.config["ADMIN_PASSWORD"]:
|
||||
# Since 2.0
|
||||
warnings.warn(
|
||||
"The way Ihatemoney stores your ADMIN_PASSWORD has changed. You are using an unhashed"
|
||||
|
@ -99,20 +104,22 @@ def validate_configuration(app):
|
|||
+ " endpoints. Please use the command 'ihatemoney generate_password_hash'"
|
||||
+ " to generate a proper password HASH and copy the output to the value of"
|
||||
+ " ADMIN_PASSWORD in your settings file.",
|
||||
UserWarning
|
||||
UserWarning,
|
||||
)
|
||||
|
||||
|
||||
def page_not_found(e):
|
||||
return render_template('404.html', root="main"), 404
|
||||
return render_template("404.html", root="main"), 404
|
||||
|
||||
|
||||
def create_app(configuration=None, instance_path='/etc/ihatemoney',
|
||||
instance_relative_config=True):
|
||||
def create_app(
|
||||
configuration=None, instance_path="/etc/ihatemoney", instance_relative_config=True
|
||||
):
|
||||
app = Flask(
|
||||
__name__,
|
||||
instance_path=instance_path,
|
||||
instance_relative_config=instance_relative_config)
|
||||
instance_relative_config=instance_relative_config,
|
||||
)
|
||||
|
||||
# If a configuration object is passed, use it. Otherwise try to find one.
|
||||
load_configuration(app, configuration)
|
||||
|
@ -136,9 +143,9 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
|
|||
app.mail = mail
|
||||
|
||||
# Jinja filters
|
||||
app.jinja_env.globals['static_include'] = static_include
|
||||
app.jinja_env.globals['locale_from_iso'] = locale_from_iso
|
||||
app.jinja_env.filters['minimal_round'] = minimal_round
|
||||
app.jinja_env.globals["static_include"] = static_include
|
||||
app.jinja_env.globals["locale_from_iso"] = locale_from_iso
|
||||
app.jinja_env.filters["minimal_round"] = minimal_round
|
||||
|
||||
# Translations
|
||||
babel = Babel(app)
|
||||
|
@ -148,10 +155,10 @@ def create_app(configuration=None, instance_path='/etc/ihatemoney',
|
|||
# get the lang from the session if defined, fallback on the browser "accept
|
||||
# languages" header.
|
||||
lang = session.get(
|
||||
'lang',
|
||||
request.accept_languages.best_match(app.config['SUPPORTED_LANGUAGES'])
|
||||
"lang",
|
||||
request.accept_languages.best_match(app.config["SUPPORTED_LANGUAGES"]),
|
||||
)
|
||||
setattr(g, 'lang', lang)
|
||||
setattr(g, "lang", lang)
|
||||
return lang
|
||||
|
||||
return app
|
||||
|
@ -162,5 +169,5 @@ def main():
|
|||
app.run(host="0.0.0.0", debug=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,8 +11,18 @@ and `add_project_id` for a quick overview)
|
|||
|
||||
import os
|
||||
from flask import (
|
||||
abort, Blueprint, current_app, flash, g, redirect, render_template, request,
|
||||
session, url_for, send_file, send_from_directory
|
||||
abort,
|
||||
Blueprint,
|
||||
current_app,
|
||||
flash,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
session,
|
||||
url_for,
|
||||
send_file,
|
||||
send_from_directory,
|
||||
)
|
||||
from flask_mail import Message
|
||||
from flask_babel import get_locale, gettext as _
|
||||
|
@ -24,10 +34,22 @@ from functools import wraps
|
|||
|
||||
from ihatemoney.models import db, Project, Person, Bill
|
||||
from ihatemoney.forms import (
|
||||
AdminAuthenticationForm, AuthenticationForm, EditProjectForm,
|
||||
InviteForm, MemberForm, PasswordReminder, ResetPasswordForm, ProjectForm, get_billform_for
|
||||
AdminAuthenticationForm,
|
||||
AuthenticationForm,
|
||||
EditProjectForm,
|
||||
InviteForm,
|
||||
MemberForm,
|
||||
PasswordReminder,
|
||||
ResetPasswordForm,
|
||||
ProjectForm,
|
||||
get_billform_for,
|
||||
)
|
||||
from ihatemoney.utils import (
|
||||
Redirect303,
|
||||
list_of_dicts2json,
|
||||
list_of_dicts2csv,
|
||||
LoginThrottler,
|
||||
)
|
||||
from ihatemoney.utils import Redirect303, list_of_dicts2json, list_of_dicts2csv, LoginThrottler
|
||||
|
||||
main = Blueprint("main", __name__)
|
||||
|
||||
|
@ -46,17 +68,20 @@ def requires_admin(bypass=None):
|
|||
Admin authentication will be bypassed when ALLOW_PUBLIC_PROJECT_CREATION is
|
||||
set to True.
|
||||
"""
|
||||
|
||||
def check_admin(f):
|
||||
@wraps(f)
|
||||
def admin_auth(*args, **kws):
|
||||
is_admin_auth_bypassed = False
|
||||
if bypass is not None and current_app.config.get(bypass[0]) == bypass[1]:
|
||||
is_admin_auth_bypassed = True
|
||||
is_admin = session.get('is_admin')
|
||||
is_admin = session.get("is_admin")
|
||||
if is_admin or is_admin_auth_bypassed:
|
||||
return f(*args, **kws)
|
||||
raise Redirect303(url_for('.admin', goto=request.path))
|
||||
raise Redirect303(url_for(".admin", goto=request.path))
|
||||
|
||||
return admin_auth
|
||||
|
||||
return check_admin
|
||||
|
||||
|
||||
|
@ -66,10 +91,10 @@ def add_project_id(endpoint, values):
|
|||
|
||||
This is to not carry it everywhere in the templates.
|
||||
"""
|
||||
if 'project_id' in values or not hasattr(g, 'project'):
|
||||
if "project_id" in values or not hasattr(g, "project"):
|
||||
return
|
||||
if current_app.url_map.is_endpoint_expecting(endpoint, 'project_id'):
|
||||
values['project_id'] = g.project.id
|
||||
if current_app.url_map.is_endpoint_expecting(endpoint, "project_id"):
|
||||
values["project_id"] = g.project.id
|
||||
|
||||
|
||||
@main.url_value_preprocessor
|
||||
|
@ -97,21 +122,19 @@ def pull_project(endpoint, values):
|
|||
return
|
||||
if not values:
|
||||
values = {}
|
||||
project_id = values.pop('project_id', None)
|
||||
project_id = values.pop("project_id", None)
|
||||
if project_id:
|
||||
project = Project.query.get(project_id)
|
||||
if not project:
|
||||
raise Redirect303(url_for(".create_project",
|
||||
project_id=project_id))
|
||||
raise Redirect303(url_for(".create_project", project_id=project_id))
|
||||
|
||||
is_admin = session.get('is_admin')
|
||||
is_admin = session.get("is_admin")
|
||||
if session.get(project.id) or is_admin:
|
||||
# add project into kwargs and call the original function
|
||||
g.project = project
|
||||
else:
|
||||
# redirect to authentication page
|
||||
raise Redirect303(
|
||||
url_for(".authenticate", project_id=project_id))
|
||||
raise Redirect303(url_for(".authenticate", project_id=project_id))
|
||||
|
||||
|
||||
@main.route("/admin", methods=["GET", "POST"])
|
||||
|
@ -121,30 +144,41 @@ def admin():
|
|||
When ADMIN_PASSWORD is empty, admin authentication is deactivated.
|
||||
"""
|
||||
form = AdminAuthenticationForm()
|
||||
goto = request.args.get('goto', url_for('.home'))
|
||||
is_admin_auth_enabled = bool(current_app.config['ADMIN_PASSWORD'])
|
||||
goto = request.args.get("goto", url_for(".home"))
|
||||
is_admin_auth_enabled = bool(current_app.config["ADMIN_PASSWORD"])
|
||||
if request.method == "POST":
|
||||
client_ip = request.remote_addr
|
||||
if not login_throttler.is_login_allowed(client_ip):
|
||||
msg = _("Too many failed login attempts, please retry later.")
|
||||
form.errors['admin_password'] = [msg]
|
||||
return render_template("admin.html", form=form, admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled)
|
||||
form.errors["admin_password"] = [msg]
|
||||
return render_template(
|
||||
"admin.html",
|
||||
form=form,
|
||||
admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled,
|
||||
)
|
||||
if form.validate():
|
||||
# Valid password
|
||||
if (check_password_hash(current_app.config['ADMIN_PASSWORD'],
|
||||
form.admin_password.data)):
|
||||
session['is_admin'] = True
|
||||
if check_password_hash(
|
||||
current_app.config["ADMIN_PASSWORD"], form.admin_password.data
|
||||
):
|
||||
session["is_admin"] = True
|
||||
session.update()
|
||||
login_throttler.reset(client_ip)
|
||||
return redirect(goto)
|
||||
# Invalid password
|
||||
login_throttler.increment_attempts_counter(client_ip)
|
||||
msg = _("This admin password is not the right one. Only %(num)d attempts left.",
|
||||
num=login_throttler.get_remaining_attempts(client_ip))
|
||||
form.errors['admin_password'] = [msg]
|
||||
return render_template("admin.html", form=form, admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled)
|
||||
msg = _(
|
||||
"This admin password is not the right one. Only %(num)d attempts left.",
|
||||
num=login_throttler.get_remaining_attempts(client_ip),
|
||||
)
|
||||
form.errors["admin_password"] = [msg]
|
||||
return render_template(
|
||||
"admin.html",
|
||||
form=form,
|
||||
admin_auth=True,
|
||||
is_admin_auth_enabled=is_admin_auth_enabled,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/authenticate", methods=["GET", "POST"])
|
||||
|
@ -152,13 +186,13 @@ def authenticate(project_id=None):
|
|||
"""Authentication form"""
|
||||
form = AuthenticationForm()
|
||||
# Try to get project_id from token first
|
||||
token = request.args.get('token')
|
||||
token = request.args.get("token")
|
||||
if token:
|
||||
project_id = Project.verify_token(token, token_type='non_timed_token')
|
||||
project_id = Project.verify_token(token, token_type="non_timed_token")
|
||||
token_auth = True
|
||||
else:
|
||||
if not form.id.data and request.args.get('project_id'):
|
||||
form.id.data = request.args['project_id']
|
||||
if not form.id.data and request.args.get("project_id"):
|
||||
form.id.data = request.args["project_id"]
|
||||
project_id = form.id.data
|
||||
token_auth = False
|
||||
if project_id is None:
|
||||
|
@ -172,16 +206,22 @@ def authenticate(project_id=None):
|
|||
if not project:
|
||||
# If the user try to connect to an unexisting project, we will
|
||||
# propose him a link to the creation form.
|
||||
return render_template("authenticate.html", form=form, create_project=project_id)
|
||||
return render_template(
|
||||
"authenticate.html", form=form, create_project=project_id
|
||||
)
|
||||
|
||||
# if credentials are already in session, redirect
|
||||
if session.get(project_id):
|
||||
setattr(g, 'project', project)
|
||||
setattr(g, "project", project)
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
# else do form authentication or token authentication
|
||||
is_post_auth = request.method == "POST" and form.validate()
|
||||
if is_post_auth and check_password_hash(project.password, form.password.data) or token_auth:
|
||||
if (
|
||||
is_post_auth
|
||||
and check_password_hash(project.password, form.password.data)
|
||||
or token_auth
|
||||
):
|
||||
# maintain a list of visited projects
|
||||
if "projects" not in session:
|
||||
session["projects"] = []
|
||||
|
@ -189,11 +229,11 @@ def authenticate(project_id=None):
|
|||
session["projects"].insert(0, (project_id, project.name))
|
||||
session[project_id] = True
|
||||
session.update()
|
||||
setattr(g, 'project', project)
|
||||
setattr(g, "project", project)
|
||||
return redirect(url_for(".list_bills"))
|
||||
if is_post_auth and not check_password_hash(project.password, form.password.data):
|
||||
msg = _("This private code is not the right one")
|
||||
form.errors['password'] = [msg]
|
||||
form.errors["password"] = [msg]
|
||||
|
||||
return render_template("authenticate.html", form=form)
|
||||
|
||||
|
@ -202,21 +242,27 @@ def authenticate(project_id=None):
|
|||
def home():
|
||||
project_form = ProjectForm()
|
||||
auth_form = AuthenticationForm()
|
||||
is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
|
||||
is_public_project_creation_allowed = current_app.config['ALLOW_PUBLIC_PROJECT_CREATION']
|
||||
is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
|
||||
is_public_project_creation_allowed = current_app.config[
|
||||
"ALLOW_PUBLIC_PROJECT_CREATION"
|
||||
]
|
||||
|
||||
return render_template("home.html", project_form=project_form,
|
||||
is_demo_project_activated=is_demo_project_activated,
|
||||
is_public_project_creation_allowed=is_public_project_creation_allowed,
|
||||
auth_form=auth_form, session=session)
|
||||
return render_template(
|
||||
"home.html",
|
||||
project_form=project_form,
|
||||
is_demo_project_activated=is_demo_project_activated,
|
||||
is_public_project_creation_allowed=is_public_project_creation_allowed,
|
||||
auth_form=auth_form,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@main.route("/create", methods=["GET", "POST"])
|
||||
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
|
||||
def create_project():
|
||||
form = ProjectForm()
|
||||
if request.method == "GET" and 'project_id' in request.values:
|
||||
form.name.data = request.values['project_id']
|
||||
if request.method == "GET" and "project_id" in request.values:
|
||||
form.name.data = request.values["project_id"]
|
||||
|
||||
if request.method == "POST":
|
||||
# At first, we don't want the user to bother with the identifier
|
||||
|
@ -239,26 +285,34 @@ def create_project():
|
|||
# send reminder email
|
||||
g.project = project
|
||||
|
||||
message_title = _("You have just created '%(project)s' "
|
||||
"to share your expenses", project=g.project.name)
|
||||
message_title = _(
|
||||
"You have just created '%(project)s' " "to share your expenses",
|
||||
project=g.project.name,
|
||||
)
|
||||
|
||||
message_body = render_template("reminder_mail.%s.j2" %
|
||||
get_locale().language)
|
||||
message_body = render_template(
|
||||
"reminder_mail.%s.j2" % get_locale().language
|
||||
)
|
||||
|
||||
msg = Message(message_title,
|
||||
body=message_body,
|
||||
recipients=[project.contact_email])
|
||||
msg = Message(
|
||||
message_title, body=message_body, recipients=[project.contact_email]
|
||||
)
|
||||
try:
|
||||
current_app.mail.send(msg)
|
||||
except SMTPRecipientsRefused:
|
||||
msg_compl = 'Problem sending mail. '
|
||||
msg_compl = "Problem sending mail. "
|
||||
# TODO: destroy the project and cancel instead?
|
||||
else:
|
||||
msg_compl = ''
|
||||
msg_compl = ""
|
||||
|
||||
# redirect the user to the next step (invite)
|
||||
flash(_("%(msg_compl)sThe project identifier is %(project)s",
|
||||
msg_compl=msg_compl, project=project.id))
|
||||
flash(
|
||||
_(
|
||||
"%(msg_compl)sThe project identifier is %(project)s",
|
||||
msg_compl=msg_compl,
|
||||
project=project.id,
|
||||
)
|
||||
)
|
||||
return redirect(url_for(".list_bills", project_id=project.id))
|
||||
|
||||
return render_template("create_project.html", form=form)
|
||||
|
@ -273,10 +327,13 @@ def remind_password():
|
|||
project = Project.query.get(form.id.data)
|
||||
# send a link to reset the password
|
||||
password_reminder = "password_reminder.%s.j2" % get_locale().language
|
||||
current_app.mail.send(Message(
|
||||
"password recovery",
|
||||
body=render_template(password_reminder, project=project),
|
||||
recipients=[project.contact_email]))
|
||||
current_app.mail.send(
|
||||
Message(
|
||||
"password recovery",
|
||||
body=render_template(password_reminder, project=project),
|
||||
recipients=[project.contact_email],
|
||||
)
|
||||
)
|
||||
return redirect(url_for(".password_reminder_sent"))
|
||||
|
||||
return render_template("password_reminder.html", form=form)
|
||||
|
@ -287,18 +344,24 @@ def password_reminder_sent():
|
|||
return render_template("password_reminder_sent.html")
|
||||
|
||||
|
||||
@main.route('/reset-password', methods=['GET', 'POST'])
|
||||
@main.route("/reset-password", methods=["GET", "POST"])
|
||||
def reset_password():
|
||||
form = ResetPasswordForm()
|
||||
token = request.args.get('token')
|
||||
token = request.args.get("token")
|
||||
if not token:
|
||||
return render_template('reset_password.html', form=form, error=_("No token provided"))
|
||||
return render_template(
|
||||
"reset_password.html", form=form, error=_("No token provided")
|
||||
)
|
||||
project_id = Project.verify_token(token)
|
||||
if not project_id:
|
||||
return render_template('reset_password.html', form=form, error=_("Invalid token"))
|
||||
return render_template(
|
||||
"reset_password.html", form=form, error=_("Invalid token")
|
||||
)
|
||||
project = Project.query.get(project_id)
|
||||
if not project:
|
||||
return render_template('reset_password.html', form=form, error=_("Unknown project"))
|
||||
return render_template(
|
||||
"reset_password.html", form=form, error=_("Unknown project")
|
||||
)
|
||||
|
||||
if request.method == "POST" and form.validate():
|
||||
project.password = generate_password_hash(form.password.data)
|
||||
|
@ -306,7 +369,7 @@ def reset_password():
|
|||
db.session.commit()
|
||||
flash(_("Password successfully reset."))
|
||||
return redirect(url_for(".home"))
|
||||
return render_template('reset_password.html', form=form)
|
||||
return render_template("reset_password.html", form=form)
|
||||
|
||||
|
||||
@main.route("/<project_id>/edit", methods=["GET", "POST"])
|
||||
|
@ -324,40 +387,38 @@ def edit_project():
|
|||
edit_form.contact_email.data = g.project.contact_email
|
||||
|
||||
return render_template(
|
||||
"edit_project.html",
|
||||
edit_form=edit_form,
|
||||
current_view="edit_project"
|
||||
"edit_project.html", edit_form=edit_form, current_view="edit_project"
|
||||
)
|
||||
|
||||
|
||||
@main.route("/<project_id>/delete")
|
||||
def delete_project():
|
||||
g.project.remove_project()
|
||||
flash(_('Project successfully deleted'))
|
||||
flash(_("Project successfully deleted"))
|
||||
|
||||
return redirect(request.headers.get('Referer') or url_for('.home'))
|
||||
return redirect(request.headers.get("Referer") or url_for(".home"))
|
||||
|
||||
|
||||
@main.route("/<project_id>/export/<string:file>.<string:format>")
|
||||
def export_project(file, format):
|
||||
if file == 'transactions':
|
||||
if file == "transactions":
|
||||
export = g.project.get_transactions_to_settle_bill(pretty_output=True)
|
||||
elif file == "bills":
|
||||
export = g.project.get_pretty_bills(export_format=format)
|
||||
else:
|
||||
abort(404, 'No such export type')
|
||||
abort(404, "No such export type")
|
||||
|
||||
if format == "json":
|
||||
file2export = list_of_dicts2json(export)
|
||||
elif format == "csv":
|
||||
file2export = list_of_dicts2csv(export)
|
||||
else:
|
||||
abort(404, 'No such export format')
|
||||
abort(404, "No such export format")
|
||||
|
||||
return send_file(
|
||||
file2export,
|
||||
attachment_filename="%s-%s.%s" % (g.project.id, file, format),
|
||||
as_attachment=True
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -377,16 +438,18 @@ def demo():
|
|||
Create a demo project if it doesn't exists yet (or has been deleted)
|
||||
If the demo project is deactivated, one is redirected to the create project form
|
||||
"""
|
||||
is_demo_project_activated = current_app.config['ACTIVATE_DEMO_PROJECT']
|
||||
is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"]
|
||||
project = Project.query.get("demo")
|
||||
|
||||
if not project and not is_demo_project_activated:
|
||||
raise Redirect303(url_for(".create_project",
|
||||
project_id='demo'))
|
||||
raise Redirect303(url_for(".create_project", project_id="demo"))
|
||||
if not project and is_demo_project_activated:
|
||||
project = Project(id="demo", name=u"demonstration",
|
||||
password=generate_password_hash("demo"),
|
||||
contact_email="demo@notmyidea.org")
|
||||
project = Project(
|
||||
id="demo",
|
||||
name="demonstration",
|
||||
password=generate_password_hash("demo"),
|
||||
contact_email="demo@notmyidea.org",
|
||||
)
|
||||
db.session.add(project)
|
||||
db.session.commit()
|
||||
session[project.id] = True
|
||||
|
@ -403,15 +466,19 @@ def invite():
|
|||
if form.validate():
|
||||
# send the email
|
||||
|
||||
message_body = render_template("invitation_mail.%s.j2" %
|
||||
get_locale().language)
|
||||
message_body = render_template(
|
||||
"invitation_mail.%s.j2" % get_locale().language
|
||||
)
|
||||
|
||||
message_title = _("You have been invited to share your "
|
||||
"expenses for %(project)s", project=g.project.name)
|
||||
msg = Message(message_title,
|
||||
body=message_body,
|
||||
recipients=[email.strip()
|
||||
for email in form.emails.data.split(",")])
|
||||
message_title = _(
|
||||
"You have been invited to share your " "expenses for %(project)s",
|
||||
project=g.project.name,
|
||||
)
|
||||
msg = Message(
|
||||
message_title,
|
||||
body=message_body,
|
||||
recipients=[email.strip() for email in form.emails.data.split(",")],
|
||||
)
|
||||
current_app.mail.send(msg)
|
||||
flash(_("Your invitations have been sent"))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
@ -423,17 +490,19 @@ def invite():
|
|||
def list_bills():
|
||||
bill_form = get_billform_for(g.project)
|
||||
# set the last selected payer as default choice if exists
|
||||
if 'last_selected_payer' in session:
|
||||
bill_form.payer.data = session['last_selected_payer']
|
||||
if "last_selected_payer" in session:
|
||||
bill_form.payer.data = session["last_selected_payer"]
|
||||
# Preload the "owers" relationship for all bills
|
||||
bills = g.project.get_bills().options(orm.subqueryload(Bill.owers))
|
||||
|
||||
return render_template("list_bills.html",
|
||||
bills=bills, member_form=MemberForm(g.project),
|
||||
bill_form=bill_form,
|
||||
add_bill=request.values.get('add_bill', False),
|
||||
current_view="list_bills",
|
||||
)
|
||||
return render_template(
|
||||
"list_bills.html",
|
||||
bills=bills,
|
||||
member_form=MemberForm(g.project),
|
||||
bill_form=bill_form,
|
||||
add_bill=request.values.get("add_bill", False),
|
||||
current_view="list_bills",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/<project_id>/members/add", methods=["GET", "POST"])
|
||||
|
@ -452,8 +521,11 @@ def add_member():
|
|||
|
||||
@main.route("/<project_id>/members/<member_id>/reactivate", methods=["POST"])
|
||||
def reactivate(member_id):
|
||||
person = Person.query.filter(Person.id == member_id)\
|
||||
.filter(Project.id == g.project.id).all()
|
||||
person = (
|
||||
Person.query.filter(Person.id == member_id)
|
||||
.filter(Project.id == g.project.id)
|
||||
.all()
|
||||
)
|
||||
if person:
|
||||
person[0].activated = True
|
||||
db.session.commit()
|
||||
|
@ -466,23 +538,27 @@ def remove_member(member_id):
|
|||
member = g.project.remove_member(member_id)
|
||||
if member:
|
||||
if not member.activated:
|
||||
flash(_("User '%(name)s' has been deactivated. It will still "
|
||||
flash(
|
||||
_(
|
||||
"User '%(name)s' has been deactivated. It will still "
|
||||
"appear in the users list until its balance "
|
||||
"becomes zero.", name=member.name))
|
||||
"becomes zero.",
|
||||
name=member.name,
|
||||
)
|
||||
)
|
||||
else:
|
||||
flash(_("User '%(name)s' has been removed", name=member.name))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
|
||||
@main.route("/<project_id>/members/<member_id>/edit",
|
||||
methods=["POST", "GET"])
|
||||
@main.route("/<project_id>/members/<member_id>/edit", methods=["POST", "GET"])
|
||||
def edit_member(member_id):
|
||||
member = Person.query.get(member_id, g.project)
|
||||
if not member:
|
||||
raise NotFound()
|
||||
form = MemberForm(g.project, edit=True)
|
||||
|
||||
if request.method == 'POST' and form.validate():
|
||||
if request.method == "POST" and form.validate():
|
||||
form.save(g.project, member)
|
||||
db.session.commit()
|
||||
flash(_("User '%(name)s' has been edited", name=member.name))
|
||||
|
@ -495,10 +571,10 @@ def edit_member(member_id):
|
|||
@main.route("/<project_id>/add", methods=["GET", "POST"])
|
||||
def add_bill():
|
||||
form = get_billform_for(g.project)
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
if form.validate():
|
||||
# save last selected payer in session
|
||||
session['last_selected_payer'] = form.payer.data
|
||||
session["last_selected_payer"] = form.payer.data
|
||||
session.update()
|
||||
|
||||
bill = Bill()
|
||||
|
@ -509,9 +585,9 @@ def add_bill():
|
|||
|
||||
args = {}
|
||||
if form.submit2.data:
|
||||
args['add_bill'] = True
|
||||
args["add_bill"] = True
|
||||
|
||||
return redirect(url_for('.list_bills', **args))
|
||||
return redirect(url_for(".list_bills", **args))
|
||||
|
||||
return render_template("add_bill.html", form=form)
|
||||
|
||||
|
@ -521,13 +597,13 @@ def delete_bill(bill_id):
|
|||
# fixme: everyone is able to delete a bill
|
||||
bill = Bill.query.get(g.project, bill_id)
|
||||
if not bill:
|
||||
return redirect(url_for('.list_bills'))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
db.session.delete(bill)
|
||||
db.session.commit()
|
||||
flash(_("The bill has been deleted"))
|
||||
|
||||
return redirect(url_for('.list_bills'))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
|
||||
@main.route("/<project_id>/edit/<int:bill_id>", methods=["GET", "POST"])
|
||||
|
@ -539,12 +615,12 @@ def edit_bill(bill_id):
|
|||
|
||||
form = get_billform_for(g.project, set_default=False)
|
||||
|
||||
if request.method == 'POST' and form.validate():
|
||||
if request.method == "POST" and form.validate():
|
||||
form.save(bill, g.project)
|
||||
db.session.commit()
|
||||
|
||||
flash(_("The bill has been modified"))
|
||||
return redirect(url_for('.list_bills'))
|
||||
return redirect(url_for(".list_bills"))
|
||||
|
||||
if not form.errors:
|
||||
form.fill(bill)
|
||||
|
@ -554,21 +630,17 @@ def edit_bill(bill_id):
|
|||
|
||||
@main.route("/lang/<lang>")
|
||||
def change_lang(lang):
|
||||
session['lang'] = lang
|
||||
session["lang"] = lang
|
||||
session.update()
|
||||
|
||||
return redirect(request.headers.get('Referer') or url_for('.home'))
|
||||
return redirect(request.headers.get("Referer") or url_for(".home"))
|
||||
|
||||
|
||||
@main.route("/<project_id>/settle_bills")
|
||||
def settle_bill():
|
||||
"""Compute the sum each one have to pay to each other and display it"""
|
||||
bills = g.project.get_transactions_to_settle_bill()
|
||||
return render_template(
|
||||
"settle_bills.html",
|
||||
bills=bills,
|
||||
current_view='settle_bill',
|
||||
)
|
||||
return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
|
||||
|
||||
|
||||
@main.route("/<project_id>/statistics")
|
||||
|
@ -577,22 +649,25 @@ def statistics():
|
|||
return render_template(
|
||||
"statistics.html",
|
||||
members_stats=g.project.members_stats,
|
||||
current_view='statistics',
|
||||
current_view="statistics",
|
||||
)
|
||||
|
||||
|
||||
@main.route("/dashboard")
|
||||
@requires_admin()
|
||||
def dashboard():
|
||||
is_admin_dashboard_activated = current_app.config['ACTIVATE_ADMIN_DASHBOARD']
|
||||
is_admin_dashboard_activated = current_app.config["ACTIVATE_ADMIN_DASHBOARD"]
|
||||
return render_template(
|
||||
"dashboard.html",
|
||||
projects=Project.query.all(),
|
||||
is_admin_dashboard_activated=is_admin_dashboard_activated
|
||||
is_admin_dashboard_activated=is_admin_dashboard_activated,
|
||||
)
|
||||
|
||||
|
||||
@main.route('/favicon.ico')
|
||||
@main.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_from_directory(os.path.join(main.root_path, 'static'),
|
||||
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||||
return send_from_directory(
|
||||
os.path.join(main.root_path, "static"),
|
||||
"favicon.ico",
|
||||
mimetype="image/vnd.microsoft.icon",
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue