diff --git a/.github/workflows/starter-no-infra_msdcos-poython-postgres-pap3.yml b/.github/workflows/starter-no-infra_msdcos-poython-postgres-pap3.yml new file mode 100644 index 00000000..9d9f4fd2 --- /dev/null +++ b/.github/workflows/starter-no-infra_msdcos-poython-postgres-pap3.yml @@ -0,0 +1,81 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions +# More info on Python, GitHub Actions, and Azure App Service: https://aka.ms/python-webapps-actions + +name: Build and deploy Python app to Azure Web App - msdcos-poython-postgres-pap3 + +on: + push: + branches: + - starter-no-infra + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read #This is required for actions/checkout + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python version + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Create and start virtual environment + run: | + python -m venv venv + source venv/bin/activate + + - name: Install dependencies + run: pip install -r requirements.txt + + # Optional: Add step to run tests here (PyTest, Django test suites, etc.) + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment jobs + uses: actions/upload-artifact@v4 + with: + name: python-app + path: | + release.zip + !venv/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write #This is required for requesting the JWT + contents: read #This is required for actions/checkout + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: python-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_15D32A133FE345938D0B68C1387A074C }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_961C12A76F67417695358C3D734F4D13 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_55D54E4C2A824EB39284FE19BA8B29CF }} + + - name: 'Deploy to Azure Web App' + uses: azure/webapps-deploy@v3 + id: deploy-to-webapp + with: + app-name: 'msdcos-poython-postgres-pap3' + slot-name: 'Production' + \ No newline at end of file diff --git a/app.py b/app.py index 8fc6a89b..e9c6d589 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ import os -from datetime import datetime +from datetime import datetime, timezone -from flask import Flask, redirect, render_template, request, send_from_directory, url_for +from flask import Flask, jsonify, redirect, render_template, request, send_from_directory, url_for from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from flask_wtf.csrf import CSRFProtect @@ -32,13 +32,19 @@ migrate = Migrate(app, db) # The import must be done after db initialization due to circular import issue -from models import Restaurant, Review +from models import Restaurant, Review, ImageData + +#@app.route('/', methods=['GET']) +#def index(): +# print('Request for index page received') +# restaurants = Restaurant.query.all() +# return render_template('index.html', restaurants=restaurants) @app.route('/', methods=['GET']) def index(): print('Request for index page received') - restaurants = Restaurant.query.all() - return render_template('index.html', restaurants=restaurants) + images = ImageData.query.all() + return render_template('index.html', images=images) @app.route('/', methods=['GET']) def details(id): @@ -114,6 +120,49 @@ def star_rating(id): return dict(star_rating=star_rating) +@app.route('/images', methods=['GET']) +def image_table(): + print('Request for image table page received') + images = ImageData.query.order_by(ImageData.upload_time.desc()).all() + return render_template('index.html', images=images) + +@csrf.exempt +@app.route('/upload_image', methods=['POST']) +def upload_image(): + print('Request to upload image received') + if not request.is_json: + return jsonify({"error": "Request must be JSON"}), 400 + + data = request.get_json() + filename = data.get('filename') + pixel_red = data.get('pixel_red') + pixel_green = data.get('pixel_green') + pixel_blue = data.get('pixel_blue') + username = data.get('username') + + + if not all([filename, pixel_red, pixel_green, pixel_blue, username]): + return jsonify({"error": "All fields ('filename', 'pixel_red', 'pixel_green', 'pixel_blue', 'username') are required"}), 400 + + try: + new_image = ImageData( + filename=filename, + pixel_red=pixel_red, + pixel_green=pixel_green, + pixel_blue=pixel_blue, + username=username, + upload_time=datetime.now(timezone.utc) + + ) + #db.session.create_all() + #db.session.commit() + db.session.add(new_image) + db.session.commit() + return jsonify({"message": "Image uploaded successfully"}), 201 + except Exception as e: + db.session.rollback() + return jsonify({"error": str(e)}), 500 + @app.route('/favicon.ico') def favicon(): return send_from_directory(os.path.join(app.root_path, 'static'), diff --git a/azureproject/production.py b/azureproject/production.py index 331bd036..314ec193 100644 --- a/azureproject/production.py +++ b/azureproject/production.py @@ -1,8 +1,8 @@ import os -# DATABASE_URI = 'postgresql+psycopg2://{dbuser}:{dbpass}@{dbhost}/{dbname}'.format( -# dbuser=os.getenv('AZURE_POSTGRESQL_USER'), -# dbpass=os.getenv('AZURE_POSTGRESQL_PASSWORD'), -# dbhost=os.getenv('AZURE_POSTGRESQL_HOST'), -# dbname=os.getenv('AZURE_POSTGRESQL_NAME') -# ) +DATABASE_URI = 'postgresql+psycopg2://{dbuser}:{dbpass}@{dbhost}/{dbname}'.format( + dbuser=os.getenv('AZURE_POSTGRESQL_USER'), + dbpass=os.getenv('AZURE_POSTGRESQL_PASSWORD'), + dbhost=os.getenv('AZURE_POSTGRESQL_HOST'), + dbname=os.getenv('AZURE_POSTGRESQL_NAME') +) diff --git "a/migrations/versions/1920d7e9c1dc_recrear_migraci\303\263n_perdida.py" "b/migrations/versions/1920d7e9c1dc_recrear_migraci\303\263n_perdida.py" new file mode 100644 index 00000000..a9e45913 --- /dev/null +++ "b/migrations/versions/1920d7e9c1dc_recrear_migraci\303\263n_perdida.py" @@ -0,0 +1,24 @@ +"""Recrear migración perdida + +Revision ID: 1920d7e9c1dc +Revises: +Create Date: 2025-05-08 17:10:57.001491 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1920d7e9c1dc' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/1c80845faa01_unify_heads.py b/migrations/versions/1c80845faa01_unify_heads.py new file mode 100644 index 00000000..a970c9ca --- /dev/null +++ b/migrations/versions/1c80845faa01_unify_heads.py @@ -0,0 +1,24 @@ +"""Unify heads + +Revision ID: d0c7b8e4b57c +Revises: 7d50f8ce1dd1, e95f32cb7145 +Create Date: 2025-05-08 22:28:42.064772 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd0c7b8e4b57c' +down_revision = ('7d50f8ce1dd1', 'e95f32cb7145') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/2b81d0790f10_a.py b/migrations/versions/2b81d0790f10_a.py new file mode 100644 index 00000000..dda1960f --- /dev/null +++ b/migrations/versions/2b81d0790f10_a.py @@ -0,0 +1,37 @@ +"""a + +Revision ID: 2b81d0790f10 +Revises: f7006424e5cd +Create Date: 2025-05-08 21:56:08.485113 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2b81d0790f10' +down_revision = 'f7006424e5cd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('image_data') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('image_data', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('filename', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('pixel_red', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('pixel_green', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('pixel_blue', sa.INTEGER(), autoincrement=False, nullable=False), + sa.Column('username', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('upload_time', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='image_data_pkey') + ) + # ### end Alembic commands ### diff --git a/migrations/versions/7d50f8ce1dd1_regenerar_migraciones.py b/migrations/versions/7d50f8ce1dd1_regenerar_migraciones.py new file mode 100644 index 00000000..27bc04f4 --- /dev/null +++ b/migrations/versions/7d50f8ce1dd1_regenerar_migraciones.py @@ -0,0 +1,50 @@ +"""Regenerar migraciones + +Revision ID: 7d50f8ce1dd1 +Revises: 1920d7e9c1dc +Create Date: 2025-05-08 17:11:06.789172 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '7d50f8ce1dd1' +down_revision = '1920d7e9c1dc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('image_data', schema=None) as batch_op: + batch_op.add_column(sa.Column('pixel_red', sa.String(), nullable=False)) + batch_op.add_column(sa.Column('pixel_green', sa.String(), nullable=False)) + batch_op.add_column(sa.Column('pixel_blue', sa.String(), nullable=False)) + batch_op.add_column(sa.Column('upload_time', sa.DateTime(), nullable=False)) + batch_op.alter_column('username', + existing_type=sa.VARCHAR(length=50), + type_=sa.String(length=100), + existing_nullable=False) + batch_op.drop_column('pixel_data') + batch_op.drop_column('upload_datetime') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('image_data', schema=None) as batch_op: + batch_op.add_column(sa.Column('upload_datetime', postgresql.TIMESTAMP(), autoincrement=False, nullable=True)) + batch_op.add_column(sa.Column('pixel_data', sa.TEXT(), autoincrement=False, nullable=False)) + batch_op.alter_column('username', + existing_type=sa.String(length=100), + type_=sa.VARCHAR(length=50), + existing_nullable=False) + batch_op.drop_column('upload_time') + batch_op.drop_column('pixel_blue') + batch_op.drop_column('pixel_green') + batch_op.drop_column('pixel_red') + + # ### end Alembic commands ### diff --git a/migrations/versions/d0c7b8e4b57c_initial_migration.py b/migrations/versions/d0c7b8e4b57c_initial_migration.py deleted file mode 100644 index 1a5d874d..00000000 --- a/migrations/versions/d0c7b8e4b57c_initial_migration.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Initial migration. - -Revision ID: d0c7b8e4b57c -Revises: -Create Date: 2022-11-08 17:00:02.151921 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd0c7b8e4b57c' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('restaurant', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=50), nullable=True), - sa.Column('street_address', sa.String(length=50), nullable=True), - sa.Column('description', sa.String(length=250), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - op.create_table('review', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('restaurant', sa.Integer(), nullable=True), - sa.Column('user_name', sa.String(length=30), nullable=True), - sa.Column('rating', sa.Integer(), nullable=True), - sa.Column('review_text', sa.String(length=500), nullable=True), - sa.Column('review_date', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['restaurant'], ['restaurant.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('review') - op.drop_table('restaurant') - # ### end Alembic commands ### \ No newline at end of file diff --git a/migrations/versions/e95f32cb7145_b.py b/migrations/versions/e95f32cb7145_b.py new file mode 100644 index 00000000..16747869 --- /dev/null +++ b/migrations/versions/e95f32cb7145_b.py @@ -0,0 +1,37 @@ +"""b + +Revision ID: e95f32cb7145 +Revises: 2b81d0790f10 +Create Date: 2025-05-08 22:07:14.980769 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e95f32cb7145' +down_revision = '2b81d0790f10' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('image_data', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('filename', sa.String(length=255), nullable=False), + sa.Column('pixel_red', sa.Integer(), nullable=False), + sa.Column('pixel_green', sa.Integer(), nullable=False), + sa.Column('pixel_blue', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=100), nullable=False), + sa.Column('upload_time', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('image_data') + # ### end Alembic commands ### diff --git a/migrations/versions/f7006424e5cd_initial_migration.py b/migrations/versions/f7006424e5cd_initial_migration.py new file mode 100644 index 00000000..a36045f9 --- /dev/null +++ b/migrations/versions/f7006424e5cd_initial_migration.py @@ -0,0 +1,67 @@ +"""Initial migration. + +Revision ID: f7006424e5cd +Revises: +Create Date: 2022-11-08 17:00:02.151921 + +""" +from sqlalchemy import inspect +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f7006424e5cd' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + bind = op.get_bind() + inspector = inspect(bind) + if 'restaurant' not in inspector.get_table_names(): + op.create_table( + 'restaurant', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('name', sa.String(length=50)), + sa.Column('street_address', sa.String(length=50)), + sa.Column('description', sa.String(length=250)) + ) + if 'review' not in inspector.get_table_names(): + op.create_table( + 'review', + sa.Column('id', sa.Integer(), primary_key=True), + sa.Column('restaurant', sa.Integer(), sa.ForeignKey('restaurant.id', ondelete="CASCADE")), + sa.Column('user_name', sa.String(length=30)), + sa.Column('rating', sa.Integer()), + sa.Column('review_text', sa.String(length=500)), + sa.Column('review_date', sa.DateTime()) + ) + + # ### commands auto generated by Alembic - please adjust! ### + #op.create_table('restaurant', + #sa.Column('id', sa.Integer(), nullable=False), + #sa.Column('name', sa.String(length=50), nullable=True), + #sa.Column('street_address', sa.String(length=50), nullable=True), + #sa.Column('description', sa.String(length=250), nullable=True), + #sa.PrimaryKeyConstraint('id') + #) + #op.create_table('review', + #sa.Column('id', sa.Integer(), nullable=False), + #sa.Column('restaurant', sa.Integer(), nullable=True), + #sa.Column('user_name', sa.String(length=30), nullable=True), + #sa.Column('rating', sa.Integer(), nullable=True), + #sa.Column('review_text', sa.String(length=500), nullable=True), + #sa.Column('review_date', sa.DateTime(), nullable=True), + #sa.ForeignKeyConstraint(['restaurant'], ['restaurant.id'], ondelete='CASCADE'), + #sa.PrimaryKeyConstraint('id') + #) + ## ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('review') + op.drop_table('restaurant') + # ### end Alembic commands ### \ No newline at end of file diff --git a/models.py b/models.py index 7f3dac97..2556963e 100644 --- a/models.py +++ b/models.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy.orm import validates @@ -30,3 +31,17 @@ def validate_rating(self, key, value): def __str__(self): return f"{self.user_name}: {self.review_date:%x}" + +class ImageData(db.Model): + __tablename__ = 'image_data' + + id = Column(Integer, primary_key=True) + filename = Column(String(255), nullable=False) + pixel_red = Column(Integer, nullable=False) + pixel_green = Column(Integer, nullable=False) # + pixel_blue = Column(Integer, nullable=False) + username = Column(String(100), nullable=False) + upload_time = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + + def __repr__(self): + return f"" diff --git a/templates/base.html b/templates/base.html index 78fb4061..3881a3fb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@
- Azure Restaurant Review + Scalacomp Imágenes Review