The following text explains how to use the Jam.py framework at https://jam-py.com
User interface pages in the Jam.py framework are called 'tasks'. Tasks are added to a Jam.py project with Jam.py's visual browser based IDE.
Each task has its own associated database table schema, which is built using the Jam.py IDE. Properties of the task UI and its database tables are referred to with dot notation (i.e., task.some_Object, task.some_Method, etc.). An editable front-end client Javascript code module is associated with each task. Similarly, a back-end server Python module is associated with each task. Both the back-end and front-end code of any task can access the database tables for associated task, or for any other task in the hierarchical task tree. Javascript functions can call Python server functions directly, and can use built-in jQuery event handlers, as well as built-in Bootstrap styling. In this way, the Jam.py framework eliminates any need for hand written AJAX code. The ORM included in Jam.py also eliminates the need for any hand written SQLAlchemy or 3rd party database tools.
Jam.py has a built-in no-code schema editor, and it automatically generates full-featured UI tables (data grids) and forms which are pre-wired to perform CRUD interactions with the database tables. The term 'item' is used to refer to the database rows displayed in auto-generated front-end UIs.
In order to build a custom layout and UI interface to the database (beyond the automatically generated grids and forms), HTML code can be added to a template section within the project index.html file. Templates are divs which can contain any valid HTML. The default index.html file can contain any CSS or other information in the head tags:
The index.html file also includes some defaut layout and templates used by jam.py:
Javascript libraries can be imported, and you can include any custom JS in the script section of the index.html file:
Custom CSS code is typically added to the project.css file.
Here are some examples of custom Jam.py templates which have been added to an index.html in various projects:
Coin toss
A Form
This form was created with the Grapes.js visual layout editor. The HTML code generated by Grapes.js was copied into the Jam.py index.html file (as divs in the template section), and the CSS code was copied directly into the Jam.py project.css file.
In Jam.py, a new task page is then created. On that task page, the saved HTML code is imported from the template section of the index.html file. This import is performed in the client JS code of the task page, within the on_view_form_created handler. In the client JS code, a click event handler is also written for the generated HTML button. In that JS handler function, the data collected from each form field is sent to a Python function on the Jam.py back-end. In the back-end function, Jam.py database functions are used to save the form data values to a database table, which has been created using the no-code schema builder in Jam.py.
Another task page in the Jam.py project is also created, on which an automatically generated Jam.py grid displays all the rows of data which have ever been entered into the custom HTML form. The automatically generated set of Jam.py grid and form UI components are capable of enabling all the same functionality of the custom Grapes.js form, without having to write any code at all. The point of this example is just to show how simple it is to wire custom UI interfaces to any of the database schema tables and UI grids/forms which can be created using Jam.py's no-code tools.
In order to display a template layout on a task page, and to apply Javascript code to tags in the layout, we find a specific template class, and then typically clone and display that template, either in place of the default generated Jam.py 'view_form' layout:
function on_view_form_created(item) {
const template = task.templates.find(".markdown-container").clone();
item.view_form.empty().append(template);
}
or along with the generated Jam.py layout:
function on_view_form_created(item) {
const template = task.templates.find(".markdown-container").clone();
item.view_form.append(template);
}
We use jQuery events to handle events within the default UI grid, form, and page layout. These handlers are built-in:
function on_after_append(item) {}
function on_after_apply(item) {}
function on_after_cancel(item) {}
function on_after_delete(item) {}
function on_after_edit(item) {}
function on_after_open(item) {}
function on_after_post(item) {}
function on_after_scroll(item) {}
function on_before_append(item) {}
function on_before_apply(item, params) {}
function on_before_cancel(item) {}
function on_before_delete(item) {}
function on_before_edit(item) {}
function on_before_field_changed(field) {}
function on_before_open(item, params) {}
function on_before_post(item) {}
function on_before_scroll(item) {}
function on_detail_changed(item, detail) {}
function on_edit_form_close_query(item) {}
function on_edit_form_closed(item) {}
function on_edit_form_created(item) {}
function on_edit_form_keydown(item, event) {}
function on_edit_form_keyup(item, event) {}
function on_edit_form_shown(item) {}
function on_field_changed(field, lookup_item) {}
function on_field_get_html(field) {}
function on_field_get_summary(field, value) {}
function on_field_get_text(field) {}
function on_field_select_value(field, lookup_item) {}
function on_field_validate(field) {}
function on_filter_changed(filter) {}
function on_filter_form_close_query(item) {}
function on_filter_form_closed(item) {}
function on_filter_form_created(item) {}
function on_filter_form_shown(item) {}
function on_filter_record(item) {}
function on_filter_select_value(field, lookup_item) {}
function on_filters_applied(item) {}
function on_filters_apply(item) {}
function on_view_form_close_query(item) {}
function on_view_form_closed(item) {}
function on_view_form_created(item) {}
function on_view_form_keydown(item, event) {}
function on_view_form_keyup(item, event) {}
function on_view_form_shown(item) {}
Within Javascript functions, we can directly call back-end Python functions, passing any argument values from the front-end JS. Then we can apply any imported JS library functions to the data returned from the back-end. For example, this code gets markdown code from a back-end Python function called get_markdown(), and then parses & displays the rendered markdown code within the cloned custom ".markdown-container" template layout:
function on_view_form_created(item) {
const template = task.templates.find(".markdown-container").clone();
item.view_form.empty().append(template);
// Call the server function to get Markdown text
item.server("get_markdown", function (markdownText, error) {
if (error) {
console.error("Error fetching Markdown:", error);
return;
}
// Convert Markdown to HTML using the imported marked JS library
let htmlContent = marked.parse(markdownText);
// Insert HTML into the UI
item.view_form.find(".markdown-container").html(htmlContent);
});
}
Here is the Python function which returns markdown code to the JS function above:
def get_markdown(item): #item parameter is required from Jam.py
return """
# Welcome to the App
This is a **bold** statement.
- Item 1
- Item 2
- Item 3
[Click Here](https://example.com) for more info
"""
Jam.py enables database operations to be accessed directly in front-end JavaScript code. The example below creates a new row in the table associated with the 'form_tables' task, and saves the values which the user has entered into the 'custom_form' HTML template:
function on_view_form_created(item) {
const template = task.templates.find(".custom-form").clone();
item.view_form.empty().append(template);
$("#ifv3g").submit(function (event) {
event.preventDefault(); // Prevent default page reload
let c = task.form_tables.copy();
c.open();
c.append();
c.text.value=$("#multi-text").val();
c.email.value=$("#multi-email").val();
c.password.value=$("#multi-password").val();
c.gender.value=$("input[name='gender']:checked").val() || null;
c.options.value=$("#multi-select").val();
c.date.value = $("#multi-date").val() ? new Date($("#multi-date").val() + "T00:00:00") : null;
c.number.value=$("#multi-number").val();
c.created_at.value=new Date();
c.post();
c.apply();
});
}
Custom code can be written to handle events of any sort, in the UI forms and grids which are automatically generated by Jam.py (and/or in any custom template code):
function on_field_changed(field, lookup_item) {
field.owner.difference.value = (
field.owner.end_date.value - field.owner.start_date.value
) / (1000 * 60 * 60 * 24);
}
function on_field_validate(field) {
if (field.owner.end_date.value < field.owner.start_date.value) {
return "End date must be later than start date";
}
}
Images and files which are uploaded by automatically generated Jam.py forms, are stored in the ./static/files/ sub-folder of the project directory. They can be accessed and used in custom layouts:
function on_view_form_created(item) {
const template = task.templates.find(".cointoss").clone();
item.view_form.empty().append(template);
$("#coinform").submit(function (event) {
event.preventDefault();
const images = [
"./static/files/tails_2025-02-08_17-43-48_211693.jpg",
"./static/files/heads_2025-02-08_17-43-34_191100.jpg"
];
const randomImage = images[Math.floor(Math.random() * images.length)];
$("#coinimage").attr("src", randomImage);
console.log("Flipped to: " + randomImage);
});
}
In the example below, notice that the 'item' parameter (which represents database values in auto-generated UI grid values) is passed to the Python server function from the Jam.py front-end JS function. This is always required, whether or not those values are needed by the back end function. In this example, values returned from the back-end REST API call, is used on the front end:
import requests
def guess_age(item, person): # the 'item' parameter is required
r = requests.get(f'https://api.agify.io/?name={person}').json()
return r["age"]
function on_field_changed(field, lookup_item) {
let name = field.value; // Get the entered name
# The 'item' parameter is NOT explicitly passed by the front end:
field.owner.server("guess_age", name, function (age, error) {
if (age) {
field.owner.guessed_age.value = age;
}
});
}
Here's another example of the response to a back-end API call, being returned and used in a front end function:
function on_field_changed(field, lookup_item) {
let zipCode = field.value; // Get the entered zip code
field.owner.server("get_weather", zipCode, function (weatherData, error) {
if (error) {
console.error("Error fetching weather data:", error);
return;
}
if (weatherData) {
// Update the fields with fetched temperature and wind speed
field.owner.temperature.value = weatherData.temp;
field.owner.wind_speed.value = weatherData.wind_speed;
} else {
console.warn("No weather data received.");
}
});
}
import os
import requests
def get_weather(item, zipc):
"""
Fetch weather data from OpenWeather API using a zip code.
:param item: Required by Jam.py (not used)
:param zipc: Zip code (string or integer)
:return: Dictionary containing temperature and wind speed or None on error
"""
api_key = os.getenv("OPENWEATHER_API_KEY") # Read from environment
if not api_key:
print("❌ ERROR: Missing API Key! Set OPENWEATHER_API_KEY in the environment.")
return None
url = f"https://api.openweathermap.org/data/2.5/weather?zip={zipc}&appid={api_key}&units=imperial"
try:
response = requests.get(url)
response.raise_for_status()
data = response.json()
return {
"temp": data["main"]["temp"],
"wind_speed": data["wind"]["speed"]
}
except requests.exceptions.RequestException as e:
print(f"Error fetching weather data: {e}")
return None
A common requirement in web application development is to build 'repeating-panel' or 'card' layouts from data returned by server functions (imagine repetitive Amazon search result layouts, blog post layouts, etc.). Here's an example of how to build and display such layouts:
import requests
def get_json_data(item):
url = "https://jsonplaceholder.typicode.com/users"
try:
response = requests.get(url)
response.raise_for_status() # Raise an error for bad status codes
users = response.json() # Parse the JSON response into a Python object (list/dict)
print(users)
return users
except requests.exceptions.RequestException as e:
print(f"An error occurred: {e}")
return None
function on_view_form_created(item) {
// Clone the template that contains the repeating panel container
const template = task.templates.find(".api-repeating-panel").clone();
item.view_form.empty().append(template);
function renderLayout() {
item.server("get_json_data", function (rows, error) {
if (error) {
console.error("Error fetching topics:", error);
return;
}
// Find the repeating panel container within the view form
let repeatingPanel = item.view_form.find(".api-repeating-panel");
repeatingPanel.empty();
// Loop through each user (row) and create a detailed card.
rows.forEach(rowItem => {
// Create the main card container with additional custom styling
let card = $("
")
.addClass("card custom-card mb-4")
.data("rowItem-id", rowItem.id);
// Card header with a gradient background displaying the name and username
let cardHeader = $("
")
.addClass("card-header")
.html(`
${rowItem.name}
@${rowItem.username}`);
// Card body for the main details
let cardBody = $("
").addClass("card-body");
// Basic user details: email, phone, and website
cardBody.append(`
`);
// Address details from the nested address object
const addr = rowItem.address;
cardBody.append(`
Address: ${addr.street}, ${addr.suite}
${addr.city}, ${addr.zipcode}
Geo: lat ${addr.geo.lat}, lng ${addr.geo.lng}
`);
// Company details from the nested company object
const comp = rowItem.company;
cardBody.append(`
Company: ${comp.name}
"${comp.catchPhrase}"
${comp.bs}
`);
// Assemble the card
card.append(cardHeader);
card.append(cardBody);
// Append the card to the repeating panel container
repeatingPanel.append(card);
});
});
}
renderLayout();
}
Here's an another repeating card layout example, displaying a list of images stored in a JS list:
function on_view_form_created(item) {
// Clone the template that contains the webcam-repeating-panel
const template = task.templates.find(".webcam-repeating-panel").clone();
item.view_form.empty().append(template);
function renderLayout() {
const baseUrl = "https://cwwp2.dot.ca.gov/data/d2/cctv/image/";
const imagePaths = [
"pitriverbridge/pitriverbridge.jpg",
"mthebron/mthebron.jpg",
"eurekaway/eurekaway.jpg",
"sr70us395/sr70us395.jpg",
"bogard/bogard.jpg",
"eastriverside/eastriverside.jpg",
];
let repeatingPanel = item.view_form.find(".webcam-repeating-panel");
repeatingPanel.empty();
imagePaths.forEach(camurl => {
let fullImageUrl = baseUrl + camurl;
let card = $("
").addClass("card webcam-card mb-4");
let imgElement = $("")
.attr("src", fullImageUrl)
.attr("alt", "Traffic Camera")
.addClass("card-img-top");
let cardBody = $("
").addClass("card-body");
cardBody.append(`
${camurl.split("/")[0]}
`);
card.append(imgElement);
card.append(cardBody);
repeatingPanel.append(card);
});
}
renderLayout();
}
The following example demonstrates how to record audio using the JS 'getUserMedia' API, then convert the audio data to base64 format, save the base64 data to the database using Python server functions, and finally retrieve the data via Python server code, and play the retrieved file with JS code:
function on_view_form_created(item) {
// Clone the audio recorder template and append it to the view
const template = task.templates.find(".audio-recorder").clone();
item.view_form.append(template);
let mediaRecorder;
let audioChunks = [];
let isRecording = false;
let audioStream; // variable to store the stream
// Select elements within the view form
const recordButton = item.view_form.find("#recordButton");
const audioPlayer = item.view_form.find("#audioPlayer");
const playButton = item.view_form.find("#playButton");
// Attach event listeners correctly using .on() inside the function
recordButton.on("click", function () {
toggleRecording();
});
playButton.on("click", function () {
loadAndPlayAudio();
});
function startRecording() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
console.error("❌ getUserMedia is not supported in this browser!");
alert("Audio recording is not supported in your browser. Please update or use Chrome/Edge/Firefox.");
return;
}
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
audioStream = stream; // save the stream
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.start();
isRecording = true;
audioChunks = [];
mediaRecorder.ondataavailable = event => {
audioChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
convertToBase64(audioBlob, function (base64Audio) {
saveAudioToDatabase(base64Audio);
});
};
recordButton.text("Stop Recording");
})
.catch(err => {
console.error("❌ Microphone access denied!", err);
alert("❌ Microphone access was denied. Please enable it in browser settings.");
});
}
function stopRecording() {
if (mediaRecorder && isRecording) {
mediaRecorder.stop();
// Stop all tracks of the stream to release the microphone
if (audioStream) {
audioStream.getTracks().forEach(track => track.stop());
}
isRecording = false;
recordButton.text("Start Recording");
}
}
function toggleRecording() {
isRecording ? stopRecording() : startRecording();
}
function convertToBase64(blob, callback) {
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
callback(reader.result.split(",")[1]); // Extract only Base64
};
}
function saveAudioToDatabase(base64Audio) {
item.server("save_audio", base64Audio, function (response, error) {
if (error) {
console.error("❌ Error saving audio:", error);
alert("❌ Failed to save the audio.");
} else {
console.log("✅ Audio saved successfully!", response);
alert("✅ Audio saved successfully!");
}
});
}
function loadAndPlayAudio() {
// Use rec_no from the current dataset to get the selected record number.
// Adjust this according to your dataset structure—if your audio records dataset is a sub-table,
// replace "item.rec_no" with the correct reference, e.g. "item.audio_table.rec_no".
let currentRecNo = item.rec_no;
if (currentRecNo === undefined || currentRecNo === null) {
alert("No record is selected!");
return;
}
// Pass the current record number to the server function "get_audio"
item.server("get_audio", currentRecNo, function(base64Audio, error) {
if (error) {
console.error("❌ Error fetching audio:", error);
alert("❌ Failed to fetch audio.");
return;
}
if (base64Audio) {
audioPlayer.attr("src", "data:audio/webm;base64," + base64Audio);
audioPlayer[0].play();
} else {
alert("No audio file found for the selected record.");
}
});
}
}
import datetime
import binascii
def save_audio(item, base64_audio):
"""
Saves recorded audio to the database.
:param item: Required by Jam.py
:param base64_audio: The Base64-encoded audio data
:return: Confirmation message
"""
try:
item.open()
item.append()
item.audio_data.value = base64_audio
item.timestamp.value = datetime.datetime.now()
item.post()
item.apply()
return "Audio saved successfully"
except Exception as e:
print(f"Error saving audio: {e}")
return "Error saving audio"
def get_audio(item, record_id):
"""
Fetches the recorded audio from the audio_records table for the given record_id.
:param item: Required by Jam.py.
:param record_id: The record number (rec_no) of the audio record to retrieve.
:return: Base64 audio data from the specified record or None.
"""
try:
# Open the dataset
item.open()
# Set the current record using rec_no. Note that rec_no is 0-based.
item.rec_no = record_id
the_sound_file = item.audio_data.value
item.close()
return the_sound_file
except Exception as e:
print(f"Error fetching audio for record {record_id}: {e}")
return None
Here are some more examples of how to interact with the built-in UI components generated by Jam.py, how to display custom HTML within a default layout, and how to perform some other various client interactions:
function on_view_form_created(item) {
// Ensure the correct template is used
const template = task.templates.find(".journal-view").clone();
item.view_form.empty().append(template);
// Add dynamic behavior or adjustments
item.view_form.find(".custom-header").addClass("header-styled");
// Customize table options and initialize the grid
item.table_options.height -= 100; // Adjust grid height
item.create_table(item.view_form.find(".view-table"));
// Initialize buttons
const newButton = item.view_form.find("#new-btn");
const editButton = item.view_form.find("#edit-btn");
const deleteButton = item.view_form.find("#delete-btn");
// Assign event handlers to buttons
if (item.can_create()) {
newButton.on("click.task", function () {
item.insert_record();
});
} else {
newButton.prop("disabled", true);
}
if (item.can_edit()) {
editButton.on("click.task", function () {
item.edit_record();
});
} else {
editButton.prop("disabled", true);
}
if (item.can_delete()) {
deleteButton.on("click.task", function () {
item.delete_record();
});
} else {
deleteButton.prop("disabled", true);
}
// Log success message
console.log("Grid and buttons initialized successfully for persons task.");
}
The JS code above uses the following custom template:
This Is More Custom HTML
Click the catalogue links above to see app examples embedded in iframes.
This is Deb, she's the best:
The example below uses JS to place a text area in the UI layout, and calls a server function, to display all the values from each database row, in the column_1 column:
function on_view_form_created(item) {
console.log("Fetching full dataset from server...");
item.server("get_all_column_1_values", function(values, error) {
if (error) {
console.error("Error fetching data from server:", error);
return;
}
console.log("Received full dataset from server:", values);
let textArea = item.view_form.find("#column_1_values");
textArea.val(values.length ? values.join("\n") : "No valid column_1 data found.");
console.log("Text area updated with column_1 values.");
});
}
def get_all_column_1_values(item):
"""Fetch all values from column_1 in the database table and return them."""
item.open() # Ensure the dataset is fully opened
values = [row.column_1.value for row in item if row.column_1.value] # Collect non-empty values
return values # Send back the list of values
Here's the client JS code of a task which simply displays an iframe containing another application (written using the SQLPage framework, which connects to the same database as the Jam.py application):
function on_view_form_created(item) {
const template = task.templates.find(".sqlpage-iframe-view").clone();
item.view_form.empty().append(template);
}
Here's the HTML template used by that JS:
And just for reference (not needed for any Jam.py coding), here is the SQLPage code which can be used to create such an app, which connects to the database create by Jam.py:
-- Insert new record only if all fields are provided and no edit is in progress
INSERT INTO JAM_FORUM_POSTS (CONTENT, CREATED_AT, DELETED, AUTHOR, MASTER_REC_ID, MASTER_ID)
SELECT :Content, :Created_At, :Deleted, :Author, :Master_Rec_ID, :Master_ID
WHERE :Content IS NOT NULL AND $e IS NULL;
-- Update the record when editing
UPDATE JAM_FORUM_POSTS
SET CONTENT = :Content, CREATED_AT = :Created_At, DELETED = :Deleted, AUTHOR = :Author, MASTER_REC_ID = :Master_Rec_ID, MASTER_ID = :Master_ID
WHERE id = $e AND :Content IS NOT NULL;
-- Delete the record
DELETE FROM JAM_FORUM_POSTS WHERE id = $d;
-- Conditionally show the form for editing or adding a new entry
SELECT 'form' AS component;
-- Populate form fields for both adding and editing
SELECT (SELECT CONTENT FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Content' AS name;
SELECT (SELECT CREATED_AT FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Created_At' AS name;
SELECT (SELECT DELETED FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Deleted' AS name;
SELECT (SELECT MASTER_REC_ID FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Master_Rec_ID' AS name;
SELECT (SELECT MASTER_ID FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Master_ID' AS name;
SELECT (SELECT AUTHOR FROM JAM_FORUM_POSTS WHERE id = $e) AS value, 'Author' AS name;
-- Add "Add New" button to set the $add parameter
SELECT 'button' AS component, 'center' AS justify;
SELECT '?add=1' AS link, 'Clear and Add New' AS title;
-- Display the table with actions
SELECT 'table' AS component,
'Edit' AS markdown,
'Remove' AS markdown,
TRUE AS sort,
TRUE AS search;
SELECT
id AS ID,
CONTENT AS Content,
CREATED_AT AS Created_At,
DELETED AS Deleted,
AUTHOR AS Author,
MASTER_REC_ID AS Master_Rec_ID,
MASTER_ID AS Master_ID,
'[Edit](?e=' || id || ')' AS Edit, -- Dynamic link for edit
'[🗑ï¸](?d=' || id || ')' AS Remove -- Dynamic link for delete
FROM JAM_FORUM_POSTS;
When using another framework to interact with database tables created by a Jam.py application, just be sure to set the path to the database created by your Jam.py app, in your database configuration file. For example, for the SQLPage app above:
{
"database_url": "sqlite://../jampy_folder/sqlite_database_filename_created_by_jampy?mode=rwc"
}
This example loads an RShiny app in an iframe, along with displaying the UI grid which is automatically generated by Jam.py:
function on_view_form_created(item) {
// Load the correct template
const template = task.templates.find(".shiny-iframe-view").clone();
item.view_form.empty().append(template);
// Debug: Ensure view-table exists
const tableContainer = item.view_form.find(".view-table");
if (tableContainer.length === 0) {
console.error("Error: view-table container not found.");
return;
}
// Initialize the grid
item.table_options.height = 300; // Adjust height as needed
item.create_table(tableContainer);
// Initialize buttons
const newButton = item.view_form.find("#new-btn");
const editButton = item.view_form.find("#edit-btn");
const deleteButton = item.view_form.find("#delete-btn");
if (item.can_create()) {
newButton.on("click.task", function () {
item.insert_record();
});
} else {
newButton.prop("disabled", true);
}
if (item.can_edit()) {
editButton.on("click.task", function () {
item.edit_record();
});
} else {
editButton.prop("disabled", true);
}
if (item.can_delete()) {
deleteButton.on("click.task", function () {
item.delete_record();
});
} else {
deleteButton.prop("disabled", true);
}
console.log("Iframe, grid, and buttons loaded successfully for shiny_iframe task.");
}
Here's the HTML template used by the JS above:
Here's how a splash page with animated imagery can be added to the startup of a Jam.py app, within $(document).ready(function(){}):
This example demonstrates how to build a custom front end to a forum application, which is built on a set of master-details tables created by the Jam.py visual builder:
function on_view_form_created(item) {
console.log("on_view_form_created executed");
let forumContainer = $("#forum-container");
function renderTopics() {
item.server("get_all_topics", function (topics, error) {
if (error) return console.error("Error fetching topics:", error);
let topicList = $("#topic-list");
topicList.empty();
topics.forEach(topic => {
let topicElement = $("
")
.addClass("list-group-item forum-topic")
.text(topic.title)
.data("topic-id", topic.id)
.click(() => loadPosts(topic.id)); // Click loads posts but doesn't hide topics
topicList.append(topicElement);
});
});
}
function loadPosts(topicId) {
console.log("Loading posts for topic ID:", topicId);
item.server("get_posts_by_topic", [topicId], function (posts, error) {
if (error) return console.error("Error fetching posts:", error);
console.log("Received posts:", posts);
let forumContainer = $("#forum-container");
if (forumContainer.length === 0) {
console.warn("#forum-container NOT FOUND! Creating it now...");
$("body").append(''); // Create container
forumContainer = $("#forum-container"); // Re-select after adding
}
// Remove previous posts section before adding a new one
$("#simple-posts").remove();
// Add a new div to display posts
forumContainer.append("");
let postList = $("#simple-posts"); // Select newly created div
postList.empty(); // Clear old posts
postList.append("
Posts
"); // This will be the ONLY header
// Add "New Post" button
postList.append("");
if (posts.length === 0) {
postList.append("
`);
});
}
console.log("Posts successfully updated.");
$("#create-post-btn").click(() => {
let author = prompt("Enter your name:");
let content = prompt("Enter post content:");
if (!author || !content) return;
console.log(`Attempting to create post for topic: ${topicId}`);
item.server("create_post", [topicId, author, content], function(response, error) {
if (error) {
console.error("Error creating post:", error);
return;
}
if (response.error) {
console.error("Server Error:", response.error);
} else {
console.log("Post created successfully, refreshing posts...");
loadPosts(topicId);
}
});
});
});
}
$("#create-topic-btn").click(() => {
let title = prompt("Enter topic title:");
if (!title) return;
item.server("create_topic", [title], function (response, error) {
if (error) return console.error("Error creating topic:", error);
renderTopics();
});
});
forumContainer.append($(".topics-view").clone()); // Ensures the topic list is always present
renderTopics(); // Load topics immediately
}
The HTML template used by that example code is:
Topics view
Forum Topics
Posts view
Posts
This back-end code demonstrates how to not only use the Jam.py ORM, but also how to integrate custom SQLAlchemy code, to interact with the database tables created by Jam.py:
from datetime import datetime
import sqlalchemy
from sqlalchemy import create_engine, Column, Integer, String, DateTime, ForeignKey, MetaData, inspect
from sqlalchemy.orm import relationship, declarative_base, Session
Base = declarative_base()
DATABASE_URL = "sqlite:////root/jamforum3/jam_forum3"
engine = create_engine(DATABASE_URL, echo=True)
class Topic(Base):
__tablename__ = "JAM_FORUM_TOPICS"
id = Column(Integer, primary_key=True)
title = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
posts = relationship("Post", back_populates="topic")
class Post(Base):
__tablename__ = "JAM_FORUM_POSTS"
id = Column(Integer, primary_key=True)
deleted = Column(Integer, default=0) # Default to 0 like Jam.py
master_id = Column(Integer, ForeignKey("JAM_FORUM_TOPICS.id"), nullable=False) # Matches MASTER_ID
master_rec_id = Column(Integer, nullable=True) # Mimic Jam.py's MASTER_REC_ID
author = Column(String, nullable=False)
content = Column(String, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
topic = relationship("Topic", back_populates="posts")
def create_post(self, topic_id, author, content): #(topic_id, author, content):
try:
print(f"Creating new post for topic {topic_id}...")
with Session(engine) as session:
# Fetch topic to determine MASTER_ID # MASTER_REC_ID
topic = session.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
print(f"ERROR: Topic {topic_id} not found!")
return {"error": "Topic not found"}
# Assign MASTER_REC_ID as the Topic ID
new_post = Post(
master_id=7, # topic_id, # Matches MASTER_ID
master_rec_id=topic.id, # 7, # topic.id, # Ensures MASTER_REC_ID is set!
author=author,
content=content,
created_at=datetime.utcnow()
)
session.add(new_post)
session.commit()
print(f"Post created successfully! ID: {new_post.id}, MASTER_REC_ID: {new_post.master_rec_id}")
return {"message": "Post Created", "id": new_post.id, "master_rec_id": new_post.master_rec_id}
except Exception as e:
print(f"ERROR: Failed to create post - {e}")
return {"error": str(e)}
def get_all_topics(item):
item.open()
return [{"id": t.id.value, "title": t.title.value} for t in item]
def get_posts_by_topic(item, topic_id):
with Session(engine) as session:
print(f"Fetching posts for topic {topic_id}...")
session.expire_all() # Force refresh
# Debugging log
print(f"Querying posts where MASTER_ID = {topic_id}")
posts = session.query(Post).filter(Post.master_rec_id == topic_id).all()
results = [
{
"id": p.id,
"author": p.author,
"content": p.content,
"created_at": p.created_at.strftime("%Y-%m-%d %H:%M:%S")
}
for p in posts
]
print(f"Returning posts for topic {topic_id}: {results}")
return results
def create_topic(item, title):
item.append()
item.title.value = title
item.created_at.value = datetime.now()
item.post()
item.apply()
return {"message": "Topic Created", "id": item.id.value}
The two examples below demonstrate how to display automatically generated UI data grids for the forum database tables above, on separate task pages:
function on_view_form_created(item) {
$('#content').empty();
$('#content').append($('
'));
let c = task.details.posts.copy(); // task.details.items[0] // task.details.task.posts.copy();
c.create_table($('#table'));
c.open();
}
function on_view_form_created(item) {
$('#content').empty();
$('#content').append($('
'));
let c = task.journals.topics.copy(); // task.details.items[0] // task.details.task.posts.copy()
c.create_table($('#table'));
c.open();
}
This code duplicates a row in a task's database table:
function on_view_form_created(item) {
var duplicateSqlBtn = item.add_view_button('Duplicate');
duplicateSqlBtn.on('click', function() {
const field1 = item.field1.value;
const field2 = item.field2.value;
const field3 = item.field3.value;
item.append();
item.field1.value = field1;
item.field2.value = field2;
item.field3.value = field3;
item.post();
item.apply();
item.refresh_page(true);
});
}
This version automatically copies all field records:
function on_view_form_created(item) {
var duplicateSqlBtn = item.add_view_button('Duplicate');
duplicateSqlBtn.on('click', function() {
let dataToDuplicate = {};
item.each_field(function(field) {
if (field.field_name.toLowerCase() !== 'id') {
dataToDuplicate[field.field_name] = field.value;
}
});
item.append();
for (const fieldName in dataToDuplicate) {
if (item.field_by_name(fieldName)) {
item.field_by_name(fieldName).value = dataToDuplicate[fieldName];
}
}
item.post();
item.apply();
item.refresh_page(true);
});
}
To create a custom menu, instead of having jam.py automatically generate menu links under Catalog and
We will modify the on_page_loaded function in the Task's Client Module to provide the custom_menu option to the task.create_menu function.
Code (Task Client Module):
// Standard setup often found here (keep it for basic functionality)
$("title").text(task.item_caption); // Set browser tab title
$("#project-title").text(task.item_caption); // Set title on the page (if element exists)
if (task.safe_mode) {
// Code to display user info and logout link (if using safe mode)
$("#user-info").text(task.user_info.role_name + ' ' + task.user_info.user_name);
$('#log-out').show().click(function(e) {
e.preventDefault();
task.logout();
});
}
// Make container visible AFTER potential setup
$('#container').show();
// --- Define Your Simplest Custom Menu ---
let mySimpleMenu = [
// 1. A direct top-level link to the 'Customers' item view
task.catalogs.customers,
// 2. A top-level dropdown menu named "Sales"
['Sales', [ // First element is the title, second is an array of submenu items
task.journals.invoices, // Link to 'Invoices' item view
task.catalogs.products, // Link to 'Products' item view
task.catalogs.notes
]]
// You could add more top-level items or dropdowns here following the patterns above.
// An empty string '' would add a separator line.
];
// --- Call create_menu with the custom_menu option ---
task.create_menu(
$("#menu"), // The jQuery selector for the main menu
element in index.html
$("#content"), // The jQuery selector for the content
where item views will load
{ // Options object
custom_menu: mySimpleMenu, // ** This is the key part **
view_first: task.catalogs.customers // Optional: Automatically open the Customers view first
}
);
// --- Setup for any STATIC menu items (like About) if you have them in index.html ---
// If you have
in index.html:
$("#menu-right #about a").click(function(e) {
e.preventDefault();
task.message(task.templates.find('.about'), {title: 'About This App'}); // Assumes you have
in templates
});
// Add handlers for other static items (#pass, #help, etc.) if needed.
}
// --- IMPORTANT: Ensure this function replaces or overrides the default task.on_page_loaded ---
// You might need to add this line at the end of your Task Client Module:
task.on_page_loaded = on_page_loaded;
// Also ensure the necessary Jam.py Item objects (customers, invoices, products)
// actually exist and are visible in the Builder.
// THIS IS THE ORIGINAL TOP LEVEL TASK's on_page_loaded() FUNCTION:
// function on_page_loaded(task) {
// $("title").text(task.item_caption);
// $("#title").text(task.item_caption);
// if (task.safe_mode) {
// $("#user-info").text(task.user_info.role_name + ' ' + task.user_info.user_name);
// $('#log-out')
// .show()
// .click(function(e) {
// e.preventDefault();
// task.logout();
// });
// }
// if (task.full_width) {
// $('#container').removeClass('container').addClass('container-fluid');
// }
// $('#container').show();
// task.create_menu($("#menu"), $("#content"), {
// // splash_screen: '
Application
',
// view_first: true
// });
// // $(document).ajaxStart(function() { $("html").addClass("wait"); });
// // $(document).ajaxStop(function() { $("html").removeClass("wait"); });
// }
// below this code, keep the on_view_form_created(item) function as it is by default
The way you install Jam.py is:
python3 -m venv NEWFOLDERNAME
cd NEWFOLDERNAME
source bin/activate
pip install jam.py
python3 bin/jam-project.py
python3 server.py 9003 # choose any preferred port
To import an existing sqlite db and install a newer sqlalchemy version, together with Jam.py's sqlalchemy version:
rm -rf /root/NEWFOLDERNAME/lib/python3.8/site-packages/jam/third_party/sqlalchemy
pip install --force-reinstall sqlalchemy==2.0.37
python3 -c "import sqlalchemy; print(sqlalchemy.__version__)"
nano /root/NEWFOLDERNAME/lib/python3.8/site-packages/jam/server_classes.py
Find this line:
from .third_party.sqlalchemy.pool import NullPool, QueuePool
Replace it with:
from sqlalchemy.pool import NullPool, QueuePool
python3 server.py 9003
WHEN INITIALIZING THE PROJECT (at yoursite:9003/builder.html), CONFIGURE THE *SAME PROJECT NAME* (lowercase), BUT THE *NEW DATABASE NAME*
NOTE: If using SQLite, and importing a project, edit task.dat in the zip file, and change:
DATABASE_URL = \"sqlite:////root/ORIGIAL_FOLDER_NAME/ORIGINAL_DB_FILENAME
to
DATABASE_URL = \"sqlite:////root/NEWFOLDERNAME/NEW_DB_FILENAME (filename on new server)
Be sure to save that edited task.dat file back into the zip file
The full Jam.py documentation is available at https://jam-py.com/docs/contents.html . A downloadable PDF version is available at https://jampy-docs.readthedocs.io/_/downloads/en/latest/pdf . An additional helpful guide is available at https://jampy-application-design-tips.readthedocs.io/en/latest and https://jampy-application-design-tips.readthedocs.io/_/downloads/en/latest/pdf . The official github repository, with a variety of example application links, is available at https://github.com/jam-py/jam-py .
The following summary of the Jam.py documentation explains how to work with the no-code and built-in database functionalities (ORM) on both the front-end and back-end:
Jam.py Framework Overview & Essential Concepts
1. Introduction
Jam.py is a low-code, open-source Python framework for building database-driven web applications. It follows the task tree model, where objects (catalogs, journals, reports) interact with an SQL database. The framework supports lookup fields, filters, CRUD operations, and event-driven programming on both the client and server.
2. Building Your First Project
2.1 Creating a Catalog
Open Application Builder.
Add a new Catalog (e.g., Customers) with fields:
Firstname (TEXT, 30 chars)
Lastname (TEXT, 30 chars, Required)
Phone (TEXT, 20 chars)
Click OK to generate the corresponding SQL table.
2.2 Creating a Journal
Journals store transactional data linked to catalogs.
Add a Journal (e.g., Contacts).
Define fields:
Contact date (DATETIME, default: CURRENT DATETIME)
Notes (TEXT)
Add a lookup field:
Customer (references Customers catalog via Lastname).
Click OK.
Contacts now links to Customers, enabling relational data management.
3. Lookup Fields & Lists
3.1 Lookup Fields (Foreign Keys)
A lookup field allows linking between tables.
For Contacts, the Customer lookup field:
Stores the CustomerID
Displays Lastname instead of the ID
Enables typeahead/autocomplete
Additional lookup fields (Firstname, Phone) can be master-linked to Customer to avoid redundant table joins.
3.2 Lookup Lists (Dropdowns)
For predefined choices, use lookup lists:
Create a lookup list (e.g., Status).
Add values:
1 - New
2 - In Progress
3 - Completed
Assign Status as a lookup field in Contacts, using the list.
4. Forms & UI Customization
4.1 Default Forms
Jam.py auto-generates:
View Forms (display records)
Edit Forms (CRUD operations)
Filter Forms (search data)
4.2 Customizing Forms
Use Edit Form Dialog to reorder/hide fields.
Define default values (CURRENT DATETIME for Contact date).
Enable Typeahead for lookup fields.
4.3 Using Bootstrap
Jam.py integrates with Bootstrap for responsive layouts:
Use tabs to organize fields.
Define grid layouts for structured form design.
5. Data Handling & CRUD Operations
5.1 Dataset Basics
Each catalog/journal operates as a dataset.
Call open() to load records.
Use .next() to iterate rows.
Example (Python server-side):
def get_customers(customers):
customers.open()
for c in customers:
print(c.firstname.value, c.lastname.value)
5.2 Editing Data
Operations change dataset state:
edit(), append(), insert() → Modify records.
post() → Save changes.
apply() → Write to database.
Example (Client-side JavaScript):
function update_customer(customers) {
customers.open();
customers.edit();
customers.firstname.value = "John";
customers.post();
customers.apply();
}
6. Filters & Querying Data
6.1 Defining Filters
Filters refine data in UI without writing SQL. Example filter (invoicedate__ge):
invoices.filters.invoicedate1.value = datetime.now() - timedelta(days=30)
invoices.open()
6.2 Query Options
Use .set_where() to filter programmatically:
invoices.set_where(invoicedate__ge=datetime.now())
invoices.open()
Supported conditions:
Operator SQL Equivalent
eq =
ne <>
gt >
lt <
contains LIKE '%val%'
7. Reports & Exporting Data
7.1 Generating Reports
Use LibreOffice templates.
Call .print() on report items.
Example:
task.customers_report.print()
7.2 Exporting to CSV
Jam.py supports exporting datasets:
task.customers.export_to_csv("customers.csv")
8. Client & Server Logic
8.1 Event Handling
Jam.py is event-driven. Common hooks:
on_view_form_created (UI customization)
on_apply (Custom save logic)
on_open (Modify queries dynamically)
Example (on_apply event):
def on_apply(item, delta, params):
for d in delta:
if d.total.value > 100:
d.vip.value = True # Flag as VIP customer
8.2 Running Server Methods
Call server functions from the client:
task.customers.server("my_server_function", arg1, arg2)
9. Security & User Authentication
9.1 Safe Mode
If enabled, users must log in.
Permissions restrict read/write access.
9.2 Enforcing Roles
Assign roles:
def on_created(task):
task.users.open()
task.users.role = "Admin"
10. Deployment
10.1 Running Locally
python server.py
10.2 Hosting
Deploy with:
Gunicorn (Linux)
IIS (Windows)
Docker (Containerized apps)
Here is some more useful summarized information from the Jam.py documentation:
1. Task Tree & Object Model
Jam.py task tree organizes application elements.
A task contains:
Catalogs (Data tables)
Journals (Transactional logs)
Details (Child tables)
Reports (Output generation)
Each element has attributes (field_name, lookup_value) and methods (open(), apply()).
Example: Accessing Objects in the Task Tree
def on_apply(item, delta, params):
customers = item.task.catalogs.customers.copy()
customers.open()
for c in customers:
print(c.firstname.value, c.lastname.value)
2. Creating & Managing Forms
Jam.py auto-generates forms but supports customization.
Default Form Templates
Located in index.html inside:
...
...
Programmatically Modifying Forms
function on_edit_form_created(item) {
item.edit_form.find("#ok-btn").on('click', function() {
item.apply_record();
});
}
3. Event Handling (Client & Server)
Jam.py uses event-driven programming.
Common Client-Side Events
Event Name Description
on_page_loaded(task) Fires when the app loads.
on_view_form_created(item) Customizes view forms.
on_edit_form_created(item) Customizes edit forms.
Common Server-Side Events
Event Name Description
on_open(item) Modifies queries before execution.
on_apply(item) Executes custom save logic.
on_created(task) Initializes application state.
Example: Customizing Data Query (Server)
def on_open(item, params):
item.set_where(status__eq='Active')
4. Advanced Data Operations
Jam.py datasets behave like ORM objects, allowing:
Filtering (set_where)
Sorting (set_order)
Batch processing
Filtering & Sorting Example
customers.set_where(country__eq="USA")
customers.set_order("lastname")
customers.open()
Iterating Over Data
for c in customers:
print(c.firstname.value, c.lastname.value)
5. Lookup Fields & Foreign Keys
Lookup fields link to other tables.
Used for dropdowns and typeahead fields.
Example: Defining a Lookup Field
customers.firstname.lookup_value = "John"
Example: Creating a Lookup Field Programmatically
def create_lookup_field():
field = task.customers.add_field("region", "RegionLookup")
field.lookup_item = task.regions
field.lookup_field = "region_name"
6. Customizing UI with Bootstrap
Jam.py integrates with Bootstrap for responsive layouts.
Example: Adding Tabs in an Edit Form
Example: Dynamically Updating UI
function on_view_form_created(item) {
$("#header-title").text("Customer Details");
}
7. Permissions & User Roles
Jam.py enforces role-based access control.
Setting User Roles (Server-Side)
def on_created(task):
task.users.open()
task.users.role = "Admin"
Checking User Permissions
if task.user_info.role_name == "Admin":
task.customers.open()
8. Deployment & Server Configuration
Jam.py supports WSGI, Docker, and IIS.
Running Jam.py Locally
python server.py
Deploying with Gunicorn
gunicorn -w 4 server:app
Configuring Static Files in Production
Modify server.py:
app = create_app(static_folder="static")
Understand the Task Tree – How items relate (e.g., journals, catalogs).
Know Default UI Behaviors – How forms are auto-generated and customized.
Handle CRUD Operations – Using open(), apply(), and event-driven workflows.
Use Lookup Fields Correctly – How to define and retrieve related data.
Support Client-Server Logic – Using JavaScript for UI, Python for backend.
Implement Security & Roles – Restrict access based on user permissions.
Deploy Correctly – Understand production setup.
Do you understand from this text how to structure code in Jam.py?