| @ -0,0 +1,49 @@ | |||
| from flask import Flask, render_template, redirect, url_for | |||
| from flask_sqlalchemy import SQLAlchemy | |||
| from flask_migrate import Migrate | |||
| from .models import db, Vehicle, Repostaje | |||
| from .forms import VehicleForm, RepostajeForm | |||
| app = Flask(__name__) | |||
| app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///vehicles.db' | |||
| app.config['SECRET_KEY'] = 'mysecret' | |||
| db.init_app(app) | |||
| migrate = Migrate(app, db) | |||
| @app.route('/') | |||
| def index(): | |||
| vehicles = Vehicle.query.all() | |||
| repostajes = Repostaje.query.all() | |||
| return render_template('index.html', vehicles=vehicles, repostajes=repostajes) | |||
| @app.route('/add_vehicle', methods=['GET', 'POST']) | |||
| def add_vehicle(): | |||
| form = VehicleForm() | |||
| if form.validate_on_submit(): | |||
| vehicle = Vehicle(license_plate=form.license_plate.data, model=form.model.data) | |||
| db.session.add(vehicle) | |||
| db.session.commit() | |||
| return redirect(url_for('index')) | |||
| return render_template('add_vehicle.html', form=form) | |||
| @app.route('/add_repostaje', methods=['GET', 'POST']) | |||
| def add_repostaje(): | |||
| form = RepostajeForm() | |||
| form.vehicle_id.choices = [(v.id, v.license_plate) for v in Vehicle.query.all()] | |||
| if form.validate_on_submit(): | |||
| repostaje = Repostaje( | |||
| vehicle_id=form.vehicle_id.data, | |||
| amount=form.amount.data, | |||
| date=form.date.data, | |||
| kilometers=form.kilometers.data, | |||
| liters=form.liters.data, | |||
| total_amount=form.total_amount.data | |||
| ) | |||
| db.session.add(repostaje) | |||
| db.session.commit() | |||
| return redirect(url_for('index')) | |||
| return render_template('add_repostaje.html', form=form) | |||
| if __name__ == '__main__': | |||
| app.run(debug=True) | |||
| @ -0,0 +1,17 @@ | |||
| from flask_wtf import FlaskForm | |||
| from wtforms import StringField, FloatField, DateField, SelectField, SubmitField | |||
| from wtforms.validators import DataRequired | |||
| class VehicleForm(FlaskForm): | |||
| license_plate = StringField('License Plate', validators=[DataRequired()]) | |||
| model = StringField('Model', validators=[DataRequired()]) | |||
| submit = SubmitField('Submit') | |||
| class RepostajeForm(FlaskForm): | |||
| vehicle_id = SelectField('Vehicle', coerce=int, validators=[DataRequired()]) | |||
| amount = FloatField('Amount', validators=[DataRequired()]) | |||
| date = DateField('Date', format='%Y-%m-%d', validators=[DataRequired()]) | |||
| kilometers = FloatField('Kilometers', validators=[DataRequired()]) | |||
| liters = FloatField('Liters', validators=[DataRequired()]) | |||
| total_amount = FloatField('Total Amount', validators=[DataRequired()]) | |||
| submit = SubmitField('Submit') | |||
| @ -0,0 +1,4 @@ | |||
| export FLASK_APP=app.py | |||
| flask db init | |||
| flask db migrate -m "Initial migration." | |||
| flask db upgrade | |||
| @ -0,0 +1 @@ | |||
| Single-database configuration for Flask. | |||
| @ -0,0 +1,50 @@ | |||
| # A generic, single database configuration. | |||
| [alembic] | |||
| # template used to generate migration files | |||
| # file_template = %%(rev)s_%%(slug)s | |||
| # set to 'true' to run the environment during | |||
| # the 'revision' command, regardless of autogenerate | |||
| # revision_environment = false | |||
| # Logging configuration | |||
| [loggers] | |||
| keys = root,sqlalchemy,alembic,flask_migrate | |||
| [handlers] | |||
| keys = console | |||
| [formatters] | |||
| keys = generic | |||
| [logger_root] | |||
| level = WARN | |||
| handlers = console | |||
| qualname = | |||
| [logger_sqlalchemy] | |||
| level = WARN | |||
| handlers = | |||
| qualname = sqlalchemy.engine | |||
| [logger_alembic] | |||
| level = INFO | |||
| handlers = | |||
| qualname = alembic | |||
| [logger_flask_migrate] | |||
| level = INFO | |||
| handlers = | |||
| qualname = flask_migrate | |||
| [handler_console] | |||
| class = StreamHandler | |||
| args = (sys.stderr,) | |||
| level = NOTSET | |||
| formatter = generic | |||
| [formatter_generic] | |||
| format = %(levelname)-5.5s [%(name)s] %(message)s | |||
| datefmt = %H:%M:%S | |||
| @ -0,0 +1,113 @@ | |||
| import logging | |||
| from logging.config import fileConfig | |||
| from flask import current_app | |||
| from alembic import context | |||
| # this is the Alembic Config object, which provides | |||
| # access to the values within the .ini file in use. | |||
| 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') | |||
| def get_engine(): | |||
| try: | |||
| # this works with Flask-SQLAlchemy<3 and Alchemical | |||
| return current_app.extensions['migrate'].db.get_engine() | |||
| except (TypeError, AttributeError): | |||
| # this works with Flask-SQLAlchemy>=3 | |||
| return current_app.extensions['migrate'].db.engine | |||
| def get_engine_url(): | |||
| try: | |||
| return get_engine().url.render_as_string(hide_password=False).replace( | |||
| '%', '%%') | |||
| except AttributeError: | |||
| return str(get_engine().url).replace('%', '%%') | |||
| # add your model's MetaData object here | |||
| # for 'autogenerate' support | |||
| # from myapp import mymodel | |||
| # target_metadata = mymodel.Base.metadata | |||
| config.set_main_option('sqlalchemy.url', get_engine_url()) | |||
| target_db = current_app.extensions['migrate'].db | |||
| # other values from the config, defined by the needs of env.py, | |||
| # can be acquired: | |||
| # my_important_option = config.get_main_option("my_important_option") | |||
| # ... etc. | |||
| def get_metadata(): | |||
| if hasattr(target_db, 'metadatas'): | |||
| return target_db.metadatas[None] | |||
| return target_db.metadata | |||
| def run_migrations_offline(): | |||
| """Run migrations in 'offline' mode. | |||
| This configures the context with just a URL | |||
| and not an Engine, though an Engine is acceptable | |||
| here as well. By skipping the Engine creation | |||
| we don't even need a DBAPI to be available. | |||
| Calls to context.execute() here emit the given string to the | |||
| script output. | |||
| """ | |||
| url = config.get_main_option("sqlalchemy.url") | |||
| context.configure( | |||
| url=url, target_metadata=get_metadata(), literal_binds=True | |||
| ) | |||
| with context.begin_transaction(): | |||
| context.run_migrations() | |||
| def run_migrations_online(): | |||
| """Run migrations in 'online' mode. | |||
| In this scenario we need to create an Engine | |||
| and associate a connection with the context. | |||
| """ | |||
| # this callback is used to prevent an auto-migration from being generated | |||
| # when there are no changes to the schema | |||
| # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html | |||
| def process_revision_directives(context, revision, directives): | |||
| if getattr(config.cmd_opts, 'autogenerate', False): | |||
| script = directives[0] | |||
| if script.upgrade_ops.is_empty(): | |||
| directives[:] = [] | |||
| logger.info('No changes in schema detected.') | |||
| conf_args = current_app.extensions['migrate'].configure_args | |||
| if conf_args.get("process_revision_directives") is None: | |||
| conf_args["process_revision_directives"] = process_revision_directives | |||
| connectable = get_engine() | |||
| with connectable.connect() as connection: | |||
| context.configure( | |||
| connection=connection, | |||
| target_metadata=get_metadata(), | |||
| **conf_args | |||
| ) | |||
| with context.begin_transaction(): | |||
| context.run_migrations() | |||
| if context.is_offline_mode(): | |||
| run_migrations_offline() | |||
| else: | |||
| run_migrations_online() | |||
| @ -0,0 +1,24 @@ | |||
| """${message} | |||
| Revision ID: ${up_revision} | |||
| Revises: ${down_revision | comma,n} | |||
| Create Date: ${create_date} | |||
| """ | |||
| from alembic import op | |||
| import sqlalchemy as sa | |||
| ${imports if imports else ""} | |||
| # revision identifiers, used by Alembic. | |||
| revision = ${repr(up_revision)} | |||
| down_revision = ${repr(down_revision)} | |||
| branch_labels = ${repr(branch_labels)} | |||
| depends_on = ${repr(depends_on)} | |||
| def upgrade(): | |||
| ${upgrades if upgrades else "pass"} | |||
| def downgrade(): | |||
| ${downgrades if downgrades else "pass"} | |||
| @ -0,0 +1,46 @@ | |||
| """Initial migration. | |||
| Revision ID: 55bdc15a1654 | |||
| Revises: | |||
| Create Date: 2024-06-13 11:33:22.248968 | |||
| """ | |||
| from alembic import op | |||
| import sqlalchemy as sa | |||
| # revision identifiers, used by Alembic. | |||
| revision = '55bdc15a1654' | |||
| down_revision = None | |||
| branch_labels = None | |||
| depends_on = None | |||
| def upgrade(): | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| op.create_table('vehicle', | |||
| sa.Column('id', sa.Integer(), nullable=False), | |||
| sa.Column('license_plate', sa.String(length=10), nullable=False), | |||
| sa.Column('model', sa.String(length=50), nullable=False), | |||
| sa.PrimaryKeyConstraint('id'), | |||
| sa.UniqueConstraint('license_plate') | |||
| ) | |||
| op.create_table('repostaje', | |||
| sa.Column('id', sa.Integer(), nullable=False), | |||
| sa.Column('vehicle_id', sa.Integer(), nullable=False), | |||
| sa.Column('amount', sa.Float(), nullable=False), | |||
| sa.Column('date', sa.Date(), nullable=False), | |||
| sa.Column('kilometers', sa.Float(), nullable=False), | |||
| sa.Column('liters', sa.Float(), nullable=False), | |||
| sa.Column('total_amount', sa.Float(), nullable=False), | |||
| sa.ForeignKeyConstraint(['vehicle_id'], ['vehicle.id'], ), | |||
| sa.PrimaryKeyConstraint('id') | |||
| ) | |||
| # ### end Alembic commands ### | |||
| def downgrade(): | |||
| # ### commands auto generated by Alembic - please adjust! ### | |||
| op.drop_table('repostaje') | |||
| op.drop_table('vehicle') | |||
| # ### end Alembic commands ### | |||
| @ -0,0 +1,24 @@ | |||
| from flask_sqlalchemy import SQLAlchemy | |||
| db = SQLAlchemy() | |||
| class Vehicle(db.Model): | |||
| id = db.Column(db.Integer, primary_key=True) | |||
| license_plate = db.Column(db.String(10), nullable=False, unique=True) | |||
| model = db.Column(db.String(50), nullable=False) | |||
| repostajes = db.relationship('Repostaje', backref='vehicle', lazy=True) | |||
| def __repr__(self): | |||
| return f'<Vehicle {self.license_plate}>' | |||
| class Repostaje(db.Model): | |||
| id = db.Column(db.Integer, primary_key=True) | |||
| vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), nullable=False) | |||
| amount = db.Column(db.Float, nullable=False) | |||
| date = db.Column(db.Date, nullable=False) | |||
| kilometers = db.Column(db.Float, nullable=False) | |||
| liters = db.Column(db.Float, nullable=False) | |||
| total_amount = db.Column(db.Float, nullable=False) | |||
| def __repr__(self): | |||
| return f'<Repostaje {self.id}>' | |||
| @ -0,0 +1,26 @@ | |||
| {% extends "layout.html" %} | |||
| {% block content %} | |||
| <h1>Add Repostaje</h1> | |||
| <form method="POST"> | |||
| {{ form.hidden_tag() }} | |||
| <div class="form-group"> | |||
| {{ form.vehicle_id.label }} {{ form.vehicle_id(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.amount.label }} {{ form.amount(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.date.label }} {{ form.date(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.kilometers.label }} {{ form.kilometers(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.liters.label }} {{ form.liters(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.total_amount.label }} {{ form.total_amount(class_="form-control") }} | |||
| </div> | |||
| {{ form.submit(class_="btn btn-primary") }} | |||
| </form> | |||
| {% endblock %} | |||
| @ -0,0 +1,14 @@ | |||
| {% extends "layout.html" %} | |||
| {% block content %} | |||
| <h1>Add Vehicle</h1> | |||
| <form method="POST"> | |||
| {{ form.hidden_tag() }} | |||
| <div class="form-group"> | |||
| {{ form.license_plate.label }} {{ form.license_plate(class_="form-control") }} | |||
| </div> | |||
| <div class="form-group"> | |||
| {{ form.model.label }} {{ form.model(class_="form-control") }} | |||
| </div> | |||
| {{ form.submit(class_="btn btn-primary") }} | |||
| </form> | |||
| {% endblock %} | |||
| @ -0,0 +1,46 @@ | |||
| {% extends "layout.html" %} | |||
| {% block content %} | |||
| <h1>Vehicles and Repostajes</h1> | |||
| <a href="{{ url_for('add_vehicle') }}" class="btn btn-primary">Add Vehicle</a> | |||
| <a href="{{ url_for('add_repostaje') }}" class="btn btn-primary">Add Repostaje</a> | |||
| <h2>Vehicles</h2> | |||
| <table class="table"> | |||
| <thead> | |||
| <tr> | |||
| <th>ID</th> | |||
| <th>License Plate</th> | |||
| <th>Model</th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| {% for vehicle in vehicles %} | |||
| <tr> | |||
| <td>{{ vehicle.id }}</td> | |||
| <td>{{ vehicle.license_plate }}</td> | |||
| <td>{{ vehicle.model }}</td> | |||
| </tr> | |||
| {% endfor %} | |||
| </tbody> | |||
| </table> | |||
| <h2>Repostajes</h2> | |||
| <table class="table"> | |||
| <thead> | |||
| <tr> | |||
| <th>ID</th> | |||
| <th>Vehicle</th> | |||
| <th>Amount</th> | |||
| <th>Date</th> | |||
| </tr> | |||
| </thead> | |||
| <tbody> | |||
| {% for repostaje in repostajes %} | |||
| <tr> | |||
| <td>{{ repostaje.id }}</td> | |||
| <td>{{ repostaje.vehicle.license_plate }}</td> | |||
| <td>{{ repostaje.amount }}</td> | |||
| <td>{{ repostaje.date }}</td> | |||
| </tr> | |||
| {% endfor %} | |||
| </tbody> | |||
| </table> | |||
| {% endblock %} | |||
| @ -0,0 +1,12 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| <title>Vehicle Repostajes</title> | |||
| <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> | |||
| </head> | |||
| <body> | |||
| <div class="container"> | |||
| {% block content %}{% endblock %} | |||
| </div> | |||
| </body> | |||
| </html> | |||
| @ -0,0 +1,17 @@ | |||
| alembic==1.13.1 | |||
| blinker==1.8.2 | |||
| click==8.1.7 | |||
| Flask==3.0.3 | |||
| Flask-Login==0.6.3 | |||
| Flask-Migrate==4.0.7 | |||
| Flask-SQLAlchemy==3.1.1 | |||
| Flask-WTF==1.2.1 | |||
| greenlet==3.0.3 | |||
| itsdangerous==2.2.0 | |||
| Jinja2==3.1.4 | |||
| Mako==1.3.5 | |||
| MarkupSafe==2.1.5 | |||
| SQLAlchemy==2.0.30 | |||
| typing_extensions==4.12.2 | |||
| Werkzeug==3.0.3 | |||
| WTForms==3.1.2 | |||