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:
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 = '«';
+ 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 = '»';
+ 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