diff --git a/README b/README index b2227e831b296990ca5db8b20a0125b298166cc1..ccb8bfeba59830d148e9fe55b3a934d6f96df1e8 100644 --- a/README +++ b/README @@ -83,12 +83,12 @@ to be fully setup. # Then run the following, if you are not on port 5000 you will need to force it with the '--port 5000' arg flask run (Will run on 127.0.0.1:5000) -# To apply database changes/run migrations -flask db upgrade - # Creating new migration file (for development) flask db migrate -m "Add age column to User model" +# To apply database changes/run migrations +flask db upgrade + ########################################## # Database Commands # ########################################## diff --git a/app/__init__.py b/app/__init__.py index 6d2b047da24fe98ebc941b4c9aea32393ea0cd43..8cbfb643751a3a97067028c7d1476640f0fd4a38 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,4 @@ -from flask import Flask, g, abort, current_app, request, session, redirect, url_for +from flask import Flask, g, abort, request, session, redirect, url_for from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -10,9 +10,7 @@ from app.logger import auth_logger, error_logger from functools import wraps import os import pymysql -from sqlalchemy.sql import text -# Initialize extensions db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager() @@ -20,15 +18,15 @@ csrf = CSRFProtect() principal = Principal() def permission_required(permission): - def decorator(f): + def check_role(f): @wraps(f) - def decorated_function(*args, **kwargs): + def check_permission(*args, **kwargs): if not permission.can(): auth_logger.debug(f'Permission denied for {current_user} attempting to access {request.endpoint}.') abort(403) return f(*args, **kwargs) - return decorated_function - return decorator + return check_permission + return check_role super_admin_permission = Permission(RoleNeed('super-admin')) admin_permission = Permission(RoleNeed('admin')) @@ -159,18 +157,18 @@ def create_app(config_class=Config): response.headers['X-Frame-Options'] = 'SAMEORIGIN' return response - # @app.errorhandler(Exception) - # def handle_exception(e): - # app.logger.error(f"Unhandled exception: {e}") - # session['error_message'] = str(e) - # return redirect(url_for('errors.quandary')) - - @app.errorhandler(403) + @app.errorhandler(Exception) def handle_exception(e): app.logger.error(f"Unhandled exception: {e}") session['error_message'] = str(e) return redirect(url_for('errors.quandary')) + @app.errorhandler(403) + def handle_exception(e): + app.logger.debug(f"Unauthorized: {e}") + session['error_message'] = str(e) + return redirect(url_for('errors.no_permission')) + @app.before_request def before_request(): g.admin_permission = None diff --git a/app/admin/routes.py b/app/admin/routes.py index 7f3f80a03653d751d560c15609aa7939ee88c5ae..4668cfa26e5d93761e02220e541a45ef1cce535b 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -2,7 +2,7 @@ from flask import render_template, redirect, url_for, request, jsonify, flash from datetime import datetime, timedelta from sqlalchemy.sql import text from app import admin_permission, permission_required, super_admin_permission, db -from app.models import Listings, ListingImages, User, Bookings +from app.models import Listings, ListingImages, User, Bookings, Role from app.main.utils import generate_time_options from app.admin import bp @@ -47,10 +47,11 @@ def edit_booking(id): @permission_required(super_admin_permission) def edit_user(id): user = User.search_user_id(id) - + roles = Role.get_all_roles() return render_template( 'admin/edit_user.html', - user=user + user=user, + roles=roles ) @@ -330,20 +331,34 @@ def delete_image(image_id): db.session.rollback() return jsonify({'success': False, 'error': str(e)}), 500 - -@bp.route('/init/database', methods=['GET']) -def check_database_exists(): - try: - if Listings.check_table_exists(): - flash ("Database already exists, 'error") +@permission_required(super_admin_permission) +@bp.route('/update_user_field', methods=['POST']) +def update_user_field(): + data = request.get_json() + user_id = data.get('user_id') + field = data.get('field') + value = data.get('value') + user = User.query.get(user_id) + + if user: + if field == 'userName': + user.username = value + elif field == 'userEmail': + user.email = value + elif field == 'userRole': + role = Role.query.get(value) + if role: + user.role_id = role.id + else: + return jsonify(success=False, message="Invalid role"), 400 else: - raise Exception('Schema exists but database does not') - except: - with open('sql-setup/init.sql', 'r') as file: - sql_commands = file.read().split(';') - for command in sql_commands: - if command.strip(): - db.session.execute(text(command)) - - db.session.commit() - flash ("Database initialised", 'success') + return jsonify(success=False, message="Invalid field"), 400 + + try: + db.session.commit() + return jsonify(success=True) + except Exception as e: + db.session.rollback() + return jsonify(success=False, message=str(e)), 500 + else: + return jsonify(success=False, message="User not found"), 404 \ No newline at end of file diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 21a5c45f38673fd0e680dee80282766c5cdd5f41..b8597755fd9f16bc05dc2c4be1fa65305fb2263d 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -179,8 +179,8 @@ def listing(id): session['listing']['economy_fair_cost'] = listing.economy_fair_cost session['listing']['business_fair_cost'] = listing.business_fair_cost - listing.depart_time = pretty_time(listing.depart_time) - listing.destination_time = pretty_time(listing.destination_time) + depart_time = pretty_time(listing.depart_time) + destination_time = pretty_time(listing.destination_time) filter_data = session.pop('filter_data', None) selected_date = filter_data['date'] if filter_data and 'date' in filter_data else None @@ -193,6 +193,13 @@ def listing(id): else: base_price = listing.economy_fair_cost + main_image_url = None + for image in listing.listing_images: + if image.main_image == 1: + main_image_url = image.image_location + break + + discounted_price = base_price * (1 - discount / 100) total_cost = discounted_price @@ -205,7 +212,10 @@ def listing(id): days_away=days_away, base_price=base_price, discounted_price=discounted_price, - total_cost=total_cost + total_cost=total_cost, + depart_time = depart_time, + destination_time = destination_time, + main_image_url = main_image_url ) # This route should be used after show_listing if used internally as this clears the ajax parameters before redirecting the user @@ -376,7 +386,6 @@ def generate_ticket(id): return send_file(pdf, as_attachment=True, download_name='plane_ticket.pdf') - @bp.route('/get_user_bookings', methods=['GET']) @permission_required(user_permission) def get_user_bookings(): @@ -386,9 +395,11 @@ def get_user_bookings(): destination_location = request.args.get('destination_location') booking_date = request.args.get('booking_date') depart_date = request.args.get('depart_date') + exclude_cancelled = request.args.get('exclude_cancelled') + + if exclude_cancelled and exclude_cancelled.lower() == 'true': + query = query.filter(Bookings.cancelled == 0) - # Only get non-cancelled bookings - query = query.filter(Bookings.cancelled == 0) if depart_location: depart_locations = depart_location.split(',') query = query.filter(Listings.depart_location.in_(depart_locations)) @@ -408,8 +419,8 @@ def get_user_bookings(): 'booking_date': booking.booking_date.strftime("%a, %d %b %Y"), 'destination_location': booking.listing.destination_location, 'depart_date': booking.depart_date.strftime("%a, %d %b %Y"), + 'cancelled': 'Yes' if booking.cancelled else 'No' } for booking in filtered_data ] return jsonify(result) - diff --git a/app/errors/routes.py b/app/errors/routes.py index 3581bd0d52c884907176ff0cb22d7771402ce677..a918481d9c8e49745fc009c78e4505b743b313ad 100644 --- a/app/errors/routes.py +++ b/app/errors/routes.py @@ -3,7 +3,12 @@ from app.errors import bp @bp.route('/quandary') def quandary(): - error_message = 'Something unexpected occurred.' + error_message = 'Something went wrong, if this continues please contact support.' if 'error_message' in session: error_message = session.pop('error_message') return render_template("errors/quandary.html", error_message=error_message) + +@bp.route('/no_permission') +def no_permission(): + error_message = 'You do not have the required permission to view this page.' + return render_template("errors/quandary.html", error_message=error_message) diff --git a/app/main/routes.py b/app/main/routes.py index 14ec0e8fe689015cb4b816f940fe263a8ec0080f..4ff716adde4441120f9fa0e431772d5361cac069 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -52,3 +52,24 @@ def log_message(): return jsonify({'success': True, 'message': 'Log message recorded'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 + + +@bp.route('/about_us') +def about_us(): + return render_template('main/about_us.html') + +@bp.route('/faq') +def faq(): + return render_template('main/faq.html') + +@bp.route('/privacy_policy') +def privacy_policy(): + return render_template('main/privacy_policy.html') + +@bp.route('/tos') +def tos(): + return render_template('main/tos.html') + +@bp.route('/contact_us') +def contact_us(): + return render_template('main/contact_us.html') \ No newline at end of file diff --git a/app/models/listing_images.py b/app/models/listing_images.py index d6b43d2abf69922682420f34fa92ba4efce8a42e..c7e9f56a49909806c1b6428578f9e0d98b77c3e5 100644 --- a/app/models/listing_images.py +++ b/app/models/listing_images.py @@ -11,7 +11,6 @@ class ListingImages(db.Model): id = db.Column(db.Integer(), nullable=False, primary_key=True) image_location = db.Column(db.String(255), nullable=False) - image_description = db.Column(db.String(255), nullable=True) main_image = db.Column(db.SmallInteger(), nullable=False) listing_id = db.Column(Integer, ForeignKey('listings.id'), nullable=False) listing = relationship("Listings", back_populates="listing_images") diff --git a/app/models/role.py b/app/models/role.py index 2b5f65b07612d05ed6b3a5f5e611570d296a9579..9495c715e759e02132146b16cc3ea317f3c4613e 100644 --- a/app/models/role.py +++ b/app/models/role.py @@ -8,3 +8,7 @@ class Role(RoleMixin, db.Model): name = db.Column(db.String(80), unique=True) description = db.Column(db.String(255)) + @classmethod + def get_all_roles(cls): + roles = cls.query.all() + return [{'id': role.id, 'name': role.name} for role in roles] \ No newline at end of file diff --git a/app/static/base.css b/app/static/base.css index 74a5bbfaa256f9ec80fe42d02f0d110c40083b7f..a7efb8f77a024081cd585dd499abf64702c5eff4 100644 --- a/app/static/base.css +++ b/app/static/base.css @@ -6,6 +6,15 @@ html, body { } /** FOOTER CSS */ +.footer_column a { + color: inherit; + text-decoration: none; + font-family: inherit; +} + +.footer_column a:hover { + text-decoration: underline; +} .footer { text-align: center; @@ -126,11 +135,6 @@ Form styling options color: #FBE9D0; } -.form-check-input:checked { - color: #FBE9D0; - background: #FBE9D0; -} - .form_header { text-align: center; text-transform: uppercase; diff --git a/app/static/listings.css b/app/static/listings.css index 4fc4a6aa1e674086fd64d4c7765ebd4a29a7fec7..82d15d1d8a4198d6b020142f0c749600754a4629 100644 --- a/app/static/listings.css +++ b/app/static/listings.css @@ -7,21 +7,4 @@ font-family: "Work Sans", sans-serif; padding: 20px 20px; } - -.deals_underline { -position: relative; -white-space: nowrap; -&:after { - --deco-height: 0.3125em; - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: calc(var(--deco-height) * -0.625); - height: var(--deco-height); - background-image: url("data:image/svg+xml,%3Csvg width='100' height='64' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23a)'%3E%3Cpath d='M-17 30.5C-1 22 72-4 54 13 37.9 28.2-2.5 57.5 16 55.5s72-29 104-40' stroke='%2300FDCF' stroke-width='10'/%3E%3C/g%3E%3Cdefs%3E%3CclipPath id='a'%3E%3Cpath fill='%23fff' d='M0 0h100v64H0z'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E%0A"); - background-size: auto 100%; - background-repeat: round; - background-position: 0em; -} -} + diff --git a/app/templates/admin/edit_user.html b/app/templates/admin/edit_user.html index 52c83b35b79137290a915b8a388696a3ff9e56d4..c3260edd12c1c3928cb3fe1c1f0fa43898797e90 100644 --- a/app/templates/admin/edit_user.html +++ b/app/templates/admin/edit_user.html @@ -1,89 +1,147 @@ {% extends 'base.html' %} {% block content %} +<head> + <script src="{{ url_for('static', filename='generic.js') }}"></script> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"> +</head> <div class="container mt-5"> - <div class="card mb-4"> - <div class="card-body"> - <h2 class="card-title">User Information</h2> - <p><strong>User Name:</strong> - <span id="userName">{{user.username}}</span> - <button class="btn btn-primary" onclick="editField('userName', this)">Update</button> - </p> - <p><strong>Email:</strong> - <span id="userEmail">{{user.email}}</span> - <button class="btn btn-primary" onclick="editField('userEmail', this)">Update</button> - </p> - <p><strong>Role:</strong> - <span id="userRole">{{user.role.name}}</span> - <button class="btn btn-primary" onclick="editField('userRole', this)">Update</button> - </p> - </div> + <div class="card mb-4"> + <div class="card-body"> + <h2 class="card-title">User Information</h2> + <input type="hidden" id="userId" value="{{ user.id }}"> + <p><strong>User Name:</strong> <span id="userName">{{ user.username }}</span> + <button class="btn btn-primary" onclick="editField('userName', this)">Update</button> + </p> + <p><strong>Email:</strong> <span id="userEmail">{{ user.email }}</span> + <button class="btn btn-primary" onclick="editField('userEmail', this)">Update</button> + </p> + <p><strong>Role:</strong> <span id="userRole">{{ user.role.name }}</span> + <button class="btn btn-primary" onclick="editField('userRole', this)">Update</button> + </p> </div> + </div> </div> <div class="text-center"> - <button class="btn btn-secondary btn-lg"> - Save Changes - </button> + <a class="btn btn-warning btn-lg" href="{{ url_for('admin.manage_users') }}"> + <i class="fas fa-arrow-left"></i> Go Back + </a> </div> - <style> - .input-container { - display: flex; - align-items: center; - } - .input-container input { - flex: 1; - max-width: 500px; - } - .input-container button { - margin-left: 10px; - } + .input-container { + display: flex; + align-items: center; + } + .input-container input, .input-container select { + flex: 1; + max-width: 500px; + } + .input-container button { + margin-left: 10px; + } </style> - <script> -function editField(fieldId, button) { + function editField(fieldId, button) { const field = document.getElementById(fieldId); const originalValue = field.textContent; - const input = document.createElement("input"); - input.type = "text"; - input.value = originalValue; - input.classList.add("form-control"); - input.autofocus = true; + if (fieldId === 'userRole') { + const select = document.createElement("select"); + select.classList.add("form-control"); + const roles = JSON.parse('{{ roles|tojson|safe }}'); + const currentRoleId = '{{ user.role_id }}'; + roles.forEach(role => { + const option = document.createElement("option"); + option.value = role.id; + option.textContent = role.name; + if (role.id == currentRoleId) { + option.selected = true; + } + select.appendChild(option); + }); + const container = document.createElement("div"); + container.classList.add("input-container"); + container.appendChild(select); + field.parentNode.replaceChild(container, field); - const container = document.createElement("div"); - container.classList.add("input-container"); - container.appendChild(input); + select.onblur = function() { + if (!container.contains(select)) return; + saveField(select, fieldId, button); + }; + button.innerText = 'Apply'; + button.onclick = function() { + saveField(select, fieldId, button); + }; + container.appendChild(button); + } else { + const input = document.createElement("input"); + input.type = "text"; + input.value = originalValue; + input.classList.add("form-control"); + input.autofocus = true; - field.parentNode.replaceChild(container, field); + const container = document.createElement("div"); + container.classList.add("input-container"); + container.appendChild(input); + field.parentNode.replaceChild(container, field); - input.onblur = function() { + input.onblur = function() { if (!container.contains(input)) return; saveField(input, fieldId, button); - }; - - button.innerText = 'Apply'; - button.onclick = function() { + }; + button.innerText = 'Apply'; + button.onclick = function() { saveField(input, fieldId, button); - }; - - container.appendChild(button); -} - -function saveField(input, fieldId, button) -{ - const newValue = input.value; + }; + container.appendChild(button); + } + } + function saveField(input, fieldId, button) { + let newValue; + if (fieldId === 'userRole') { + newValue = input.value; // Get the selected role ID + var newSpanText = input.options[input.selectedIndex].text; // Get the selected role name + } else { + newValue = input.value; + var newSpanText = newValue; + } const newSpan = document.createElement("span"); newSpan.id = fieldId; - newSpan.textContent = newValue; - + newSpan.textContent = newSpanText; const container = input.parentNode; container.parentNode.replaceChild(newSpan, container); - button.innerText = 'Update'; button.onclick = editField.bind(null, fieldId, button); - newSpan.parentNode.appendChild(button); -} + // Send update to the Flask route for validation and saving + updateUserField(fieldId, newValue); + } + + function updateUserField(fieldId, newValue) { + const data = { + user_id: '{{ user.id }}', + field: fieldId, + value: newValue + }; + fetch("{{ url_for('admin.update_user_field') }}", { + method: "POST", + headers: { + "Content-Type": "application/json", + 'X-CSRFToken': '{{ csrf_token() }}' + }, + body: JSON.stringify(data) + }) + .then(response => response.json()) + .then(data => { + if (!data.success) { + alert('Error updating field: ' + data.message); + location.reload(); + } + }) + .catch(error => { + alert('Error updating field: ' + error); + location.reload(); + }); + } </script> {% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index d8ab657335f94f23ffeb6c26094f4aa419e11207..e3a1194adea1e5cf862628d6d65fd778b2047342 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -119,14 +119,15 @@ <div class="footer"> <div class="footer_row"> <div class="footer_column"> - <div>About Us</div> - <div>Contact Us</div> + <div><a href="{{ url_for('profile.index') }}">Profile</a></div> + <div><a href="{{ url_for('main.about_us') }}">About Us</a></div> + <div><a href="{{ url_for('main.contact_us') }}">Contact Us</div> </div> <div class="footer_column"> - <a>About Us</a> - <a>Contact Us</a> + <div><a href="{{ url_for('main.faq') }}">FAQ</a></div> + <div><a href="{{ url_for('main.privacy_policy') }}">Privacy Policy</a></div> + <div><a href="{{ url_for('main.tos') }}">Terms of Service</a></div> </div> - <div class="footer_column"><a>Login</a></div> </div> <div class="footer_row"> <div class="copyright"> @@ -134,7 +135,7 @@ </div> </div> </div> - </footer> + </footer> {% endblock %} </div> </body> diff --git a/app/templates/bookings/listing.html b/app/templates/bookings/listing.html index f165b558bfee036608dfea21784f0cf59dca32fe..7e41f5196ea7dd797a006f05cdeaf7f90c002029 100644 --- a/app/templates/bookings/listing.html +++ b/app/templates/bookings/listing.html @@ -4,37 +4,42 @@ <div id="discountBanner" class="alert alert-success" role="alert" style="display: {% if discount > 0 %}block{% else %}none{% endif %};"> Special Offer! Get <span id="discountPercent">{{ discount }}</span>% off on your booking as you are booking <span id="daysAway">{{ days_away }}</span> days away! </div> - <div class="date-container"> - <div class="col-md-6 text-start mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> - <label class="form-label me-2">Departure Date:</label> - <div class="col-md-6"> - <input type="date" class="form-control" id="departDate" name="departDate" required value="{{ selected_date or today }}"> + <div class="row"> + <div class="col-md-6"> + <div class="date-container mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> + <label class="form-label me-2">Departure Date:</label> + <div class="col-md-6"> + <input type="date" class="form-control" id="departDate" name="departDate" required value="{{ selected_date or today }}"> + </div> + <button type="button" class="btn btn-warning ms-2" id="resetDate"> + <i class="fa-solid fa-rotate-right"></i> Reset Date + </button> </div> - <button type="button" class="btn btn-warning ms-2" id="resetDate"> - <i class="fa-solid fa-rotate-right"></i> Reset Date - </button> - </div> - <div class="col-md-6 text-start mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> - <label class="form-label me-2">Number of Seats:</label> - <div class="col-md-6"> - <select class="form-select" id="numSeats" name="numSeats"> - <option value="1">1</option> - <option value="2">2</option> - <option value="3">3</option> - <option value="4">4</option> - <option value="5">5</option> - </select> + <div class="mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> + <label class="form-label me-2">Number of Seats:</label> + <div class="col-md-6"> + <select class="form-select" id="numSeats" name="numSeats"> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + </select> + </div> </div> - </div> - <div class="col-md-6 text-start mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> - <label class="form-label me-2">Seat Type:</label> - <div class="col-md-6"> - <select class="form-select" id="seatType" name="seatType"> - <option value="economy" {% if seat_type == 'economy' %}selected{% endif %}>Economy</option> - <option value="business" {% if seat_type == 'business' %}selected{% endif %}>Business</option> - </select> + <div class="mb-3 d-flex align-items-center flex-md-nowrap flex-wrap"> + <label class="form-label me-2">Seat Type:</label> + <div class="col-md-6"> + <select class="form-select" id="seatType" name="seatType"> + <option value="economy" {% if seat_type == 'economy' %}selected{% endif %}>Economy</option> + <option value="business" {% if seat_type == 'business' %}selected{% endif %}>Business</option> + </select> + </div> </div> - </div> + </div> + <div class="col-md-6 d-flex justify-content-end"> + <img src="{{ url_for('main.upload_file', filename=main_image_url) }}" class="card-img-top" style="max-width: 200px;" loading="lazy" alt="Main Image"> + </div> </div> <h2 class="mb-4" style="text-align: center; margin-top: 40px;">Booking Details</h2> <div class="row"> @@ -43,7 +48,7 @@ <div class="card-body"> <h2 class="card-title">Departure Information</h2> <p><strong>Location:</strong> {{ listing.depart_location }}</p> - <p><strong>Time:</strong> {{ listing.depart_time }}</p> + <p><strong>Time:</strong> {{ depart_time }}</p> </div> </div> </div> @@ -52,7 +57,7 @@ <div class="card-body"> <h2 class="card-title">Destination Information</h2> <p><strong>Location:</strong> {{ listing.destination_location }}</p> - <p><strong>Time:</strong> {{ listing.destination_time }}</p> + <p><strong>Time:</strong> {{ destination_time }}</p> </div> </div> </div> diff --git a/app/templates/errors/quandary.html b/app/templates/errors/quandary.html index c699910b54fc50548d541b5d2a5051cb158140be..afa71393a9374ea052920fb06e81f87f707393ba 100644 --- a/app/templates/errors/quandary.html +++ b/app/templates/errors/quandary.html @@ -6,7 +6,7 @@ <div class="quandary-div"> <h1>Something went wrong</h1> <div class="container"> - <div><span>Something went wrong, if this continues please contact support</span></div> + <div><span>{{ error_message }}</span></div> <div class="button_2" onclick="history.back()"> <i class="fa-solid fa-circle-arrow-left"></i><span> Go back</span> </div> diff --git a/app/templates/index.html b/app/templates/index.html index 6990c262168cde80d46fde1b38a49330450c8043..90315b7aa513af497c6a882316b3a828b59cccf6 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -7,12 +7,25 @@ <script src="https://cdn.jsdelivr.net/npm/swiffy-slider@1.6.0/dist/js/swiffy-slider.min.js" crossorigin="anonymous" defer></script> <style> .slider-wrapper { - transform: scale(0.8); width: 100%; + margin-bottom: 30px; + } + .deals_text { + text-align: center; + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 20px; + } + .slider-nav { + opacity: 0.7; + } + .slider-nav:hover { + opacity: 1; } </style> </head> <div class="container mt-4"> + <h2 class="text-center mb-4">Book Your Trip</h2> <form id="travelForm" class="row g-3" action="{{ url_for('bookings.listings') }}" method="GET"> <div class="col-md-6"> <label for="departLocation" class="form-label">Departure Location:</label> @@ -53,37 +66,35 @@ </div> </form> </div> -<div> - <div class="deals_text"><span class="deals_underline">Currently Hot Locations</span></div> - <div class="slider-wrapper"> - <div class="swiffy-slider slider-item-show2 slider-item-reveal slider-nav-outside slider-nav-round slider-nav-visible slider-nav-loop slider-indicators-outside slider-indicators-round slider-indicators-dark slider-nav-animation slider-nav-animation-fadein slider-item-first-visible"> - <ul class="slider-container py-4"> - {% for listing in top_listings %} - <li class="slide-visible"> - <div class="card shadow h-100"> - <div class="ratio ratio-16x9"> - <th>{{top_listing_images[listing.id]}}</th> - <img src="{{ url_for('main.upload_file', filename=top_listing_images[listing.id]) }}" class="card-img-top" loading="lazy" alt="Main Image"> - </div> - <div class="card-body p-3 p-xl-4"> - <h3 class="card-title h5">{{listing.destination_location}}</h3> - <p class="card-text">Add Location description here once implemented</p> - <div><a href="#" class="btn btn-primary">Book now</a> +<div class="container mt-5"> + <div class="deals_text"><span style="font-size: 35px;">Currently Hot Deals</span></div> + <div class="slider-wrapper"> + <div class="swiffy-slider slider-item-show3 slider-item-reveal slider-nav-outside slider-nav-round slider-nav-visible slider-nav-loop slider-indicators-outside slider-indicators-round slider-indicators-dark slider-nav-animation slider-nav-animation-slide"> + <ul class="slider-container py-4"> + {% for listing in top_listings %} + <li class="slide-visible"> + <div class="card shadow h-100"> + <div class="ratio ratio-16x9"> + <img src="{{ url_for('main.upload_file', filename=top_listing_images[listing.id]) }}" class="card-img-top" loading="lazy" alt="Main Image"> + </div> + <div class="card-body p-3 p-xl-4"> + <h3 class="card-title h5">{{listing.destination_location}}</h3> + <div><a href="{{ url_for('bookings.listing', id=listing.id) }}" class="btn btn-primary">Book now</a> + </div> </div> </div> - </div> - </li> - {% endfor %} - </ul> + </li> + {% endfor %} + </ul> - <button type="button" class="slider-nav slider-nav-prev" aria-label="Go left"></button> - <button type="button" class="slider-nav slider-nav-next" aria-label="Go right"></button> + <button type="button" class="slider-nav slider-nav-prev" aria-label="Go left"></button> + <button type="button" class="slider-nav slider-nav-next" aria-label="Go right"></button> - <div class="slider-indicators"> - <button class="active" aria-label="Go to slide"></button> - <button aria-label="Go to slide"></button> - <button aria-label="Go to slide"></button> - <button aria-label="Go to slide"></button> + <div class="slider-indicators"> + <button class="active" aria-label="Go to slide"></button> + <button aria-label="Go to slide"></button> + <button aria-label="Go to slide"></button> + </div> </div> </div> </div> @@ -103,25 +114,20 @@ }); const departDateInput = document.getElementById('departDate'); - const resetDateButton = document.getElementById('resetDate'); - // Open date picker when the date field is clicked departDateInput.addEventListener('focus', (event) => { event.preventDefault(); departDateInput.showPicker(); }); - // Prevent any dates being changed by the user manually typing departDateInput.addEventListener('keydown', (event) => { event.preventDefault(); }); - // Set default date to today and prevent selecting past dates const today = new Date().toISOString().split('T')[0]; departDateInput.value = today; departDateInput.setAttribute('min', today); - // Set max date to 90 days from today let maxDate = new Date(); maxDate.setDate(maxDate.getDate() + 90); departDateInput.setAttribute('max', maxDate.toISOString().split('T')[0]); diff --git a/app/templates/main/about_us.html b/app/templates/main/about_us.html new file mode 100644 index 0000000000000000000000000000000000000000..c7129788a122566a571e649df49e02feb8cffed1 --- /dev/null +++ b/app/templates/main/about_us.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container"> + <div class="jumbotron mt-5"> + <h1 class="display-4">About Us</h1> + <p class="lead">Learn more about Horizon Travels and our mission.</p> + <hr class="my-4"> + </div> + <div class="mt-4"> + <h2>Our Mission</h2> + <p>At Horizon Travels, our mission is to make travel planning seamless and enjoyable for everyone.</p> + + <h2>Our Story</h2> + <p>Founded in 2024, Horizon Travels was born out of a passion for exploration and a desire to make travel accessible to all. We've helped travelers find their perfect journey.</p> + + <h2>Our Team</h2> + <p>Our team is made up of dedicated travel enthusiasts who are here to assist you every step of the way. Whether you're planning a short trip or a long adventure, we're here to help.</p> + + <h2>Contact Us</h2> + <p>If you have any questions or need assistance, feel free to reach out to us.</p> + </div> +</div> +{% endblock %} diff --git a/app/templates/main/contact_us.html b/app/templates/main/contact_us.html new file mode 100644 index 0000000000000000000000000000000000000000..8a4de5ba9ba1eb667b4df8a0dc923af528a1a299 --- /dev/null +++ b/app/templates/main/contact_us.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container"> + <div class="jumbotron mt-5"> + <h1 class="display-4">Contact Us</h1> + <p class="lead">We'd love to hear from you. Reach out to us with any questions or feedback.</p> + <hr class="my-4"> + </div> + <div class="mt-4"> + <h2>Email</h2> + <p>You can email us at <a href="mailto:support@horizontravels.com">support@horizontravels.com</a>.</p> + + <h2>Phone</h2> + <p>Feel free to call us at (123) 456-7890. Our support team is available 24/7 to assist with any queries.</p> + + <h2>Address</h2> + <p>Visit us at our office: + <br>123 Travel Street + <br>Bristol, BS1 1AA + <br>United Kingdom + </p> + </div> +</div> +{% endblock %} diff --git a/app/templates/main/faq.html b/app/templates/main/faq.html new file mode 100644 index 0000000000000000000000000000000000000000..003ffdd5324f42af6ce9adffc38bba7488036a75 --- /dev/null +++ b/app/templates/main/faq.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container"> + <div class="jumbotron mt-5"> + <h1 class="display-4">Frequently Asked Questions</h1> + <p class="lead">Find answers to common questions about Horizon Travels.</p> + <hr class="my-4"> + </div> + <div class="mt-4"> + <h2>What services do you offer?</h2> + <p>We provide comprehensive travel planning services for flights.</p> + + <h2>How can I manage my bookings?</h2> + <p>You can manage your bookings by visiting the "Manage Bookings" section in your dashboard.</p> + + <h2>What is your cancellation policy?</h2> + <p>Our cancellation policy varies depending on the booking. Please reach out to our support team for more information.</p> + + <h2>How can I contact support?</h2> + <p>You can contact our support team via email at <a href="mailto:support@horizontravels.com">support@horizontravels.com</a> or by phone at (123) 456-7890.</p> + + <h2>Do you offer travel insurance?</h2> + <p>No, unfortunately at this time we do not offer insurance options.</p> + </div> +</div> +{% endblock %} diff --git a/app/templates/main/privacy_policy.html b/app/templates/main/privacy_policy.html new file mode 100644 index 0000000000000000000000000000000000000000..e8aca2c24761129f6cd2207912b28d3aa91452ec --- /dev/null +++ b/app/templates/main/privacy_policy.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container"> + <div class="jumbotron mt-5"> + <h1 class="display-4">Privacy Policy</h1> + <p class="lead">Your privacy is important to us. Please read our policy carefully.</p> + <hr class="my-4"> + </div> + <div class="mt-4"> + <h2>1. Information Collection</h2> + <p>We collect personal information that you provide to us when you use our services.</p> + + <h2>2. Use of Information</h2> + <p>We use the information to provide and improve our services, communicate with you, and comply with legal obligations.</p> + + <h2>3. Information Sharing</h2> + <p>We do not share your personal information with third parties except as required by law.</p> + + <h2>4. Data Security</h2> + <p>We implement security measures to protect your information from unauthorized access and disclosure.</p> + + <h2>5. Your Rights</h2> + <p>You have the right to access, update, and delete your personal information. Contact us for any requests related to your data.</p> + + <h2>6. Changes to Policy</h2> + <p>We may update our privacy policy periodically. We will notify you of any changes by posting the updated policy on our site.</p> + </div> +</div> +{% endblock %} diff --git a/app/templates/main/tos.html b/app/templates/main/tos.html new file mode 100644 index 0000000000000000000000000000000000000000..d62699ed40ac1f888a581590fdcc80944548be14 --- /dev/null +++ b/app/templates/main/tos.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block content %} +<div class="container"> + <div class="jumbotron mt-5"> + <h1 class="display-4">Terms of Service</h1> + <p class="lead">Welcome to Horizon Travels. Please read these terms carefully.</p> + <hr class="my-4"> + </div> + <div class="mt-4"> + <h2>1. Acceptance of Terms</h2> + <p>By using our services, you agree to be bound by these terms.</p> + + <h2>2. Modification of Terms</h2> + <p>We reserve the right to change these terms at any time. We will notify users of any changes by posting the updated terms on our site.</p> + + <h2>3. User Responsibilities</h2> + <p>Users agree to use the site for lawful purposes and not to engage in any conduct that could harm the site or other users.</p> + + <h2>4. Limitation of Liability</h2> + <p>Horizon Travels is not liable for any damages that may occur as a result of using our services.</p> + + <h2>5. Privacy Policy</h2> + <p>Your privacy is important to us. Please review our privacy policy for more information.</p> + + <h2>6. Governing Law</h2> + <p>These terms are governed by the laws of the United Kingdom.</p> + </div> +</div> +{% endblock %} diff --git a/app/templates/profile/manage_bookings.html b/app/templates/profile/manage_bookings.html index 949b84ac32f4b7b276641e37cb2af327e50abd50..c2a0edc76a5cf68069dab3bc3560ec7be7d8e28e 100644 --- a/app/templates/profile/manage_bookings.html +++ b/app/templates/profile/manage_bookings.html @@ -19,6 +19,7 @@ <th>Departure Date</th> <th>Departure Location</th> <th>Destination Location</th> + <th>Cancelled</th> <th>Actions</th> </tr> </thead> @@ -30,6 +31,7 @@ <td>{{ booking.departure_date }}</td> <td>{{ booking.departure_location }}</td> <td>{{ booking.destination_location }}</td> + <td>{{ 'Yes' if booking.cancelled else 'No' }}</td> <td class="dt-center"> <div class="dropdown"> <button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false"> @@ -92,7 +94,11 @@ <div class="mb-3"> <label for="booking_date" class="form-label">Booking Date:</label> <input type="date" class="form-control" id="booking_date" name="booking_date"> - </div> + </div> + <div class="mb-3"> + <label for="exclude_cancelled" class="form-label">Exclude Cancelled:</label> + <input type="checkbox" class="form-check-input" id="exclude_cancelled" name="exclude_cancelled"> + </div> </form> </div> <div class="modal-footer"> @@ -159,6 +165,7 @@ d.destination_location = $('#destination_location').val() ? $('#destination_location').val().join(',') : ''; d.depart_date = $('#depart_date').val(); d.booking_date = $('#booking_date').val(); + d.exclude_cancelled = $('#exclude_cancelled').is(':checked'); } }, columns: [ @@ -167,6 +174,7 @@ { data: 'depart_date' }, { data: 'depart_location' }, { data: 'destination_location' }, + { data: 'cancelled' }, { data: null, className: "dt-center", @@ -223,8 +231,6 @@ alert('An error occurred while cancelling the booking.'); } }); - } else { - alert("Please type 'CONFIRM' to proceed."); } }); }); diff --git a/app/templates/profile/view_booking.html b/app/templates/profile/view_booking.html index 23e9fe264efe7228ed65e417e3be6cdf2cc94ab8..65d71a8958194c356a876b023f3bdeaa3f73632f 100644 --- a/app/templates/profile/view_booking.html +++ b/app/templates/profile/view_booking.html @@ -29,14 +29,22 @@ <div class="card shadow-sm"> <div class="card-body text-center"> <h3 class="card-title mb-4">Re-Download Booking Details</h3> - <div class="d-flex justify-content-center"> - <form action="{{ url_for('bookings.generate_receipt', id=booking.id) }}" method="get" class="d-inline"> - <button type="submit" class="btn btn-success btn-lg mr-2" style="margin-right: 25px">Download Receipt</button> - </form> - <form action="{{ url_for('bookings.generate_ticket', id=booking.id) }}" method="get" class="d-inline"> - <button type="submit" class="btn btn-primary btn-lg">Download Plane Ticket</button> - </form> - </div> + {% if booking.cancelled %} + <div class="alert alert-danger" role="alert"> + This booking has been cancelled. Download options are no longer available + </div> + <button type="button" class="btn btn-success btn-lg mr-2" disabled>Download Receipt</button> + <button type="button" class="btn btn-primary btn-lg" disabled>Download Plane Ticket</button> + {% else %} + <div class="d-flex justify-content-center"> + <form action="{{ url_for('bookings.generate_receipt', id=booking.id) }}" method="get" class="d-inline"> + <button type="submit" class="btn btn-success btn-lg mr-2" style="margin-right: 25px">Download Receipt</button> + </form> + <form action="{{ url_for('bookings.generate_ticket', id=booking.id) }}" method="get" class="d-inline"> + <button type="submit" class="btn btn-primary btn-lg">Download Plane Ticket</button> + </form> + </div> + {% endif %} </div> </div> </div> diff --git a/migrations/versions/22de5b143d05_create_user_roles.py b/migrations/versions/22de5b143d05_create_user_roles.py deleted file mode 100644 index 5e6a5869f16c6b4644fa9222896cee3d46486009..0000000000000000000000000000000000000000 --- a/migrations/versions/22de5b143d05_create_user_roles.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Create user roles - -Revision ID: 22de5b143d05 -Revises: 9a8cc1906445 -Create Date: 2025-01-06 13:40:11.307880 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.sql import table, column - -# revision identifiers, used by Alembic. -revision = '22de5b143d05' -down_revision = '9a8cc1906445' -branch_labels = None -depends_on = None - -roles_table = table('roles', - column('id', sa.Integer), - column('name', sa.String), - column('description', sa.String) -) - -def upgrade(): - roles = [ - {'name': 'super-admin', 'description': 'Super Admin, all admin perms and can create new admins'}, - {'name': 'admin', 'description': 'Can create/delete and modify bookings'}, - {'name': 'user', 'description': 'Standard user'} - ] - - op.bulk_insert(roles_table, roles) - -def downgrade(): - op.execute('DELETE FROM roles WHERE name IN ("super-admin", "admin", "user")') diff --git a/migrations/versions/28932a02e785_create_listing_availibility_table.py b/migrations/versions/28932a02e785_create_listing_availibility_table.py deleted file mode 100644 index 9c9e4985c1cdc1a6755f9f0b1340e583cdf048b0..0000000000000000000000000000000000000000 --- a/migrations/versions/28932a02e785_create_listing_availibility_table.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Create listing availibility table - -Revision ID: 28932a02e785 -Revises: e49c7ce461b6 -Create Date: 2025-01-08 18:41:40.962877 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -from sqlalchemy.engine.reflection import Inspector - -# revision identifiers, used by Alembic. -revision = '28932a02e785' -down_revision = 'e49c7ce461b6' -branch_labels = None -depends_on = None - -def upgrade(): - bind = op.get_bind() - inspector = Inspector.from_engine(bind) - if 'listing_availability' not in inspector.get_table_names(): - op.create_table( - 'listing_availability', - sa.Column('id', sa.Integer(), nullable=False, autoincrement=True, primary_key=True), - sa.Column('listing_id', sa.Integer(), nullable=False), - sa.Column('business_tickets', sa.Integer(), nullable=False), - sa.Column('economy_tickets', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['listing_id'], ['listings.id'], ondelete='CASCADE') - ) diff --git a/migrations/versions/489bab9aaf4f_add_listing_images_table.py b/migrations/versions/489bab9aaf4f_add_listing_images_table.py deleted file mode 100644 index 3a082241c4409eb7121047536d626504b06cd225..0000000000000000000000000000000000000000 --- a/migrations/versions/489bab9aaf4f_add_listing_images_table.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Add listing images table - -Revision ID: 489bab9aaf4f -Revises: 6791cbf31235 -Create Date: 2024-11-05 11:13:50.215159 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.engine.reflection import Inspector - - -# revision identifiers, used by Alembic. -revision = '489bab9aaf4f' -down_revision = '6791cbf31235' -branch_labels = None -depends_on = None - - -def upgrade(): - bind = op.get_bind() - inspector = Inspector.from_engine(bind) - - # Check if the 'users' table exists - if 'listing_images' not in inspector.get_table_names(): - op.create_table( - 'listing_images', - sa.Column('id', sa.Integer(), nullable=False, autoincrement=True, primary_key=True), - sa.Column('listing_id', sa.Integer(), nullable=False), - sa.Column('image_location', sa.String(255), nullable=False), - sa.Column('image_description', sa.String(255), nullable=True) - ) diff --git a/migrations/versions/6791cbf31235_add_listing_table.py b/migrations/versions/6791cbf31235_add_listing_table.py deleted file mode 100644 index a8202a88d6d0a3597a7131ffe9ab58ea5a5ffaf4..0000000000000000000000000000000000000000 --- a/migrations/versions/6791cbf31235_add_listing_table.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Add listing table - -Revision ID: 6791cbf31235 -Revises: ac9d4555724d -Create Date: 2024-11-05 10:36:32.872815 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.engine.reflection import Inspector - - -# revision identifiers, used by Alembic. -revision = '6791cbf31235' -down_revision = 'ac9d4555724d' -branch_labels = None -depends_on = None - - -def upgrade(): - bind = op.get_bind() - inspector = Inspector.from_engine(bind) - - # Check if the 'users' table exists - if 'listings' not in inspector.get_table_names(): - op.create_table( - 'listings', - sa.Column('id', sa.Integer(), nullable=False, autoincrement=True, primary_key=True), - sa.Column('depart_location', sa.String(255), nullable=False), - sa.Column('depart_time', sa.DateTime(), nullable=False), - sa.Column('destination_location', sa.String(255), nullable=False), - sa.Column('destination_time', sa.DateTime(), nullable=False), - sa.Column('fair_cost', sa.Float(2), nullable=False), - sa.Column('transport_type', sa.String(255), nullable=False), - sa.Column('business_tickets', sa.Integer(), nullable=False), - sa.Column('economy_tickets', sa.Integer(), nullable=False) - ) \ No newline at end of file diff --git a/migrations/versions/68d89ef13132_add_main_listing_image_column.py b/migrations/versions/68d89ef13132_add_main_listing_image_column.py deleted file mode 100644 index 9ccafd816ab4ff5c783ae3c73477b67d5737efa0..0000000000000000000000000000000000000000 --- a/migrations/versions/68d89ef13132_add_main_listing_image_column.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Add main Listing Image Column - -Revision ID: 68d89ef13132 -Revises: 489bab9aaf4f -Create Date: 2024-11-29 10:29:38.126811 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '68d89ef13132' -down_revision = '489bab9aaf4f' -branch_labels = None -depends_on = None - - -def upgrade(): - op.add_column( - 'listing_images', - sa.Column('main_image', sa.Boolean(), nullable=False, server_default=sa.sql.expression.false()) - ) \ No newline at end of file diff --git a/migrations/versions/6c7070736062_add_role_id_to_users_table.py b/migrations/versions/6c7070736062_add_role_id_to_users_table.py deleted file mode 100644 index b08cdd25b05f0bd104793ef65c8743f4092a4236..0000000000000000000000000000000000000000 --- a/migrations/versions/6c7070736062_add_role_id_to_users_table.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Add role_id to users table - -Revision ID: 6c7070736062 -Revises: 22de5b143d05 -Create Date: 2025-01-06 20:16:19.191868 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = '6c7070736062' -down_revision = '22de5b143d05' -branch_labels = None -depends_on = None - -def upgrade(): - # Add column role_id to users table - op.add_column('users', sa.Column('role_id', sa.Integer(), nullable=True)) - op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id']) - -def downgrade(): - # Remove column role_id from users table - op.drop_constraint(None, 'users', type_='foreignkey') - op.drop_column('users', 'role_id') diff --git a/migrations/versions/77815275598c_.py b/migrations/versions/77815275598c_.py deleted file mode 100644 index 6c4f331415488b95ad456f497c7326c0b0f2846e..0000000000000000000000000000000000000000 --- a/migrations/versions/77815275598c_.py +++ /dev/null @@ -1,24 +0,0 @@ -"""empty message - -Revision ID: 77815275598c -Revises: 6c7070736062 -Create Date: 2025-01-08 16:57:53.560001 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '77815275598c' -down_revision = '6c7070736062' -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/migrations/versions/9a8cc1906445_add_fs_uniquifier_field_to_user_model.py b/migrations/versions/9a8cc1906445_add_fs_uniquifier_field_to_user_model.py deleted file mode 100644 index 7bacfee528a6aff984243944fedccc2f36974923..0000000000000000000000000000000000000000 --- a/migrations/versions/9a8cc1906445_add_fs_uniquifier_field_to_user_model.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Add fs_uniquifier field to User model - -Revision ID: 9a8cc1906445 -Revises: 68d89ef13132 -Create Date: 2025-01-06 12:52:57.272220 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -import os - -# revision identifiers, used by Alembic. -revision = '9a8cc1906445' -down_revision = '68d89ef13132' -branch_labels = None -depends_on = None - -def column_exists(table_name, column_name): - inspector = sa.inspect(op.get_bind()) - return column_name in [col['name'] for col in inspector.get_columns(table_name)] - -def index_exists(table_name, index_name): - inspector = sa.inspect(op.get_bind()) - indexes = inspector.get_indexes(table_name) - return any(index['name'] == index_name for index in indexes) - -def upgrade(): - # Conditionally create roles table - if not op.get_bind().dialect.has_table(op.get_bind(), "roles"): - op.create_table('roles', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(length=80), nullable=True), - sa.Column('description', sa.String(length=255), nullable=True), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('name') - ) - - # Conditionally create roles_users table - if not op.get_bind().dialect.has_table(op.get_bind(), "roles_users"): - op.create_table('roles_users', - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('role_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['role_id'], ['roles.id']), - sa.ForeignKeyConstraint(['user_id'], ['users.id']) - ) - - with op.batch_alter_table('listing_images', schema=None) as batch_op: - batch_op.alter_column('main_image', - existing_type=mysql.TINYINT(display_width=1), - type_=sa.SmallInteger(), - existing_nullable=False, - existing_server_default=sa.text("'0'")) - - # Assign unique values to fs_uniquifier for existing users before adding the unique constraint - conn = op.get_bind() - users = conn.execute(sa.text("SELECT id FROM users WHERE fs_uniquifier IS NULL OR fs_uniquifier = ''")).fetchall() - for user in users: - conn.execute(sa.text("UPDATE users SET fs_uniquifier = :fs_uniquifier WHERE id = :id"), {'fs_uniquifier': os.urandom(32).hex(), 'id': user.id}) - - with op.batch_alter_table('users', schema=None) as batch_op: - if index_exists('users', 'api_token'): - batch_op.drop_index('api_token') - batch_op.create_unique_constraint(None, ['fs_uniquifier']) - if column_exists('users', 'token_expiry'): - batch_op.drop_column('token_expiry') - if column_exists('users', 'api_token'): - batch_op.drop_column('api_token') - if column_exists('users', 'role_id'): - batch_op.drop_column('role_id') - -def downgrade(): - with op.batch_alter_table('users', schema=None) as batch_op: - batch_op.add_column(sa.Column('role_id', mysql.SMALLINT(), server_default=sa.text("'1'"), autoincrement=False, nullable=False)) - batch_op.add_column(sa.Column('api_token', mysql.VARCHAR(length=255), nullable=True)) - batch_op.add_column(sa.Column('token_expiry', mysql.DATETIME(), nullable=True)) - batch_op.drop_constraint(None, type_='unique') - batch_op.create_index('api_token', ['api_token'], unique=True) - batch_op.drop_column('fs_uniquifier') - - with op.batch_alter_table('listing_images', schema=None) as batch_op: - batch_op.alter_column('main_image', - existing_type=sa.SmallInteger(), - type_=mysql.TINYINT(display_width=1), - existing_nullable=False, - existing_server_default=sa.text("'0'")) - - op.drop_table('roles_users') - op.drop_table('roles') diff --git a/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py b/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py deleted file mode 100644 index 26f22ae1471c03464d4d42a94743b849c47966e1..0000000000000000000000000000000000000000 --- a/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Add api token and expiry - -Revision ID: ac9d4555724d -Revises: -Create Date: 2024-11-01 10:56:05.827705 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -from sqlalchemy.engine.reflection import Inspector - -# revision identifiers, used by Alembic. -revision = 'ac9d4555724d' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - bind = op.get_bind() - inspector = Inspector.from_engine(bind) - - # Check if the 'users' table exists - if 'users' not in inspector.get_table_names(): - op.create_table( - 'users', - sa.Column('id', sa.Integer(), nullable=False, autoincrement=True, primary_key=True), - sa.Column('username', sa.String(255), nullable=False, unique=True), - sa.Column('email', sa.String(255), nullable=False, unique=True), - sa.Column('password', sa.String(255), nullable=False), - sa.Column('role_id', sa.SmallInteger(), nullable=False, server_default='3') - ) - -def downgrade(): - op.drop_table('users') diff --git a/migrations/versions/ce28a5ddecee_change_depart_time_and_destination_time_.py b/migrations/versions/ce28a5ddecee_change_depart_time_and_destination_time_.py deleted file mode 100644 index d8cb78142f271d8730323806f86c3c570c5f1ce5..0000000000000000000000000000000000000000 --- a/migrations/versions/ce28a5ddecee_change_depart_time_and_destination_time_.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Change depart_time and destination_time from DateTime to Time - -Revision ID: ce28a5ddecee -Revises: 28932a02e785 -Create Date: 2025-01-17 14:36:45.488306 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -# revision identifiers, used by Alembic. -revision = 'ce28a5ddecee' -down_revision = '28932a02e785' -branch_labels = None -depends_on = None - - -def upgrade(): - with op.batch_alter_table('listing_images', schema=None) as batch_op: - batch_op.create_foreign_key(None, 'listings', ['listing_id'], ['id']) - - with op.batch_alter_table('listings', schema=None) as batch_op: - batch_op.alter_column('depart_time', - existing_type=mysql.DATETIME(), - type_=sa.Time(), - existing_nullable=False) - batch_op.alter_column('destination_time', - existing_type=mysql.DATETIME(), - type_=sa.Time(), - existing_nullable=False) diff --git a/migrations/versions/e49c7ce461b6_create_bookings_table.py b/migrations/versions/e49c7ce461b6_create_bookings_table.py deleted file mode 100644 index 142d587757421070fdc9c0966a51fde4ab7ac536..0000000000000000000000000000000000000000 --- a/migrations/versions/e49c7ce461b6_create_bookings_table.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Create bookings table - -Revision ID: e49c7ce461b6 -Revises: 77815275598c -Create Date: 2025-01-08 17:08:51.080297 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql -from sqlalchemy.engine.reflection import Inspector - -# revision identifiers, used by Alembic. -revision = 'e49c7ce461b6' -down_revision = '77815275598c' -branch_labels = None -depends_on = None - -def upgrade(): - bind = op.get_bind() - inspector = Inspector.from_engine(bind) - if 'bookings' not in inspector.get_table_names(): - op.create_table( - 'bookings', - sa.Column('id', sa.Integer(), nullable=False, autoincrement=True, primary_key=True), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('listing_id', sa.Integer(), nullable=False), - sa.Column('amount_paid', sa.Integer(), nullable=False), - sa.Column('cancelled', sa.Boolean(), nullable=False), - sa.ForeignKeyConstraint(['listing_id'], ['listings.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE') - ) - - #Remove columns from listing as moved to new table - op.drop_column('listings', 'economy_tickets') - op.drop_column('listings', 'business_tickets') -