| @ -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 | |||||