diff --git a/app/admin/routes.py b/app/admin/routes.py index 100d1127cb67d3867baf9b9f974931120ce9a970..770b651fb0919de1f0b00f78959fe770428f0bdc 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,8 +1,9 @@ -from flask import render_template, redirect, url_for, request, jsonify +from flask import render_template, redirect, url_for, request, jsonify, current_app from app import db from app import admin_permission, permission_required, super_admin_permission -from app.models import Listings +from app.models import Listings, ListingImages from app.admin import bp +from app.main.utils import save_booking_image @bp.route('/home') @@ -26,7 +27,20 @@ def manage_bookings(): def edit_booking(id): locations = Listings.get_all_locations() listing_information = Listings.search_listing(id) - return render_template('admin/edit_booking.html', locations=locations, listing=listing_information) + time_options = [ + "00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00", "04:30", + "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", + "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", "14:00", "14:30", + "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", + "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30" + ] + + # Use the instance of the listing_information object to format the times + depart_time_str = listing_information.depart_time.strftime("%H:%M") + destination_time_str = listing_information.destination_time.strftime("%H:%M") + + return render_template('admin/edit_booking.html', locations=locations, listing=listing_information, time_options=time_options, depart_time_str=depart_time_str, destination_time_str=destination_time_str) + @bp.route('/manage_users') @permission_required(super_admin_permission) @@ -38,6 +52,66 @@ def manage_users(): def manage_user_bookings(): return render_template('admin/index.html') +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +@bp.route('update_booking/<int:id>', methods=['POST']) +@permission_required(admin_permission) +def update_booking(id): + depart_location = request.form.get('departLocation') + destination_location = request.form.get('destinationLocation') + depart_time = request.form.get('departTime') + destination_time = request.form.get('destinationTime') + fair_cost = request.form.get('fairCost') + transport_type = request.form.get('transportType') + images = request.files.getlist('images') + main_image_id = request.form.get('main_image') + + listing = Listings.query.get(id) + if listing: + if depart_location: + listing.depart_location = depart_location + if destination_location: + listing.destination_location = destination_location + if depart_time: + listing.depart_time = depart_time + if destination_time: + listing.destination_time = destination_time + if fair_cost: + listing.fair_cost = fair_cost + if transport_type: + listing.transport_type = transport_type + + try: + ListingImages.set_main_image(listing.id, main_image_id) + # Wrap upload of images in transaction in case any weird issues occur + for image in images: + new_image = ListingImages.save_image(image, listing.id) + if new_image == False: + continue + if new_image == None: + raise Exception("Failed to save image") + + db.session.commit() + except Exception as e: + print(f"Error: {e}") + db.session.rollback() + locations = Listings.get_all_locations() + listing_information = Listings.search_listing(id) + return render_template( + 'admin/edit_booking.html', + locations=locations, + listing=listing_information, + error="An error occurred while updating the booking." + ) + + locations = Listings.get_all_locations() + return redirect(url_for('admin.manage_bookings')) + + + @bp.route('get_bookings', methods=['GET']) @permission_required(admin_permission) def get_bookings(): diff --git a/app/main/routes.py b/app/main/routes.py index 1970ff647df87d0007565b1426762db398c7642a..9d37e2641502bb381b11bb7f40997d2bb32b55b3 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -27,4 +27,4 @@ def index(): def upload_file(filename): upload_folder = os.path.join(os.getcwd(), 'app/uploads') - return send_from_directory(upload_folder, f'listing_images/{filename}') \ No newline at end of file + return send_from_directory(upload_folder, f'listing_images/{filename}') diff --git a/app/main/utils.py b/app/main/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..a4f73ed6f910d125e56c06ed7f3177cfa832a505 --- /dev/null +++ b/app/main/utils.py @@ -0,0 +1,25 @@ +# utils.py + +import os +from flask import current_app +from werkzeug.utils import secure_filename + + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS'] + +def save_booking_image(file): + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + file_path = os.path.join(os.path.join(current_app.config['BOOKING_IMAGE_UPLOADS'], filename)) + + # Ensure the directory exists before saving images + os.makedirs(os.path.join(current_app.config['BOOKING_IMAGE_UPLOADS']), exist_ok=True) + + print(f"File Path: {file_path}") + + + file.save(file_path) + return filename + else: + return None diff --git a/app/models/listing_images.py b/app/models/listing_images.py index 30152a45f10bc201a669ff757080f34e38724537..e0b41f6ef1054b2607126bd94135725a2e740d25 100644 --- a/app/models/listing_images.py +++ b/app/models/listing_images.py @@ -1,6 +1,10 @@ from sqlalchemy import Integer, ForeignKey from sqlalchemy.orm import relationship from app import db +import os +from werkzeug.utils import secure_filename +from flask import current_app +import uuid class ListingImages(db.Model): __tablename__ = 'listing_images' @@ -22,3 +26,40 @@ class ListingImages(db.Model): ordered_listing_images = {listing.listing_id: listing.image_location for listing in listing_images} return ordered_listing_images + + @staticmethod + def allowed_file(filename): + allowed_extensions = current_app.config['ALLOWED_EXTENSIONS'] + return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions + + @staticmethod + def save_image(file, listing_id): + if file and ListingImages.allowed_file(file.filename): + extension = file.filename.rsplit('.', 1)[1].lower() + filename = f"{listing_id}_{uuid.uuid4().hex}.{extension}" #Change to make all files uploaded unique to prevent collisions + upload_folder = current_app.config['BOOKING_IMAGE_UPLOADS'] + file_path = os.path.join(upload_folder, filename) + + try: + os.makedirs(upload_folder, exist_ok=True) + file.save(file_path) + new_image = ListingImages( + listing_id=listing_id, + image_location=filename, + main_image=False + ) + db.session.add(new_image) + return new_image + except Exception as e: + print(f"Error saving file: {e}") + return None + else: + return False + + @classmethod + def set_main_image(cls, listing_id, image_id): + cls.query.filter_by(listing_id=listing_id).update({'main_image': False}) + main_image = cls.query.filter_by(id=image_id, listing_id=listing_id).first() + if main_image: + main_image.main_image = True + db.session.commit() \ No newline at end of file diff --git a/app/templates/admin/edit_booking.html b/app/templates/admin/edit_booking.html index f51a0c2d02f7dc5f095fb8543b595913407da021..0974cd8e3ca24c817babb5b9554332695967e891 100644 --- a/app/templates/admin/edit_booking.html +++ b/app/templates/admin/edit_booking.html @@ -1,133 +1,125 @@ {% extends 'base.html' %} -{% macro time_options(selected_time) %} - {% for time in ["00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00", "04:30", - "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", - "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", "14:00", "14:30", - "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", - "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30"] %} - <option value="{{ time }}" {% if selected_time == time %}selected{% endif %}>{{ time }}</option> - {% endfor %} -{% endmacro %} {% block content %} -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Edit Booking</title> - <link rel="stylesheet" href="path/to/your/css/style.css"> -</head> -<body> - <div class="container mt-4"> - <h2>Edit Booking</h2> - <form id="editBookingForm" class="row g-3" action="{{ url_for('admin.get_bookings', id=listing.id) }}" method="post" enctype="multipart/form-data"> - <div class="col-md-6"> - <label for="departLocation" class="form-label">Departure Location:</label> - <select class="form-select select2-multiple" id="departLocation" name="departLocation" required> - <option value="" disabled selected>Select locations</option> - {% for location in locations %} - <option value="{{ location }}" {% if location == listing.depart_location %}selected{% endif %}>{{ location }}</option> - {% endfor %} - </select> - </div> - <div class="col-md-6"> - <label for="departTime" class="form-label">Departure Time:</label> - <select class="form-control select2-dropdown" id="departTime" name="departTime" required> - <option value="" disabled selected>Select a time</option> - {{ time_options(listing.depart_time.strftime('%H:%M')) }} - </select> - </div> - <div class="col-md-6"> - <label for="destinationLocation" class="form-label">Destination Location:</label> - <select class="form-select select2-multiple" id="destinationLocation" name="destinationLocation" required> - <option value="" disabled selected>Select locations</option> - {% for location in locations %} - <option value="{{ location }}" {% if location == listing.destination_location %}selected{% endif %}>{{ location }}</option> - {% endfor %} - </select> - </div> - <div class="col-md-6"> - <label for="destinationTime" class="form-label">Arrival Time:</label> - <select class="form-control select2-dropdown" id="destinationTime" name="destinationTime" required> - <option value="" disabled selected>Select a time</option> - {{ time_options(listing.destination_time.strftime('%H:%M')) }} - </select> - </div> - - <script> - $(document).ready(function() { - $('.select2-dropdown').select2({ - placeholder: "Select a time", - width: '100%', - minimumResultsForSearch: Infinity - }); - }); - </script> - - <div class="col-md-6"> - <label for="fairCost" class="form-label">Fair Cost:</label> - <input type="number" step="0.01" class="form-control" id="fairCost" name="fairCost" value="{{ listing.fair_cost }}" required> - </div> - <div class="col-md-6"> - <label for="transportType" class="form-label">Transport Type:</label> - <select id="transportType" class="form-select" name="transportType" value="Airplane" disabled> - <option value="Airplane">Airplane</option> - </select> - </div> - <div class="col-md-12"> - <label for="images" class="form-label">Upload Images:</label> - <input type="file" class="form-control" id="images" name="images" multiple> - <div class="current-images mt-3"> - <h4>Current Images:</h4> - <div class="row"> - {% for image in listing.listing_images %} - <div class="col-md-3 mb-3"> - <div class="card"> - <div class="image-container"> - <img src="{{ url_for('main.upload_file', filename=image.image_location) }}" class="img-thumbnail"> - </div> - <div class="card-body"> - <input type="radio" class="btn-check" name="main_image" id="{{image.id}}" value="{{ image.main_image }}" autocomplete="off" {% if image.main_image == 1 %}checked{% endif %}> - <label class="btn btn-outline-success w-100" id="{{image.id}}">Main Image</label> +<div class="container mt-4"> + <h2>Edit Booking</h2> + <form id="editBookingForm" class="row g-3" action="{{ url_for('admin.update_booking', id=listing.id) }}" method="post" enctype="multipart/form-data"> + <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> + <div class="col-md-6"> + <label for="departLocation" class="form-label">Departure Location:</label> + <select class="form-select select2-multiple" id="departLocation" name="departLocation" required> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}" {% if location == listing.depart_location %}selected{% endif %}>{{ location }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="departTime" class="form-label">Departure Time:</label> + <select class="form-control select2-dropdown" id="departTime" name="departTime" required> + <option value="" disabled>Select a time</option> + {% for time in time_options %} + <option value="{{ time }}" {% if time == depart_time_str %}selected{% endif %}>{{ time }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="destinationLocation" class="form-label">Destination Location:</label> + <select class="form-select select2-multiple" id="destinationLocation" name="destinationLocation" required> + <option value="" disabled selected>Select locations</option> + {% for location in locations %} + <option value="{{ location }}" {% if location == listing.destination_location %}selected{% endif %}>{{ location }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="destinationTime" class="form-label">Arrival Time:</label> + <select class="form-control select2-dropdown" id="destinationTime" name="destinationTime" required> + <option value="" disabled>Select a time</option> + {% for time in time_options %} + <option value="{{ time }}" {% if time == destination_time_str %}selected{% endif %}>{{ time }}</option> + {% endfor %} + </select> + </div> + <div class="col-md-6"> + <label for="fairCost" class="form-label">Fair Cost:</label> + <input type="number" step="0.01" class="form-control" id="fairCost" name="fairCost" value="{{ listing.fair_cost }}" required> + </div> + <div class="col-md-6"> + <label for="transportType" class="form-label">Transport Type:</label> + <select id="transportType" class="form-select" name="transportType" value="Airplane" disabled> + <option value="Airplane">Airplane</option> + </select> + </div> + <div class="col-md-12"> + <label for="images" class="form-label">Upload Images:</label> + <input type="file" class="form-control" id="images" name="images" multiple> + <div class="current-images mt-3"> + <h4>Current Images:</h4> + <div class="row"> + {% for image in listing.listing_images %} + <div class="col-md-3 mb-3"> + <div class="card"> + <div class="image-container"> + <img src="{{ url_for('main.upload_file', filename=image.image_location) }}" class="img-thumbnail"> + </div> + <div class="card-body"> + <input type="radio" class="btn-check" name="main_image" id="{{image.id}}" value="{{ image.id }}" autocomplete="off" {% if image.main_image == 1 %}checked{% endif %}> + <label class="btn btn-outline-success w-100" for="{{image.id}}">Main Image</label> - <button type="button" class="btn btn-danger btn-sm mt-2 delete-image-btn w-100">Delete</button> - </div> + <button type="button" class="btn btn-danger btn-sm mt-2 delete-image-btn w-100" data-image="{{ image.image_location }}">Delete</button> </div> </div> - {% endfor %} </div> + {% endfor %} </div> </div> - <div class="col-12"> - <button type="submit" class="btn btn-primary">Update Booking</button> - </div> - </form> - </div> + </div> + <div class="col-12"> + <button type="submit" class="btn btn-primary">Update Booking</button> + </div> + </form> +</div> + +<script> + $('.select2-dropdown').select2({ + width: '100%', + minimumResultsForSearch: Infinity + }); - <script> - // JavaScript to handle image deletion - document.querySelectorAll('.delete-image-btn').forEach(button => { - button.addEventListener('click', function() { - const image = this.getAttribute('data-image'); - this.closest('.col-md-3').remove(); - // Add logic to handle image deletion in the backend - console.log('Deleted image:', image); - }); + const time_options = [ + "00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", "03:30", "04:00", "04:30", + "05:00", "05:30", "06:00", "06:30", "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", + "10:00", "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", "14:00", "14:30", + "15:00", "15:30", "16:00", "16:30", "17:00", "17:30", "18:00", "18:30", "19:00", "19:30", + "20:00", "20:30", "21:00", "21:30", "22:00", "22:30", "23:00", "23:30" + ]; + + // Add time_options to filter elements + time_options.forEach(time => { + $('#departTime').append(new Option(time, time)); + $('#destinationTime').append(new Option(time, time)); + }); + + // JavaScript to handle image deletion + document.querySelectorAll('.delete-image-btn').forEach(button => { + button.addEventListener('click', function() { + const image = this.getAttribute('data-image'); + this.closest('.col-md-3').remove(); + console.log('Deleted image:', image); }); - </script> -</body> -</html> + }); +</script> <style> .image-container { position: relative; - width: 100%; - padding-top: 100%; - overflow: hidden; + width: 100%; + padding-top: 100%; + overflow: hidden; display: flex; justify-content: center; align-items: center; } - + .image-container img { position: absolute; top: 50%; @@ -135,7 +127,7 @@ width: 100%; height: 100%; object-fit: cover; - transform: translate(-50%, -50%); /* Makes image center */ + transform: translate(-50%, -50%); } - </style> -{% endblock %} \ No newline at end of file +</style> +{% endblock %} diff --git a/app/uploads/listing_images/1_ddde7029b2714e27b58b483cfec65d29.gif b/app/uploads/listing_images/1_ddde7029b2714e27b58b483cfec65d29.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/1_ddde7029b2714e27b58b483cfec65d29.gif differ diff --git a/app/uploads/listing_images/images.jpeg b/app/uploads/listing_images/images.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c775255f893e8d4baaad92fc47832c84a101e102 Binary files /dev/null and b/app/uploads/listing_images/images.jpeg differ diff --git a/app/uploads/listing_images/surprised-turn.gif b/app/uploads/listing_images/surprised-turn.gif new file mode 100644 index 0000000000000000000000000000000000000000..71aede03087e88f8e47af526b1bcb71adb687192 Binary files /dev/null and b/app/uploads/listing_images/surprised-turn.gif differ diff --git a/config.py b/config.py index 17d6737c87bd1c7f4f2972f679b95bbe76c7153e..5b60c8529e3fd7109e5ee33e5cc70c7565cfe581 100644 --- a/config.py +++ b/config.py @@ -5,6 +5,8 @@ import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config: + BOOKING_IMAGE_UPLOADS = os.path.join(os.getcwd(), 'app/uploads/listing_images') + ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} SECRET_KEY = os.environ.get('SECRET_KEY') SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI')\ or 'sqlite:///' + os.path.join(basedir, 'app.db')