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

  1. Frontend: User interactions like adding, updating, and deleting programming languages.
  2. API Integration: RESTful communication for CRUD operations.
  3. 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

Postman Organization

Get Request:

Here is a working Get Request and ist output:

Get 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.

Error Response 400

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 404

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 the matched_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 of 0 if not provided.

  • if not name or not creator:: Validates that the name and creator fields are present, returning a 400 error if missing.

  • new_language = Language(...): Creates a new Language 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 a 500 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 the id 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 the read() method.

Parameters and Return Type

Parameters

The get method receives data via query parameters:

  1. id (Query Parameter): Represents the id used to filter languages.

Return Type

The method uses jsonify to return a JSON response:

  1. Success Response:
    [
        {
            "id": 1,
            "name": "Python",
            "creator": "Guido van Rossum",
            "popularity": 95
        },
        {
            "id": 2,
            "name": "JavaScript",
            "creator": "Brendan Eich",
            "popularity": 97
        }
    ]
    
  2. Error Responses:
    • Missing Parameter: (Error Code 400)
      {
          "message": "Language ID is required"
      }
      
    • No Languages Found: (Error Code 404)
      {
          "message": "No languages found"
      }
      

The get method showcases:

  1. Sequencing to structure the retrieval, validation, and formatting of data.
  2. Selection to handle missing parameters and empty results with appropriate error messages.
  3. 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 and creator actually exist.
  • Create a Language object and save it to the database using the create method.
  • Return the created object as a JSON response.

Return/Response from the Method

Success Response

When the request is successful, the server returns:

  1. HTTP Status Code: 200 OK.
  2. 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:

  1. HTTP Status Code: 400 Bad Request.
  2. 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