diff --git a/app/api/routes.py b/app/api/routes.py index ee448e573f754c83d2db28fed8acf2a5086ba7c8..2503793f50f195a0bf7e54f7d1e2a234dcdf4731 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,6 +1,8 @@ from flask import jsonify, request from app.api import bp from app.models import User, Listings +from app import db +from app import admin_permission, permission_required, super_admin_permission import json @bp.route('/user_id/<int:id>', methods=['GET']) @@ -58,8 +60,6 @@ def create_listing(): "destination_time": "2024-12-01T18:00:00", "fair_cost": 500.00, "transport_type": "Airplane", - "business_tickets": 10, - "economy_tickets": 50 } # Extract the required fields @@ -69,10 +69,8 @@ def create_listing(): destination_time = datetime.strptime(data['destination_time'], '%Y-%m-%dT%H:%M:%S') fair_cost = data['fair_cost'] transport_type = data['transport_type'] - business_tickets = data['business_tickets'] - economy_tickets = data['economy_tickets'] - result = Listings.create_listing(depart_location, depart_time, destination_location, destination_time, fair_cost, transport_type, business_tickets, economy_tickets) + result = Listings.create_listing(depart_location, depart_time, destination_location, destination_time, fair_cost, transport_type) if result is None: return jsonify({'error': 'User not found'}), 404 @@ -90,34 +88,52 @@ def create_listing(): # Sample data data = [ - {"Name": "Tiger Nixon", "Position": "System Architect", "Office": "Edinburgh", "Age": 61, "StartDate": "2011-04-25", "Salary": "$320,800"}, - {"Name": "Garrett Winters", "Position": "Accountant", "Office": "Tokyo", "Age": 63, "StartDate": "2011-07-25", "Salary": "$170,750"}, - {"Name": "Ashton Cox", "Position": "Junior Technical Author", "Office": "San Francisco", "Age": 66, "StartDate": "2009-01-12", "Salary": "$86,000"}, - {"Name": "Cedric Kelly", "Position": "Senior Javascript Developer", "Office": "Edinburgh", "Age": 22, "StartDate": "2012-03-29", "Salary": "$433,060"}, - {"Name": "Airi Satou", "Position": "Accountant", "Office": "Tokyo", "Age": 33, "StartDate": "2008-11-28", "Salary": "$162,700"}, - {"Name": "Brielle Williamson", "Position": "Integration Specialist", "Office": "New York", "Age": 61, "StartDate": "2012-12-02", "Salary": "$372,000"}, - {"Name": "Herrod Chandler", "Position": "Sales Assistant", "Office": "San Francisco", "Age": 59, "StartDate": "2012-08-06", "Salary": "$137,500"}, - {"Name": "Rhona Davidson", "Position": "Integration Specialist", "Office": "Tokyo", "Age": 55, "StartDate": "2010-10-14", "Salary": "$327,900"}, - {"Name": "Colleen Hurst", "Position": "Javascript Developer", "Office": "San Francisco", "Age": 39, "StartDate": "2009-09-15", "Salary": "$205,500"}, - {"Name": "Sonya Frost", "Position": "Software Engineer", "Office": "Edinburgh", "Age": 23, "StartDate": "2008-12-13", "Salary": "$103,600"}, - {"Name": "Jena Gaines", "Position": "Office Manager", "Office": "London", "Age": 30, "StartDate": "2008-12-19", "Salary": "$90,560"}, - {"Name": "Quinn Flynn", "Position": "Support Lead", "Office": "Edinburgh", "Age": 22, "StartDate": "2013-03-03", "Salary": "$342,000"}, - {"Name": "Charde Marshall", "Position": "Regional Director", "Office": "San Francisco", "Age": 36, "StartDate": "2008-10-16", "Salary": "$470,600"}, - {"Name": "Haley Kennedy", "Position": "Senior Marketing Designer", "Office": "London", "Age": 43, "StartDate": "2012-12-18", "Salary": "$313,500"}, - {"Name": "Tatyana Fitzpatrick", "Position": "Regional Director", "Office": "London", "Age": 19, "StartDate": "2010-03-17", "Salary": "$385,750"}, - {"Name": "Michael Silva", "Position": "Marketing Designer", "Office": "London", "Age": 66, "StartDate": "2012-11-27", "Salary": "$198,500"}, - {"Name": "Paul Byrd", "Position": "Chief Financial Officer (CFO)", "Office": "New York", "Age": 64, "StartDate": "2010-06-09", "Salary": "$725,000"}, - {"Name": "Gloria Little", "Position": "Systems Administrator", "Office": "New York", "Age": 59, "StartDate": "2009-04-10", "Salary": "$237,500"}, - {"Name": "Bradley Greer", "Position": "Software Engineer", "Office": "London", "Age": 41, "StartDate": "2012-10-13", "Salary": "$132,000"}, - {"Name": "Dai Rios", "Position": "Personnel Lead", "Office": "Edinburgh", "Age": 35, "StartDate": "2012-09-26", "Salary": "$217,500"}, - {"Name": "Jenette Caldwell", "Position": "Development Lead", "Office": "New York", "Age": 30, "StartDate": "2011-09-03", "Salary": "$345,000"}, + {"depart_location": "Tiger Nixon", "depart_time": "System Architect", "destination_location": "Edinburgh", "destination_time": 61, "fair_cost": "2011-04-25", "transport_type": "$320,800"}, ] -@bp.route('/api/get_data', methods=['GET']) +@bp.route('get_data', methods=['GET']) +@permission_required(super_admin_permission) def get_data(): - min_age = request.args.get('min_age', type=int, default=0) - max_age = request.args.get('max_age', type=int, default=100) - - filtered_data = [entry for entry in data if min_age <= entry['Age'] <= max_age] - return jsonify(filtered_data) + query = db.session.query(Listings) + + # Retrieve filter parameters from the request + depart_location = request.args.get('depart_location') + destination_location = request.args.get('destination_location') + min_depart_time = request.args.get('min_depart_time') + max_depart_time = request.args.get('max_depart_time') + min_fair_cost = request.args.get('min_fair_cost') + max_fair_cost = request.args.get('max_fair_cost') + transport_type = request.args.get('transport_type') + + # Apply filters dynamically + if depart_location: + query = query.filter(Listings.depart_location == depart_location) + if destination_location: + query = query.filter(Listings.destination_location == destination_location) + if min_depart_time: + query = query.filter(Listings.depart_time >= min_depart_time) + if max_depart_time: + query = query.filter(Listings.depart_time <= max_depart_time) + if min_fair_cost: + query = query.filter(Listings.fair_cost >= min_fair_cost) + if max_fair_cost: + query = query.filter(Listings.fair_cost <= max_fair_cost) + if transport_type: + query = query.filter(Listings.transport_type == transport_type) + + # Fetch the filtered results in a single query + filtered_data = query.all() + + # Convert the results to a list of dictionaries + result = [ + { + 'depart_location': listing.depart_location, + 'depart_time': listing.depart_time, + 'destination_location': listing.destination_location, + 'destination_time': listing.destination_time, + 'fair_cost': listing.fair_cost, + 'transport_type': listing.transport_type + } for listing in filtered_data + ] + return jsonify(result) diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 998d5e77e3a0ba65ab396c3ae661d3fa8b84bc3c..0078b513000b29eecdcd31b7955369db02f1eae8 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -20,4 +20,11 @@ def redirect_index(): @bp.route('/listings') def listings(): - return render_template('bookings/listings.html') + all_listings = Listings.get_all_listings() + + return render_template('bookings/listings.html', items=all_listings) + +@bp.route('/listing/<int:id>') +def show_listing(id): + Listings.get + return render_template('bookings/listings.html', id=1) \ No newline at end of file diff --git a/app/models/listing_images.py b/app/models/listing_images.py index a68b52d875b8078a2d11a056248f7beaddf4ae54..30152a45f10bc201a669ff757080f34e38724537 100644 --- a/app/models/listing_images.py +++ b/app/models/listing_images.py @@ -1,25 +1,24 @@ -from flask import request, jsonify +from sqlalchemy import Integer, ForeignKey +from sqlalchemy.orm import relationship from app import db class ListingImages(db.Model): __tablename__ = 'listing_images' id = db.Column(db.Integer(), nullable=False, primary_key=True) - listing_id = db.Column(db.Integer(), nullable=False) 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) - # listings_id = mapped_column(ForeignKey("listings.id")) - # listings = relationship("Listings", back_populates="listing_images") + listing_id = db.Column(Integer, ForeignKey('listings.id'), nullable=False) + listing = relationship("Listings", back_populates="listing_images") @classmethod def get_selected_main_images(cls, listing_ids): - listing_images = cls.query.filter( - cls.listing_id.in_(listing_ids), - cls.main_image == 1 + cls.listing_id.in_(listing_ids), + cls.main_image == 1 ).all() - + ordered_listing_images = {listing.listing_id: listing.image_location for listing in listing_images} - - return ordered_listing_images \ No newline at end of file + + return ordered_listing_images diff --git a/app/models/listings.py b/app/models/listings.py index 6c79da147ad17c0b63e316fe232fd9a3e09e1163..ee402f0c8319154e1969c7057ed29546832726c7 100644 --- a/app/models/listings.py +++ b/app/models/listings.py @@ -1,4 +1,5 @@ -from flask import request, jsonify +from sqlalchemy.orm import relationship +from sqlalchemy import Integer, ForeignKey from app import db class Listings(db.Model): @@ -9,38 +10,34 @@ class Listings(db.Model): depart_time = db.Column(db.DateTime(), nullable=False) destination_location = db.Column(db.String(255), nullable=False) destination_time = db.Column(db.DateTime(), nullable=False) - fair_cost = db.Column(db.Float(2), nullable=False) + fair_cost = db.Column(db.Float(), nullable=False) # Removed (2) from Float, it's not required transport_type = db.Column(db.String(255), nullable=False) - business_tickets = db.Column(db.Integer(), nullable=False) - economy_tickets = db.Column(db.Integer(), nullable=False) - # listing_images = relationship("ListingImages", back_populates="listing_images") + listing_images = relationship("ListingImages", back_populates="listing", cascade="all, delete-orphan") @classmethod def get_all_listings(cls): - return cls.query.all() - + return cls.query.all() + @classmethod - def create_listing(cls, depart_location, depart_time, destination_location, destination_time, fair_cost, transport_type, business_tickets, economy_tickets): + def create_listing(cls, depart_location, depart_time, destination_location, destination_time, fair_cost, transport_type): new_flight = cls(depart_location=depart_location, depart_time=depart_time, destination_location=destination_location, destination_time=destination_time, fair_cost=fair_cost, - transport_type=transport_type, - business_tickets=business_tickets, - economy_tickets=economy_tickets) + transport_type=transport_type) - # Add the new flight to the database and commit db.session.add(new_flight) db.session.commit() return new_flight - #return cls.query.all() - - @classmethod - def get_top_listings(cls, amount_of_listings = 5): - return cls.query.order_by( - cls.economy_tickets, - cls.business_tickets - ).limit(amount_of_listings).all() - + + @classmethod + def get_top_listings(cls, amount_of_listings=5): + return cls.query.limit(amount_of_listings).all() + + +# .order_by( +# cls.economy_tickets, +# cls.business_tickets +# ). \ No newline at end of file diff --git a/app/templates/admin/manage_bookings.html b/app/templates/admin/manage_bookings.html index 2cfb45a4515223289bc0a64506885a3766acebef..37a5e2fdaa058b6910fbfc3c004987adbb79df1c 100644 --- a/app/templates/admin/manage_bookings.html +++ b/app/templates/admin/manage_bookings.html @@ -1,89 +1,134 @@ -<!DOCTYPE html> -<html lang="en"> +{% extends 'base.html' %} +{% block content %} <head> <meta charset="UTF-8"> - <title>DataTables with Flask</title> <link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/jquery.dataTables.min.css"> - <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <!-- Bootstrap CSS --> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> + <script src="https://code.jquery.com/jquery-3.5.1.js"></script> + <script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script> + <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script> +</head> +<body> + <div class="container my-4"> + <div class="d-flex justify-content-between mb-3"> + <h2>Manage Bookings</h2> + <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#filterModal"> + <i class="fa-solid fa-filter"></i> Filter + </button> + </div> + + <!-- Hidden Filter Modal --> + <div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="filterModalLabel"><i class="fa-solid fa-filter"></i> Filter Options</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <form id="filter-form"> + <div class="form-group"> + <label for="depart_location">Depart Location:</label> + <input type="text" class="form-control" id="depart_location" name="depart_location"> + </div> + <div class="form-group"> + <label for="destination_location">Destination Location:</label> + <input type="text" class="form-control" id="destination_location" name="destination_location"> + </div> + <div class="form-group"> + <label for="min_depart_time">Minimum Depart Time:</label> + <input type="text" class="form-control" id="min_depart_time" name="min_depart_time"> + </div> + <div class="form-group"> + <label for="max_depart_time">Maximum Depart Time:</label> + <input type="text" class="form-control" id="max_depart_time" name="max_depart_time"> + </div> + <div class="form-group"> + <label for="min_fair_cost">Minimum Fair Cost:</label> + <input type="number" class="form-control" id="min_fair_cost" name="min_fair_cost"> + </div> + <div class="form-group"> + <label for="max_fair_cost">Maximum Fair Cost:</label> + <input type="number" class="form-control" id="max_fair_cost" name="max_fair_cost"> + </div> + <div class="form-group"> + <label for="transport_type">Transport Type:</label> + <input type="text" class="form-control" id="transport_type" name="transport_type"> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + <button type="button" class="btn btn-primary" id="apply-filters">Apply Filters</button> + </div> + </div> + </div> + </div> + + <!-- Data table starts --> + <div class="table-container"> + <table id="manage_bookings" class="table table-striped table-bordered display" style="width:95%"> + <thead> + <tr> + <th>Depart Location</th> + <th>Depart Time</th> + <th>Destination Location</th> + <th>Arrival Time</th> + <th>Fair Cost ($)</th> + <th>Transport Type</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + </tbody> + </table> + </div> + </div> <style> .table-container { - width: 90%; - margin: auto; - overflow-x: hidden; /* Hide overflow by default */ + width: 100%; + overflow-x: auto; /* Allow horizontal scroll if table is too wide */ } - @media (max-width: 800px) { .table-container { - width: 100%; padding: 0 10px; - overflow-x: auto; /* Enable horizontal scrolling */ + overflow-x: auto; } } - - @media (min-width: 801px) { - .table-container { - overflow-x: hidden; /* Ensure overflow is hidden on larger screens */ - } - } - - /* Ensure DataTable wrapper behaves responsively */ .dataTables_wrapper { width: 100%; } </style> - <script src="https://code.jquery.com/jquery-3.5.1.js"></script> - <script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script> - <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script> <!-- Bootstrap JS including Popper.js --> -</head> -<body> - <table class="inputs"> - <tr> - <td>Minimum age:</td> - <td><input type="text" id="min" name="min"></td> - </tr> - <tr> - <td>Maximum age:</td> - <td><input type="text" id="max" name="max"></td> - </tr> - </table> - - <div class="table-container"> - <table id="example" class="table table-striped table-bordered display" style="width:100%"> <!-- Use Bootstrap table classes --> - <thead> - <tr> - <th>Name</th> - <th>Position</th> - <th>Office</th> - <th>Age</th> - <th>Start date</th> - <th>Salary</th> - <th>Actions</th> <!-- Add Actions column --> - </tr> - </thead> - <tbody> - </tbody> - </table> - </div> - <script> $(document).ready(function() { - const table = $('#example').DataTable({ + const table = $('#manage_bookings').DataTable({ + pageLength: 25, + lengthChange: false, + searching: false, ajax: { - url: "{{ url_for('api.get_data') }}", // Updated URL + url: "{{ url_for('api.get_data') }}", dataSrc: '', data: function(d) { - d.min_age = $('#min').val(); - d.max_age = $('#max').val(); + d.depart_location = $('#depart_location').val(); + d.destination_location = $('#destination_location').val(); + d.min_depart_time = $('#min_depart_time').val(); + d.max_depart_time = $('#max_depart_time').val(); + d.min_fair_cost = $('#min_fair_cost').val(); + d.max_fair_cost = $('#max_fair_cost').val(); + d.transport_type = $('#transport_type').val(); } }, columns: [ - { data: 'Name' }, - { data: 'Position' }, - { data: 'Office' }, - { data: 'Age' }, - { data: 'StartDate' }, - { data: 'Salary' }, - { // Actions column + { data: 'depart_location' }, + { data: 'depart_time' }, + { data: 'destination_location' }, + { data: 'destination_time' }, + { data: 'fair_cost' }, + { data: 'transport_type' }, + { data: null, className: "dt-center", defaultContent: ` @@ -100,23 +145,24 @@ ] }); - $('#min, #max').on('input', function() { + $('#apply-filters').on('click', function() { table.ajax.reload(); + $('#filterModal').modal('hide'); }); - $('#example tbody').on('click', '.edit-btn', function() { + $('#manage_bookings tbody').on('click', '.edit-btn', function() { const data = table.row($(this).parents('tr')).data(); // Handle edit action - alert('Edit entry for ' + data.Name); + alert('Edit entry for ' + data.depart_location); }); - $('#example tbody').on('click', '.delete-btn', function() { + $('#manage_bookings tbody').on('click', '.delete-btn', function() { const data = table.row($(this).parents('tr')).data(); // Handle delete action - alert('Delete entry for ' + data.Name); + alert('Delete entry for ' + data.depart_location); }); }); </script> </body> </html> - \ No newline at end of file +{% endblock %} diff --git a/app/templates/bookings/index.html b/app/templates/bookings/index.html index 511b12109d474a6d7f2a700769cb4c9494ce271c..0dbd92f9e68694aceeeffb8463b047664091b2c9 100644 --- a/app/templates/bookings/index.html +++ b/app/templates/bookings/index.html @@ -10,6 +10,11 @@ <div class="deals_text"><span class="deals_underline">Deals for you</span></div> <div class="swiffy-slider slider-item-show2 slider-item-reveal slider-nav-outside slider-nav-round slider-nav-visible 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"> + {% if get_flashed_messages() %} + <div class="alert alert-danger" role="alert"> + get_flashed_messages() + </div> + {% endif %} {% for listing in top_listings %} <li class="slide-visible"> <div class="card shadow h-100"> diff --git a/app/templates/bookings/listings.html b/app/templates/bookings/listings.html index 5fba48d4811285e412deaa3fdb9b317dc43b515d..3bdf8a7aa60a1d809096da4686f6c58968925422 100644 --- a/app/templates/bookings/listings.html +++ b/app/templates/bookings/listings.html @@ -1,12 +1,197 @@ {% extends 'base.html' %} -# Implements CSS Slider from https://swiffyslider.com/docs/ {% block content %} - <head> - <!-- <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='listings.css')}}"> --> - <script src="https://cdn.jsdelivr.net/npm/swiffy-slider@1.6.0/dist/js/swiffy-slider.min.js" crossorigin="anonymous" defer></script> - <link href="https://cdn.jsdelivr.net/npm/swiffy-slider@1.6.0/dist/css/swiffy-slider.min.css" rel="stylesheet" crossorigin="anonymous"> - </head> - <div class="content"> - <p>Create full listings page here with search functionality</p> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Simplified Carousel Example</title> + <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> + <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script> + <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> + <style> + .container { + width: 60%; + margin: 0 auto; + font-family: Arial, sans-serif; + } + .search-bar { + margin-bottom: 20px; + display: flex; + gap: 10px; + } + .results { + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 20px; + display: flex; + gap: 20px; + cursor: pointer; + } + .main-image { + max-width: 150px; + cursor: pointer; + } + .carousel-item img { + width: 100%; + height: 100%; + object-fit: cover; + } + .carousel-item { + height: 400px; + position: absolute; + width: 100%; + transition: transform 0.5s ease-in-out; + } + .carousel-inner { + position: relative; + height: 400px; + overflow: hidden; + display: flex; + } + .carousel-item.active { + position: relative; + } + .carousel-item-next, + .carousel-item-prev, + .carousel-item.active { + display: block; + } + .carousel-item-next { + transform: translateX(100%); + } + .carousel-item-prev { + transform: translateX(-100%); + } + .carousel-item-left, + .carousel-item-next.carousel-item-left { + transform: translateX(0); + } + .carousel-item-right, + .carousel-item-prev.carousel-item-right { + transform: translateX(0); + } + </style> +</head> +<body> + <div class="container"> + <div class="search-bar"> + <input type="text" id="searchInput" placeholder="Search..."> + <button onclick="filterResults()">Search</button> + </div> + + <div id="resultsContainer"> + {% for item in items %} + <div class="results" onclick="window.location.href='/bookings/listing/{{ item.id }}'"> + <div> + {% if item.listing_images and item.listing_images[0] %} + <img src="{{ url_for('main.upload_file', filename=item.listing_images[0].image_location) }}" alt="Main Image" class="main-image" onclick="event.stopPropagation(); showModal([{% for image in item.listing_images %}'{{ url_for('main.upload_file', filename=image.image_location) }}'{% if not loop.last %}, {% endif %}{% endfor %}])"> + {% else %} + <img src="/path/to/default-image.jpg" alt="Default Image" class="main-image"> + {% endif %} + </div> + <div> + <div class="title">{{ item.depart_location }}</div> + <div class="price">{{ item.fair_cost }}</div> + <div class="delivery">{{ item.destination_location }}</div> + <div>Save up to 10% with multi-buy</div> + </div> </div> -{% endblock %} \ No newline at end of file +{% endfor %} + + </div> + </div> + + <!-- Modal --> + <div class="modal fade" id="imageModal" tabindex="-1" role="dialog" aria-labelledby="imageModalLabel" aria-hidden="true"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="imageModalLabel">Image Gallery</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <div id="carouselExampleIndicators" class="carousel slide" data-ride="carousel"> + <ol class="carousel-indicators" id="carouselIndicators"></ol> + <div class="carousel-inner" id="carouselInner"></div> + <a class="carousel-control-prev" href="#carouselExampleIndicators" role="button" data-slide="prev"> + <span class="carousel-control-prev-icon" aria-hidden="true"></span> + <span class="sr-only">Previous</span> + </a> + <a class="carousel-control-next" href="#carouselExampleIndicators" role="button" data-slide="next"> + <span class="carousel-control-next-icon" aria-hidden="true"></span> + <span class="sr-only">Next</span> + </a> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + + <script> + $(document).ready(function() { + $('#imageModal .close, #imageModal .btn-secondary').on('click', function() { + console.log("Close button clicked"); + $('#imageModal').modal('hide'); + }); + }); + + function showModal(imageUrls) { + console.log("showModal called with imageUrls:", imageUrls); + + var carouselInner = document.getElementById('carouselInner'); + var carouselIndicators = document.getElementById('carouselIndicators'); + carouselInner.innerHTML = ''; + carouselIndicators.innerHTML = ''; + + imageUrls.forEach((imageUrl, index) => { + var activeClass = (index === 0) ? ' active' : ''; + + // Create carousel item + var div = document.createElement('div'); + div.className = 'carousel-item' + activeClass; + var img = document.createElement('img'); + img.className = 'd-block w-100'; + img.src = imageUrl; + div.appendChild(img); + carouselInner.appendChild(div); + + // Create carousel indicator + var li = document.createElement('li'); + li.setAttribute('data-target', '#carouselExampleIndicators'); + li.setAttribute('data-slide-to', index); + if (index === 0) { + li.className = 'active'; + } + carouselIndicators.appendChild(li); + }); + + $('#imageModal').modal('show'); + } + + function filterResults() { + const searchTerm = document.getElementById('searchInput').value.toLowerCase(); + const results = document.querySelectorAll('.results'); + + results.forEach(result => { + const title = result.querySelector('.title').textContent.toLowerCase(); + const price = result.querySelector('.price').textContent.toLowerCase(); + const delivery = result.querySelector('.delivery').textContent.toLowerCase(); + + if (title.includes(searchTerm) || price.includes(searchTerm) || delivery.includes(searchTerm)) { + result.style.display = 'flex'; + } else { + result.style.display = 'none'; + } + }); + } + </script> +</body> +</html> +{% endblock %} diff --git a/migrations/versions/28932a02e785_create_listing_availibility_table.py b/migrations/versions/28932a02e785_create_listing_availibility_table.py new file mode 100644 index 0000000000000000000000000000000000000000..9c9e4985c1cdc1a6755f9f0b1340e583cdf048b0 --- /dev/null +++ b/migrations/versions/28932a02e785_create_listing_availibility_table.py @@ -0,0 +1,30 @@ +"""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 index bc5cb29abb12b34fcdfa29903271b8e2f7f59e8c..3a082241c4409eb7121047536d626504b06cd225 100644 --- a/migrations/versions/489bab9aaf4f_add_listing_images_table.py +++ b/migrations/versions/489bab9aaf4f_add_listing_images_table.py @@ -7,6 +7,7 @@ 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. @@ -17,10 +18,15 @@ depends_on = None def upgrade(): - 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) - ) + 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 index cb5beb74f5b8be4595388be5d89f65568bc1d345..a8202a88d6d0a3597a7131ffe9ab58ea5a5ffaf4 100644 --- a/migrations/versions/6791cbf31235_add_listing_table.py +++ b/migrations/versions/6791cbf31235_add_listing_table.py @@ -7,6 +7,7 @@ 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. @@ -17,15 +18,20 @@ depends_on = None def upgrade(): - 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 + 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/77815275598c_.py b/migrations/versions/77815275598c_.py new file mode 100644 index 0000000000000000000000000000000000000000..6c4f331415488b95ad456f497c7326c0b0f2846e --- /dev/null +++ b/migrations/versions/77815275598c_.py @@ -0,0 +1,24 @@ +"""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/ac9d4555724d_add_api_token_and_expiry.py b/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py index 59f4c9a590293c9ba96b7d83bef1228cdce3a182..26f22ae1471c03464d4d42a94743b849c47966e1 100644 --- a/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py +++ b/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py @@ -8,6 +8,7 @@ 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' @@ -17,11 +18,19 @@ depends_on = None def upgrade(): - 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') - ) \ No newline at end of file + 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/e49c7ce461b6_create_bookings_table.py b/migrations/versions/e49c7ce461b6_create_bookings_table.py new file mode 100644 index 0000000000000000000000000000000000000000..142d587757421070fdc9c0966a51fde4ab7ac536 --- /dev/null +++ b/migrations/versions/e49c7ce461b6_create_bookings_table.py @@ -0,0 +1,37 @@ +"""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') + diff --git a/migrations_backup/README b/migrations_backup/README deleted file mode 100644 index 0e048441597444a7e2850d6d7c4ce15550f79bda..0000000000000000000000000000000000000000 --- a/migrations_backup/README +++ /dev/null @@ -1 +0,0 @@ -Single-database configuration for Flask. diff --git a/migrations_backup/alembic.ini b/migrations_backup/alembic.ini deleted file mode 100644 index ec9d45c26a6bb54e833fd4e6ce2de29343894f4b..0000000000000000000000000000000000000000 --- a/migrations_backup/alembic.ini +++ /dev/null @@ -1,50 +0,0 @@ -# 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 diff --git a/migrations_backup/env.py b/migrations_backup/env.py deleted file mode 100644 index 4c9709271b2ff28271b13c29bba5c50b80fea0ac..0000000000000000000000000000000000000000 --- a/migrations_backup/env.py +++ /dev/null @@ -1,113 +0,0 @@ -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() diff --git a/migrations_backup/script.py.mako b/migrations_backup/script.py.mako deleted file mode 100644 index 2c0156303a8df3ffdc9de87765bf801bf6bea4a5..0000000000000000000000000000000000000000 --- a/migrations_backup/script.py.mako +++ /dev/null @@ -1,24 +0,0 @@ -"""${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"} diff --git a/migrations_backup/versions/22de5b143d05_create_user_roles.py b/migrations_backup/versions/22de5b143d05_create_user_roles.py deleted file mode 100644 index 5e6a5869f16c6b4644fa9222896cee3d46486009..0000000000000000000000000000000000000000 --- a/migrations_backup/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_backup/versions/489bab9aaf4f_add_listing_images_table.py b/migrations_backup/versions/489bab9aaf4f_add_listing_images_table.py deleted file mode 100644 index bc5cb29abb12b34fcdfa29903271b8e2f7f59e8c..0000000000000000000000000000000000000000 --- a/migrations_backup/versions/489bab9aaf4f_add_listing_images_table.py +++ /dev/null @@ -1,26 +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 - - -# revision identifiers, used by Alembic. -revision = '489bab9aaf4f' -down_revision = '6791cbf31235' -branch_labels = None -depends_on = None - - -def upgrade(): - 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_backup/versions/6791cbf31235_add_listing_table.py b/migrations_backup/versions/6791cbf31235_add_listing_table.py deleted file mode 100644 index cb5beb74f5b8be4595388be5d89f65568bc1d345..0000000000000000000000000000000000000000 --- a/migrations_backup/versions/6791cbf31235_add_listing_table.py +++ /dev/null @@ -1,31 +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 - - -# revision identifiers, used by Alembic. -revision = '6791cbf31235' -down_revision = 'ac9d4555724d' -branch_labels = None -depends_on = None - - -def upgrade(): - 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_backup/versions/68d89ef13132_add_main_listing_image_column.py b/migrations_backup/versions/68d89ef13132_add_main_listing_image_column.py deleted file mode 100644 index 9ccafd816ab4ff5c783ae3c73477b67d5737efa0..0000000000000000000000000000000000000000 --- a/migrations_backup/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_backup/versions/6c7070736062_add_role_id_to_users_table.py b/migrations_backup/versions/6c7070736062_add_role_id_to_users_table.py deleted file mode 100644 index b08cdd25b05f0bd104793ef65c8743f4092a4236..0000000000000000000000000000000000000000 --- a/migrations_backup/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_backup/versions/9a8cc1906445_add_fs_uniquifier_field_to_user_model.py b/migrations_backup/versions/9a8cc1906445_add_fs_uniquifier_field_to_user_model.py deleted file mode 100644 index 7bacfee528a6aff984243944fedccc2f36974923..0000000000000000000000000000000000000000 --- a/migrations_backup/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_backup/versions/ac9d4555724d_add_api_token_and_expiry.py b/migrations_backup/versions/ac9d4555724d_add_api_token_and_expiry.py deleted file mode 100644 index 59f4c9a590293c9ba96b7d83bef1228cdce3a182..0000000000000000000000000000000000000000 --- a/migrations_backup/versions/ac9d4555724d_add_api_token_and_expiry.py +++ /dev/null @@ -1,27 +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 - -# revision identifiers, used by Alembic. -revision = 'ac9d4555724d' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - 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') - ) \ No newline at end of file