Author: Argyrios Tsagkalidis
Location: Thessaloniki, Greece
License: BSD-3-Clause
Video Demo: CS50X Final Project - Swappr
Swappr is a web app project that helps the users to swap their rented houses.
The users create an account, uploads their rented house's specifications, and explore potential matches for a hassle-free swap experience.
The app uses an intelligent algorithm that suggests compatible house swap options, ensuring that the user's preferences align with potential matches. The algorithm uses a matching score based on house characteristics, house location, and desired move destination.
Swappr repo comes with no .db or .log files. These necessities will be created on program initialization. To initiate swappr run:
python app.py
But before you run the program for the first time, make sure that you have created a virtual environment and have installed all Python dependencies.
Before you run Swappr it is recommended to create a virtual environment where all Python dependencies will be installed. While in working directory run:
python -m venv .venv
Notice that a .venv
folder has been created.
After your venv creation, you should install all swappr Python dependencies. While in working directory run:
pip install -r requirements.txt
Swappr comes with an embedded argument parser and developers can check its options using python app.py -h
. Optinal arguments are:
-d
or--debug
enables Flask's embedded debug mode for testing purposes-b
or--backup
runs automated backup routines for the database and log files-l
or--limiter
enables a request limiter to protect the backend from spam-p
or--premademockups
inserts a premade JSON of mockup users and submitted houses into the database. The premade mockups are tailored in such way so that developers can test the matching algorithm for every probable location/desired move location scenario-m MOCKUPSGEN MOCKUPSGEN
or--mockupsgen MOCKUPSGEN MOCKUPSGEN
creates mockup users and submitted houses to test the matching algorithm. First integer is the number of users to be created, while the second is the number of the submitted houses. Submitted houses can't exceed the number of the mockup users - if so the program will limit submitted houses equal to users. The higher the number of mockups created, the higher the chance of catching a matching house during the test. Recommended 3000+ Mockups
Running app.py with -b
or --backup
argument initiates a simple backup functionality by triggering fbak.sh file. Doing two directories are created if not already present. First is bak.db and the second is bak.log. In there swappr.db and app.log are backed up into subfolders named by datetime logic. Backup functionality is usable only once per minute.
Swappr uses the following programming languages, frameworks, and dev tools:
- Python3 and Flask mini framework package for the backend
- HTML/CSS Bootstrap 5.2/JavaScript for the frontend
- Jinja2 for the HTML templates
- AJAX for asynchronous request logic
- SQLite3 for the database handling
- Bash for backup functionalities
- noUiSlider JavaScript package to control input values for searching filters
- jQuery.validate package for extra frontend validation
Frontend convention relies on HTML Jinja2 templates that are extensions of the prototype layout.html file, as well as in JavaScript functionalities from script.js file and CSS styling from styles.css.
Frontend also relies on the following list of well known JavaScript libraries:
- Bootstrap 5.2
- jQuery 3.7.1
- jQuery Validation v1.19.5
- Popper
- noUiSlider
Backend convention relies on splitted .py files (aka modules) where the proper code for every related section is contained. All modules and their functions are imported in a main module named as swapprfunctions which in turn is imported in app.py. For more information regarding backend functionality check section Custom modules, but first you should read some information about app.py - the main file where Flask framework get initialized.
This is the main file where Swappr Initialization and the Route Creation for all sections take place.
When app.py initiates the following functionalities take action:
- Configures a logger using loguru library
- Checks for given arguments such as
--backup
,--limiter
, and/or--premademockups
/--mockupsgen MOCKUPSGEN MOCKUPSGEN
to call corresponding functions. - Checks if SQL database is present or else it creates it with the proper schema in swappr.db
- Checks if locations table in swappr.db matches with all the data in locations.json file. If new locations are present in the JSON file, then they are imported as new entries in swappr.db locations table
Most routes have their corresponding .py file where their dependent functions are present. For instance /search route calls some functions that are imported from fsearch.py, while /signup route draws functions from fsignup.py, and so on.
Swappr uses validation logic for whatever input data the user's may request. Validation takes place in frontend using browser's embedded validation, jQuery Validation Plugin methods, and custom JavaScript functions, while backend is being protected by custom Python functions.
Proper backend error handling prevents Swappr from crushes, where the errors are logged thanks to loguru Python package.
Running app.py with -l
or --limiter
argument initiates flask_limiter - a ready to go package that can protect backend from spam requests. The default request limits per user are 2000 per day or 500 per hour. Limits for specific routes are given below:
Route | Requests per minute |
---|---|
/signup | 60 |
/signin | 60 |
/update_exposure | 30 |
/edit_submission | 60 |
/search | 100 |
/password_reset | 30 |
/update_username | 30 |
/delete_account | 30 |
Currently Swappr has no unit tests. In the future proper testing should be executed using pytest library.
Nevertheless, a python script named tcreatemocksubmissions.py is designed to create mockups (that is random fake users and submitted houses), and then import them into swappr.db for testing purposes.
To do that run:
python app.py -m MOCKUPSGEN MOCKUPSGEN
or
python app.py --mockupsgen MOCKUPSGEN MOCKUPSGEN
where the first MOCKUPSGEN is an integer that determines the number of mockup users, while the second one determines the number of mockup submitted houses. Submitted houses number can't exceed the number of users - if so the script handles the arguments to set them as equals.
By default the only static username that is created is admin and developers should use it for debugging.
Username | Password |
---|---|
admin | admin |
It is recommended to generated some thousands of mockups to have a chance in getting matching houses
Below are enlisted all the Python frameworks, libraries and packages needed for this application.
- json
- secrets
- subproccess
- loguru
- datetime
- argparse
- flask
- flask_limiter
- werkzeug.security
- string
- sqlite3
- re
- functools
- tqdm
- random
- fictional_names
Frameworks, libraries, and packages that are not embedded in Python3 should be installed using requirements.txt as previously in this article mentioned.
Swappr uses custom modules that hold constant variables and functions to be called in app.py. What follows is the description of these modules in separate section parts.
This module uses argparse Python package to create argument option instances. Added argument are stored in argparser variable, which is imported in whatever .py module is needed. You may check the argument options on section Running the app, in Argument parser block.
Contains functions needed in /account route.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
password_reset_validation |
(oldpassword, new_password, confirm_new_password, hash) | Checks password reset input validityn | None | check_password_hash 1 strong_password 2 |
update_username_validation |
(new_username, user_id) | Checks password reset input validity | None | username_exists 2 |
delete_account_validation |
(delete_account_confirmation, email) | Checks email input validity for account deletion | None | None |
Sources: 1werkzeug.security package, 2fhelpers.py
Contains functions needed in many routes.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
cursor_execute |
(query, *args) | Dynamically executes SQLite3 queries | create_database_tables |
None |
cursor_fetch |
(query, *args) | Dynamically fetches data SQLite3 database | create_database_tables |
tuples_to_dict |
tuples_to_dict |
(keys_tuple, values_tuple) | Converts two tuples into one dictionary | cursor_fetch |
None |
strong_password |
(password) | Checks if a password is strong enough | signup_validation 1 password_reset_validation 2 |
None |
email_exists |
(email) | Checks if email already exists in the database | signup_validation 1 |
None |
username_exists |
(username, *args) | Checks if username already exists in the database | signup_validation 1 update_username_validation 2 |
None |
get_list_of_values |
(json_data, column_name) | Converts a json file into a list with values of the selected column | validate_submitted_location 3 validate_searched_location 4 |
None |
check_submitted_location |
(submitted_value, valid_values, error_message) | Checks submitted location validity | validate_submitted_location 3 validate_searched_location 4 |
None |
login_required |
(f) | Decorate routes to require login | None | decorated_function |
comma |
(integer) | Formats an integer by placing commas between thousands | None | None |
whitespace |
(text) | Formats a snakecase string into a readable title | None | None |
Sources: 1fsignup.py, 2faccount.py, 3fsubmit.py, 4faccount.py
Contains constant variables and functions needed for history logger.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
log |
(message, level='INFO', indent=28) | Dynamically handles log indentations and message level | None | None |
initialize_logger |
None | Initializes loguru history logger | None | None |
log_new_locations |
(locations_update, new_locations_flag) | Add brief log info about newly inserted location entries in the database | None | None |
Contains the algorithmic functions and constant variables needed in /search route.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
room_tolerance_factor |
(tolerance, room) | Returns the tolerance factor that adjusts the maximum value of the criteria search ranges | tolerance_factors |
None |
tolerance_factors |
(tolerance) | Returns varied multipliers that affect the criteria ranges to be used in the matching score algorithm | None | room_tolerance_factor |
criteria_ranges |
(primary_submission, TOLERANCE_FACTORS) | Returns varied ranges to be used in the calculation of the matching score for each search house result | None | None |
validate_searched_digits |
(square_meters, rental, bedrooms, bathrooms) | Validates digit-required and ranged-required fields | search_validation |
None |
validate_searched_location |
(city, municipality, region) | Validates location input | search_validation |
cursor_fetch 1 get_list_of_values 1 check_submitted_location 1 |
search_validation |
(exposure, house_type, square_meters, rental, bedrooms, bathrooms, city, municipality, region) | Validates input data | None | validate_searched_digits validate_searched_location |
calculate_location_matching_score |
(result, primary_submission) | Calculates and returns a location matching score | location_matching |
None |
location_matching |
(primary_submission, search_results) | Iterates all search results and adds into them the calculated location matching score | None | calculate_location_matching_score |
calculate_house_matching_score |
(result, CRITERIA_RANGES) | Calculates and returns a house characteristics matching score | None | house_matching |
house_matching |
(search_results, CRITERIA_RANGES) | Iterates all search results and adds into them the calculated house characteristics matching score | None | calculate_house_matching_score |
matching_summary |
(search_results) | Iterates all search results and adds into them the summary matching score | None | None |
Source: 1fhelpers.py
Contains a function needed in /signin route.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
signin_validation |
(username, password, user_data) | Validates login credentials | None | 1check_password_hash |
Source: 1werkzeug.security package
Contains a function needed in /signup route.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
signup_validation |
(email, username, password, confirm_password) | Validates signup credentials | None | 1strong_password 1email_exists 1username_exists |
Source: 1fhelpers.py
Contains functions needed in database structuring.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
create_database_tables |
None | Creates database schema | None | 1cursor_execute |
import_locations |
None | Syncs new entries between location.json and swappr.db | None | None |
Contains constant variables and functions needed in both /submit and edit_submission routes.
Function name | Parameters | Description | Child of | Parent of |
---|---|---|---|---|
validate_submitted_digits |
(square_meters, rental, bedrooms, bathrooms) | Creates database schema | None | 1cursor_execute |
import_locations |
None | Validates digit-required and ranged-required fields | None | None |
validate_submitted_location |
(city, municipality, region) | Validates location input | search_validation |
get_list_of_values 1 check_submitted_location 1 cursor_fetch 1 |
submission_validation |
(all_field_values, exposure, house_type, square_meters, rental, bedrooms, bathrooms, city, municipality, region, city_destination, municipality_destination, region_destination) | Validates input data | None | validate_submitted_digits validate_submitted_location |
user_submissions_exist |
(user_id) | Checks if user has at least one primary submission | determine_primary_submission_status |
cursor_fetch |
determine_primary_submission_status |
(primary_submission, primary_submission_locked, user_id) | Determines primary submission status | None | user_submissions_exist cursor_execute |
handle_primary_submission_upon_deletion | (submission_id, user_id) | Handles primary submission in database upon submission deletion | None | cursor_fetch 1 cursor_execute 1 |
Source: 1fhelpers.py