ContactBridge

Log | Files | Refs | README

commit 1b6f3f4c960548c29ff300ba1627d2bd0b100dfc
parent 9d917beac687387db32e78cda89cbf53690857e3
Author: William Lindholm <william_lindholm@outlook.com>
Date:   Wed,  8 Nov 2023 21:27:07 +0100

Created test page that retrieves data from API using ajax. (sorting, searching, pagination all working)

Diffstat:
MWebAPI/namespaces/messages.py | 10++++++----
Mdatabase/database.db | 0
Mdatabase/models/messages.py | 16+++++++++++++---
Mmain.py | 2++
Mstatic/html/tabletest.html | 71+++++++++++++++++++++++------------------------------------------------
Mstatic/js/pagination.js | 124++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mtemplates/messages_template.html | 4++--
7 files changed, 150 insertions(+), 77 deletions(-)

diff --git a/WebAPI/namespaces/messages.py b/WebAPI/namespaces/messages.py @@ -1,9 +1,12 @@ from flask import request, render_template, make_response from flask_restx import Namespace, Resource, fields + from database.models.messages import MessageModel from database.repository import DbRepository + + messages_ns = Namespace('messages', description='Message operations') db_repository = DbRepository() @@ -37,6 +40,7 @@ paginated_message_model = messages_ns.model('MessageList', { class MessageList(Resource): @messages_ns.doc('get_messages') + @messages_ns.param('query', 'search query') @messages_ns.param('sort_by', 'Field to sort the messages by') @messages_ns.param('sort_order', 'Order to sort the messages (ASC or DESC)') @messages_ns.param('page', 'The page to retrieve') @@ -45,6 +49,7 @@ class MessageList(Resource): def get(self): """Get (paginated) messages (HTML or JSON)""" args = request.args + query = args.get('query', '', type=str) page = args.get('page', 1, type=int) page_size = args.get('page_size', 10, type=int) sort_by = args.get('sort_by', 'id') # Default sort by 'id' @@ -57,7 +62,7 @@ class MessageList(Resource): if sort_by not in valid_sort_fields: messages_ns.abort(400, f"Invalid sort_by field. Must be one of {valid_sort_fields}") - messages, total_pages = message_model.get_page(page, page_size, sort=sort_by, sort_order=sort_order) + messages, total_pages = message_model.get_page(page, page_size, sort=sort_by, sort_order=sort_order, search=query) accept_header = request.headers.get('Accept', '') if 'text/html' in accept_header: @@ -65,10 +70,7 @@ class MessageList(Resource): response.headers['X-Total-Pages'] = total_pages response.headers['X-Current-Page'] = page return response - - return render_template('messages_template.html', messages=messages) else: - # Default to JSON response return { 'messages': messages, 'total_pages': total_pages, diff --git a/database/database.db b/database/database.db Binary files differ. diff --git a/database/models/messages.py b/database/models/messages.py @@ -9,16 +9,26 @@ class MessageModel: def get_page(self, page, page_size, sort="id", sort_order="ASC", search=''): search_term = f"%{search}%" - total_messages_query = 'SELECT COUNT(*) FROM messages' - total_messages = self.db_repository.execute_query(total_messages_query, expect_result=True)[0][0] common_sql_query_base = f""" FROM messages WHERE (name LIKE ? OR email LIKE ? OR subject LIKE ? OR content LIKE ?) """ + total_messages_query = f""" + SELECT COUNT(*) + {common_sql_query_base} + """ + + total_messages = self.db_repository.execute_query( + total_messages_query, + (search_term, search_term, search_term, search_term), + expect_result=True + ) + + total_messages_count = total_messages[0][0] if total_messages else 0 offset = (page - 1) * page_size - total_pages = math.ceil(total_messages / page_size) + total_pages = math.ceil(total_messages_count / page_size) paginated_messages_query = f""" SELECT id, name, email, subject, content, timestamp, relevance diff --git a/main.py b/main.py @@ -1,4 +1,5 @@ from flask import Flask +from flask_cors import CORS from WebAPI import blueprint as web_api from WebInterface import web_interface import os @@ -7,6 +8,7 @@ app = Flask(__name__) app.register_blueprint(web_api, url_prefix='/api') app.register_blueprint(web_interface) app.secret_key = os.urandom(24) +CORS(app, expose_headers='*') if __name__ == '__main__': app.run(debug=True) \ No newline at end of file diff --git a/static/html/tabletest.html b/static/html/tabletest.html @@ -3,77 +3,51 @@ <head> <meta charset="UTF-8"> <title>Messages</title> - <!-- Materialize CSS --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css"> - <!-- HTMX --> - <script src="https://unpkg.com/htmx.org"></script> </head> <body> <div class="container"> <h2>Messages</h2> - <!-- Search Bar --> <div class="input-field"> - <input type="text" id="search-bar" placeholder="Search Messages..." hx-get="/api/messages" hx-trigger="keyup changed delay:500ms" hx-target="#table-body" hx-headers='{"Accept":"text/html"}' hx-indicator="#loading-indicator"> + <input type="text" id="search-bar" placeholder="Search Messages..." onkeyup="searchData()"> <div id="loading-indicator" style="display: none;">Loading...</div> </div> - <!-- Messages Table --> <table class="highlight responsive-table"> - <thead> + <thead id="table-headers"> <tr> - <th>ID</th> - <th>Name</th> - <th>Email</th> - <th>Subject</th> - <th>Timestamp</th> - <th>Relevance</th> + <th data-field="id" data-order="asc">ID</th> + <th data-field="name" data-order="asc">Name</th> + <th data-field="email" data-order="asc">Email</th> + <th data-field="subject" data-order="asc">Subject</th> + <th data-field="timestamp" data-order="asc">Timestamp</th> + <th data-field="relevance" data-order="asc">Relevance</th> </tr> </thead> - <tbody id="table-body" hx-get="/api/messages" hx-trigger="load" hx-headers='{"Accept":"text/html"}'> - <!-- Table rows will be populated by HTMX --> + <tbody id="table-body"> + <!-- Table data --> </tbody> </table> - <!-- Pagination --> <ul class="pagination" id="pagination-container"> - <!-- Pagination links will be dynamically inserted here --> + <!-- Pagination --> </ul> + + <select id="page-size" onchange="searchData('messages')"> + <option value="5">5</option> + <option value="10" selected>10</option> + <option value="25">25</option> + <option value="50">50</option> + </select> + </div> <!-- Materialize JS --> <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> - <script> - // JavaScript for dynamically updating pagination - function updatePagination(currentPage, totalPages) { - const container = document.getElementById('pagination-container'); - container.innerHTML = ''; // Clear existing pagination - - for (let i = 1; i <= totalPages; i++) { - const li = document.createElement('li'); - li.className = i === currentPage ? 'active' : 'waves-effect'; - const a = document.createElement('a'); - a.href = '#!'; - a.textContent = i; - a.setAttribute('hx-get', '/api/messages?page=' + i); - a.setAttribute('hx-target', '#table-body'); - a.setAttribute('hx-trigger', 'click'); - a.setAttribute('hx-headers', '{"Accept":"text/html"}'); - li.appendChild(a); - container.appendChild(li); - } - } + <script>const API_ENDPOINT = '/api/messages/';</script> - // Initial pagination setup - document.body.addEventListener('htmx:load', function(event) { - if (event.detail.path === '/api/messages') { - // Assuming the server sends back total pages and current page as data attributes - const totalPages = event.detail.xhr.getResponseHeader('X-Total-Pages'); - const currentPage = event.detail.xhr.getResponseHeader('X-Current-Page'); - updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10)); - } - }); - </script> + <script src="/static/js/pagination.js"></script> </body> -</html> +</html> +\ No newline at end of file diff --git a/static/js/pagination.js b/static/js/pagination.js @@ -1,21 +1,105 @@ -var currentPage; -var totalPages; - -document.addEventListener('DOMContentLoaded', function () { - var paginationElement = document.querySelector('.pagination'); - if (paginationElement) { - currentPage = parseInt(paginationElement.getAttribute('data-current-page'), 10); - totalPages = parseInt(paginationElement.getAttribute('data-total-pages'), 10); - } -}); - -function changePage(direction) { - var newPage = currentPage; - if (direction === 'next' && currentPage < totalPages) { - newPage = currentPage + 1; - } else if (direction === 'prev' && currentPage > 1) { - newPage = currentPage - 1; +// Function to fetch data and update the table +function fetchData(endpoint, queryParams) { + const url = new URL(endpoint, window.location.origin); + Object.keys(queryParams).forEach(key => url.searchParams.append(key, queryParams[key])); + + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.setRequestHeader('Accept', 'text/html'); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + document.getElementById('table-body').innerHTML = xhr.responseText; + const totalPages = xhr.getResponseHeader('X-total-pages'); + const currentPage = xhr.getResponseHeader('X-current-page'); + updatePagination(parseInt(currentPage, 10), parseInt(totalPages, 10), queryParams.page_size, endpoint); + } + }; + xhr.send(); +} + +// handle search input for messages or users +function searchData() { + const searchInput = document.getElementById('search-bar').value; + const pageSize = document.getElementById('page-size').value; + const queryParams = { + query: searchInput, + page_size: pageSize + }; + fetchData(API_ENDPOINT, queryParams); +} + +// Function to update pagination +function updatePagination(currentPage, totalPages, pageSize) { + const container = document.getElementById('pagination-container'); + container.innerHTML = ''; // Clear existing pagination + + // Create the 'Back' button + const backLi = document.createElement('li'); + backLi.className = currentPage === 1 ? 'disabled' : 'waves-effect'; + const backA = document.createElement('a'); + backA.href = '#!'; + backA.innerHTML = '&laquo;'; + backA.onclick = function() { + if (currentPage > 1) { + fetchData(API_ENDPOINT, { page: currentPage - 1, page_size: pageSize }); + } + }; + backLi.appendChild(backA); + container.appendChild(backLi); + + // Create the page number buttons + for (let i = 1; i <= totalPages; i++) { + const li = document.createElement('li'); + li.className = i === currentPage ? 'active' : 'waves-effect'; + const a = document.createElement('a'); + a.href = '#!'; + a.textContent = i; + a.onclick = function() { + fetchData(API_ENDPOINT, { page: i, page_size: pageSize }); + }; + li.appendChild(a); + container.appendChild(li); } - window.location.href = '?page=' + newPage; -} -\ No newline at end of file + // Create the 'Forward' button + const forwardLi = document.createElement('li'); + forwardLi.className = currentPage === totalPages ? 'disabled' : 'waves-effect'; + const forwardA = document.createElement('a'); + forwardA.href = '#!'; + forwardA.innerHTML = '&raquo;'; + forwardA.onclick = function() { + if (currentPage < totalPages) { + fetchData(API_ENDPOINT, { page: currentPage + 1, page_size: pageSize }); + } + }; + forwardLi.appendChild(forwardA); + container.appendChild(forwardLi); +} + +// Function to sort table by clicking on headers +function sortTableByHeader(type) { + document.querySelectorAll('#table-headers th').forEach(header => { + header.addEventListener('click', () => { + const sortField = header.getAttribute('data-field'); + const sortOrder = header.getAttribute('data-order'); + const searchInput = document.getElementById('search-bar').value; + const pageSize = document.getElementById('page-size').value || 10; + const queryParams = { + query: searchInput, + sort_by: sortField, + sort_order: sortOrder, + page_size: pageSize + }; + fetchData(`/api/${type}`, queryParams); + // Toggle sort order for the next click + header.setAttribute('data-order', sortOrder === 'asc' ? 'desc' : 'asc'); + }); + }); +} + +// Initial fetch for messages and setup headers sorting +document.addEventListener('DOMContentLoaded', function() { + M.AutoInit(); // Initialize all Materialize components + fetchData(API_ENDPOINT, { page: 1, page_size: 10 }); + sortTableByHeader('messages'); +}); +\ No newline at end of file diff --git a/templates/messages_template.html b/templates/messages_template.html @@ -1 +1 @@ -<tbody>{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.name }}</td><td>{{ message.email }}</td><td>{{ message.subject }}</td><td>{{ message.timestamp }}</td><td>{{ message.relevance }}</td></tr>{% endfor %}</tbody> -\ No newline at end of file +{% for message in messages %}<tr><td>{{ message.id }}</td><td>{{ message.name }}</td><td>{{ message.email }}</td><td>{{ message.subject }}</td><td>{{ message.timestamp }}</td><td>{{ message.relevance }}</td></tr>{% endfor %} +\ No newline at end of file