Blog/Reflection Sprint 5
A blog post about the work I did during Sprint 5.
Full Stack Feature Implementation: Sharing Preferences (Coding Languages)
This is specifically part to the creation portion of prism. As Yash described before, we said that we wanted to create Prism as a website to have fun, collaborate, and CREATE.
Programming is a collaborative process that transforms ideas into reality. In this blog, we’ll discuss the implementation of a Language Management application, designed to manage programming languages. This project showcases the synergy between frontend, backend, and database layers, demonstrating how individual features and APIs come together to create a seamless experience.
Purpose of the Program
The purpose of this program is to provide a full-stack solution for managing programming languages. Users can add, update, and delete programming languages through a web interface, and the changes are reflected in the backend database.
Individual Features
- Frontend: User interactions like adding, updating, and deleting programming languages.
- API Integration: RESTful communication for CRUD operations.
- Backend Model: Data storage and retrieval via SQLAlchemy models.
Input/Output Requests (DEMO)
Live Input Example: Managing Programming Languages
Here’s how the frontend handles adding a new programming language using a POST request.
The outputted JSON is iterated through, the various output properties are then put into HTML objects, and then appended to the DOM.
async function addLanguage(languageData) {
try {
const response = await fetch(`${pythonURI}/api/language`, {
...fetchOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(languageData)
});
if (!response.ok) {
throw new Error('Failed to add language: ' + response.statusText);
}
const newLanguage = await response.json();
const languageList = document.getElementById('languageList');
const listItem = document.createElement('li');
listItem.textContent = `${newLanguage.name} by ${newLanguage.creator}`;
languageList.appendChild(listItem);
} catch (error) {
console.error('Error adding language:', error);
}
}
For data management purposes, it is important to be able to initialize, backup, and restore data from a database. My feature meets these requirements, as evidenced by the code below.
Initializing Code (Skip We Only want Backend Frontend)
# File Location: model/language.py
def initLanguages():
"""
Initializes the Language table and adds tester data to the table.
Uses:
The db ORM methods to create the table.
Instantiates:
Language objects with tester data.
Raises:
IntegrityError: An error occurred when adding the tester data to the table.
"""
with app.app_context():
"""Create database and tables"""
db.create_all()
"""Tester data for table"""
languages = [
Language(name="Python", creator="Guido van Rossum", popularity=95),
Language(name="JavaScript", creator="Brendan Eich", popularity=97),
Language(name="C++", creator="Bjarne Stroustrup", popularity=90),
]
for language in languages:
try:
language.create()
print(f"Record created: {repr(language)}")
except IntegrityError:
db.session.remove()
print(f"Record exists or error: {language.name}")
Backup Code (Skip We Only want Backend Frontend)
# File Location: main.py
def extract_data():
data = {}
with app.app_context():
data['language'] = [language.read() for language in Language.query.all()]
return data
def save_data_to_json(data, directory='backup'):
if not os.path.exists(directory):
os.makedirs(directory)
for table, records in data.items():
with open(os.path.join(directory, f'{table}.json'), 'w') as f:
json.dump(records, f)
print(f"Data backed up to {directory} directory.")
def backup_data():
data = extract_data()
save_data_to_json(data)
backup_database(app.config['SQLALCHEMY_DATABASE_URI'], app.config['SQLALCHEMY_BACKUP_URI'])
The backup_data()
function is called to backup the data.
Restore Code (Skip We Only want Backend Frontend)
# File Location: model/language.py
@staticmethod
def restore(data):
"""
Restore languages from a list of dictionaries.
Args:
data (list): A list of dictionaries containing language data.
Returns:
dict: A dictionary of restored languages keyed by name.
"""
restored_languages = {}
for language_data in data:
_ = language_data.pop('id', None) # Remove 'id' from language_data if present
name = language_data.get("name", None)
language = Language.query.filter_by(name=name).first()
if language:
language.update(language_data)
else:
language = Language(**language_data)
language.create()
restored_languages[name] = language
return restored_languages
# File Location: main.py
def load_data_from_json(directory='backup'):
data = {}
for table in ['language']:
with open(os.path.join(directory, f'{table}.json'), 'r') as f:
data[table] = json.load(f)
return data
# Restore data to the new database
def restore_data(data):
with app.app_context():
_ = Language.restore(data['language'])
print("Data restored to the new database.")
@custom_cli.command('restore_data')
def restore_data_command():
data = load_data_from_json()
restore_data(data)
The restore_data
method is used to restore the JSON data back to the actual database, an important part of data management.
List and Error Codes
Postman is used as a tool to test backend API endpoints. Let me make a GET request to the Language API and we will receive the language data in JSON format, along with an error code, 200, which represents a successful request.
Organization of Postman: to /api/language
Get Request:
Here is a working Get Request and ist output:
Error Response
Here is what happens when there are invalid or missing parameters. It should return an error code of 400, which indicates a malformed request.
Error Response
Here is what happens when there are invalid or missing parameters. It should return an error code of 400, which indicates a malformed request.
Lists, Dictionaries, Database
Rows
In the language management application, database queries are executed to retrieve rows of data, representing multiple programming languages or entities. These rows are returned as Python lists. This functionality is done by third-party tools such as SQLAlchemy, which allows us to use Python code to manage SQL Databases.
Example:
matched_users = []
# Iterate over all users
for user in all_users:
# Split each user's interests into a set of interests
user_interests = set(user._interests.split(", "))
# Find the shared interests between the current user and each user
shared_interests = current_user_interests.intersection(user_interests)
# If there are shared interests, add the user to the matched users list
if shared_interests:
matched_users.append({
'username': user._name,
'shared_interests': list(shared_interests)
})
Explanation:
-
matched_users = []
: Initializes an empty list to store user data with shared interests. -
for user in all_users:
: Iterates through all users in the database except the current user. -
user_interests = set(user._interests.split(", "))
: Splits each user’s interests into a set of unique interests. -
shared_interests = current_user_interests.intersection(user_interests)
: Finds the shared interests between the current user and each other user by performing a set intersection. -
if shared_interests:
: Checks if there are any shared interests. -
matched_users.append({...})
: Adds a dictionary containing the user’s name and their shared interests as a list to thematched_users
list. -
matched_users.sort(key=lambda x: len(x['shared_interests']), reverse=True)
: Sorts the matched users by the number of shared interests in descending order.
Result:
This list stores dictionaries where each dictionary represents a user and their common interests with the current user. The list is then converted to JSON format using jsonify
and sent to the frontend.
Another Example
@token_required()
def post(self):
"""
Add a new language entry.
"""
body = request.get_json()
# Validate required fields
name = body.get('name')
creator = body.get('creator')
popularity = body.get('popularity', 0) # Default popularity is 0
if not name or not creator:
return {'message': 'Name and creator are required'}, 400
try:
# Create a new language entry
new_language = Language(name=name, creator=creator, popularity=popularity)
new_language.create()
return jsonify({'message': 'Language added successfully', 'language': new_language.read()})
except Exception as e:
return {'message': 'Failed to create language', 'error': str(e)}, 500
Explanation
-
@token_required()
: Ensures the request is authorized by verifying a JWT token. -
body = request.get_json()
: Extracts the JSON body from the incoming request. -
name = body.get('name')
: Retrieves the language name from the JSON payload. -
creator = body.get('creator')
: Retrieves the creator’s name. -
popularity = body.get('popularity', 0)
: Extracts the popularity score with a default of0
if not provided. -
if not name or not creator:
: Validates that thename
andcreator
fields are present, returning a400
error if missing. -
new_language = Language(...)
: Creates a newLanguage
instance using the provided data. -
new_language.create()
: Saves the new language entry to the database. -
return jsonify(...)
: Sends a JSON response containing the success message and the created language details. -
except Exception as e:
: Handles errors, returning a500
status with the error message.
CRUD Class for Columns
# File Location: model/language.py
def create(self):
"""
Creates a new language in the database.
Returns:
Language: The created language object, or None on error.
"""
try:
db.session.add(self)
db.session.commit()
except IntegrityError as e:
db.session.rollback()
logging.warning(f"IntegrityError: Could not create language '{self.name}' due to {str(e)}.")
return None
return self
def read(self):
"""
Retrieves language data as a dictionary.
Returns:
dict: A dictionary containing the language data.
"""
return {
"id": self.id,
"name": self.name,
"creator": self.creator,
"popularity": self.popularity
}
def update(self, data):
"""
Updates the language object with new data.
Args:
data (dict): A dictionary containing the new data for the language.
Returns:
Language: The updated language object, or None on error.
"""
if 'name' in data:
self.name = data['name']
if 'creator' in data:
self.creator = data['creator']
if 'popularity' in data:
self.popularity = data['popularity']
try:
db.session.commit()
except IntegrityError as e:
db.session.rollback()
logging.warning(f"IntegrityError: Could not update language with ID '{self.id}' due to {str(e)}.")
return None
return self
def delete(self):
"""
Deletes the language from the database.
"""
try:
db.session.delete(self)
db.session.commit()
except Exception as e:
db.session.rollback()
raise e
Through these four functions, CRUD functionality is established. These 4 methods in the model can be called by importing the Language model itself, and then calling the methods as you wish.
Algorithmic Design
Handling API Requests in Classes
The _CRUD
class defines CRUD operations:
# File Location: api/language.py
class _CRUD(Resource):
@token_required()
def post(self):
"""
Create a new language.
"""
current_user = g.current_user
data = request.get_json()
# Validate the presence of required fields
if not data or 'name' not in data or 'creator' not in data:
return {'message': 'Name and Creator are required'}, 400
# Create a new language object
language = Language(name=data['name'], creator=data['creator'], popularity=data.get('popularity', 0))
language.create()
return jsonify(language.read())
@token_required()
def get(self):
"""
Retrieve all languages.
"""
languages = Language.query.all()
if not languages:
return {'message': 'No languages found'}, 404
# Return the list of languages in JSON format
return jsonify([language.read() for language in languages])
@token_required()
def put(self):
"""
Update a language.
"""
current_user = g.current_user
data = request.get_json()
# Validate the presence of required fields
if 'id' not in data:
return {'message': 'Language ID is required'}, 400
if 'name' not in data:
return {'message': 'Language name is required'}, 400
# Find the language by ID
language = Language.query.get(data['id'])
if not language:
return {'message': 'Language not found'}, 404
# Update the language using the model's update method
updated_language = language.update(data)
if updated_language:
return jsonify(updated_language.read())
else:
return {'message': 'Failed to update language'}, 500
@token_required()
def delete(self):
"""
Delete a language by ID.
"""
data = request.get_json()
if 'id' not in data:
return {'message': 'Language ID is required'}, 400
# Find the language by ID
language = Language.query.get(data['id'])
if not language:
return {'message': 'Language not found'}, 404
# Delete the language
language.delete()
return {'message': 'Language deleted'}, 200
Let’s analyze one of them more deeply.
Sequencing, Selection, and Iteration
Sequencing
Sequencing refers to the order in which the operations are performed:
- Extract Parameter: The method starts by extracting the
id
from the request body. - Query Database: Using SQLAlchemy, it fetches the language with the given
id
. - Format Response: Converts the Language object into JSON format for the API response.
Selection
Selection is evident in the conditional checks:
- Check for Missing
id
: If theid
is not provided, the method returns a 400 Bad Request error. - Check for Empty Results: If no language is found for the given
id
, the method returns a 404 Not Found error.
Iteration
The method uses iteration to process each language:
- List Comprehension:
[language.read() for language in languages]
loops through all Language objects retrieved from the database and formats them into dictionaries using theread()
method.
Parameters and Return Type
Parameters
The get
method receives data via query parameters:
id
(Query Parameter): Represents theid
used to filter languages.
Return Type
The method uses jsonify
to return a JSON response:
- Success Response:
[ { "id": 1, "name": "Python", "creator": "Guido van Rossum", "popularity": 95 }, { "id": 2, "name": "JavaScript", "creator": "Brendan Eich", "popularity": 97 } ]
- Error Responses:
- Missing Parameter: (Error Code 400)
{ "message": "Language ID is required" }
- No Languages Found: (Error Code 404)
{ "message": "No languages found" }
- Missing Parameter: (Error Code 400)
The get
method showcases:
- Sequencing to structure the retrieval, validation, and formatting of data.
- Selection to handle missing parameters and empty results with appropriate error messages.
- Iteration to transform the database rows into a structured JSON response.
This ensures the API is robust and user-friendly while adhering to RESTful principles.
Call to Algorithm Request
Definition of Code Block to Make a Request
The frontend makes an API call using JavaScript’s fetch
function. Here is how the POST
method is used to create a new language.
async function addLanguage() {
const nameInput = document.getElementById('nameInput').value.trim();
const creatorInput = document.getElementById('creatorInput').value.trim();
const popularityInput = document.getElementById('popularityInput').value.trim();
if (nameInput && creatorInput) {
const postData = { name: nameInput, creator: creatorInput, popularity: popularityInput };
try {
const response = await fetch(`${pythonURI}/api/language`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postData),
});
const result = await response.json();
if (response.ok) {
console.log('Language added successfully:', result);
// Handle success by updating the UI
updateLanguageList(result);
} else {
console.error('Failed to add language:', result.message);
alert(result.message); // Display error message
}
} catch (error) {
console.error('Error making request:', error);
alert('An error occurred while adding the language.');
}
}
}
API Endpoint
The request is sent to the backend API endpoint at /api/language
via a POST
request. This endpoint corresponds to the _CRUD.post
method in the backend.
JSON Payload
name
: The name of the programming language.creator
: The creator of the programming language.popularity
: The popularity of the programming language.
Example Payload:
{
"name": "Python",
"creator": "Guido van Rossum",
"popularity": 95
}
Sequencing in the Backend
- Validate the request body by checking whether
name
andcreator
actually exist. - Create a
Language
object and save it to the database using thecreate
method. - Return the created object as a JSON response.
Return/Response from the Method
Success Response
When the request is successful, the server returns:
- HTTP Status Code:
200 OK
. - JSON Response:
{ "id": 1, "name": "Python", "creator": "Guido van Rossum", "popularity": 95 }
Handling Success in Frontend:
if (response.ok) {
const languageId = result.id;
const listItem = document.createElement('li');
listItem.textContent = `${result.name} by ${result.creator}`;
document.getElementById('languageList').appendChild(listItem);
console.log('Language added successfully:', result);
}
Error Response
If the request fails due to missing or invalid data, the server returns:
- HTTP Status Code:
400 Bad Request
. - JSON Response:
{ "message": "Name and Creator are required" }
Handling Errors in Frontend:
I have used try-catch statements in JavaScript to catch any potential errors, and simply remove that language from the frontend to make sure the site doesn’t go down and the user can still easily use the platform.
catch (error) {
console.error('Error adding language:', error);
alert('An error occurred while adding the language.');
}
For Loop
@staticmethod
def restore(data):
"""
Restore languages from a list of dictionaries, replacing existing entries.
Args:
data (list): List of dictionaries containing language data.
Returns:
dict: Dictionary of restored Language objects.
"""
with app.app_context():
# Clear the existing table
db.session.query(Language).delete()
db.session.commit()
restored_classes = {}
for language_data in data:
language = Language(
name=language_data['name'],
creator=language_data['creator'],
popularity=language_data.get('popularity', 0)
)
language.create()
restored_classes[language_data['id']] = language
return restored_classes