Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/development' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
vtestagrossa committed Mar 4, 2024
2 parents 211c68f + 2eef6b5 commit 8d0588c
Show file tree
Hide file tree
Showing 63 changed files with 3,057 additions and 565 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,56 @@
[![Pylint](https://github.com/Bilgecrank/hindsite/actions/workflows/pylint.yml/badge.svg?branch=development)](https://github.com/Bilgecrank/hindsite/actions/workflows/pylint.yml)

A SCRUM retrospective tool to empower teams.

## Currently in Progress as a School Capstone
![UMGC Logo](https://www.umgc.edu/content/experience-fragments/umgc/language-masters/en/header/master/_jcr_content/root/header_copy/image.coreimg.svg/1705606255029/umgc-logo-preferred-rgb.svg)
Hello! This project is currently a capstone project for a CMSC 495 course at the University of Maryland Global Campus. We are not accepting pull-requests outside our collaborative group so far, but after the assignment is submitted and graded, we can accept open-source collaboration after the fact.

## To run the project locally, you'll need to complete a few steps:

1. First, you need to set up a MySQL Database:
-https://dev.mysql.com/doc/mysql-getting-started/en/

2. Next, you need to run `pip install -r requirements.txt` in the root directory of the project.

-Note: this project was created with the latest version of Python 3. There are no version locks in the requirements.txt yet.

-Reccomendation: Use a virtual environment to install the requirements, rather than your operating system.

4. Finally, you need a .env file in the root directory of the project folder with the following environment variables:

`MYSQLUSER = ""

MYSQL_ROOT_PASSWORD = ""

MYSQL_HOST = ""

MYSQL_PORT = ""

MYSQL_DATABASE = ""

SECRET_KEY = ""`

5. Finally, to run the server, the following command is run from the root directory of the app in the terminal:

`gunicorn --timeout 600 --chdir app wsgi:app`

If you want the app to reload the server whenever you save, I recommend you use the flag --reload at the end, so:

`gunicorn --timeout 600 --chdir app wsgi:app --reload`

## Some tips:

Information for your local environment will go inside the double quotes. For example, the default port for MySQL is 3306, so:

`MYSQL_PORT = "3306"`

**MYSQL_HOST** is the hostname, so locally it will be localhost, or 127.0.0.1

**MYSQL_DATABASE** is the name of the database that you'll have to create in your local MySQL server. The user is the user that has root permission to the database, which is why the root password is named as such.

The codebase is set up to be portable between local environments and the railway server without publishing any secrets to GitHub, but python will automatically load from the .env file by first loading dotenv.

**SECRET_KEY** is optional. If you look in config.py you'll see that it's randomly generated, but you can comment out the one that uses secrets.token_hex() to generate a random key every reboot and uncomment the line that uses the environment variable if you want sessions to persist through reloads.

For the database, an empty MySQL database with the name that's in the environment variable will be populated when you run the command. You just have to make sure that you populate the .env files with the connection information to your MySQL server.
4 changes: 3 additions & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

class Config: # pylint: disable=too-few-public-methods
"""
Base configuration class. Contains default config settings
Base configuration class. Contains default config settings. Use
os.getenv to have persistent sessions between restarts. Use
secrets.token_hex(32) if you want to reset the session every time.
"""
FLASK_ENV = 'development'
DEBUG = False
Expand Down
23 changes: 19 additions & 4 deletions app/hindsite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,33 @@ def create_app():
app.register_blueprint(auth)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.core.core import core
# from app.hindsite.history.history import history
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(core)
# app.register_blueprint(history)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.home.home import home
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(home)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.group.group import group
from app.hindsite.group.group import grp
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(group)
app.register_blueprint(grp)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.settings.settings import settings
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(settings)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.retrospective.retrospective import retrospective
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(retrospective)

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.common import common
# pylint: enable=wrong-import-position,import-outside-toplevel
app.register_blueprint(common)

return app
22 changes: 12 additions & 10 deletions app/hindsite/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
from flask import Blueprint, flash, redirect, render_template, url_for, request
from flask_login import login_required
from app.hindsite.auth.authenticate_model import LoginError, \
login, register_user, logout, RegistrationError
login, register_user, logout, RegistrationError

static_dir = os.path.abspath('static')
auth = Blueprint('auth',
__name__,
template_folder='templates', # relative route to templates dir
static_folder=static_dir)
__name__,
template_folder='templates', # relative route to templates dir
static_folder=static_dir)


@auth.route('/sign-in', methods = ['POST', 'GET'])
@auth.route('/sign-in', methods=['POST', 'GET'])
def sign_in():
"""
Loads sign-in.html, sets the title
Expand All @@ -41,18 +41,20 @@ def sign_up():
error = ''
if request.method == 'POST': # Triggers if a user hits submit on a registration form.
try:
register_user(
request.form['email'],
request.form['confirmEmail'],
request.form['password'],
request.form['confirmPassword'])
# Check if fields match.
if request.form['email'].lower() != request.form['confirmEmail'].lower():
raise RegistrationError('Emails do not match.')
if request.form['password'] != request.form['confirmPassword']:
raise RegistrationError('Passwords do not match.')
register_user(request.form['email'], request.form['password'])
return redirect(url_for('home.homepage'))
except RegistrationError as e:
error = e.message
flash(error)
title = 'Sign up!'
return render_template('sign-up.html', title=title)


@auth.route('/sign-out')
@login_required
def sign_out():
Expand Down
55 changes: 47 additions & 8 deletions app/hindsite/auth/authenticate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import re # For serverside validation of secrets.

import bcrypt
from flask import session
from flask import flash, make_response, render_template, session, url_for
import flask_login
from app.hindsite.extensions import login_manager, db
from app.hindsite.tables import User, Password
Expand Down Expand Up @@ -80,7 +80,20 @@ def request_loader(request):
return UserSession(email)


def register_user(email: str, email_compare: str, password: str, password_compare: str):
@login_manager.unauthorized_handler
def unauthorized():
"""
Defines the unauthorized handler for the login_manager.
:return: **Response**
"""
response = make_response(render_template('401.html'), 401)
response.headers['hx-redirect'] = url_for('auth.sign_in')
flash("You must log-in to continue", "error")
return response


def register_user(email: str, password: str):
"""
Takes in a user's email and password, checks if the email is already associated with an account,
then checks if the password is a valid entry.
Expand All @@ -92,10 +105,6 @@ def register_user(email: str, email_compare: str, password: str, password_compar
:raises RegistrationError: Raises this in case of an already extant account or if the password
is not a valid secret.
"""
if email != email_compare:
raise RegistrationError('ERROR: Email and confirm email do not match.')
if password != password_compare:
raise RegistrationError('ERROR: Password and confirm password do not match.')
if is_user(email):
raise RegistrationError('An account already exists with this email.')
if not valid_email(email):
Expand Down Expand Up @@ -139,6 +148,25 @@ def valid_secret(secret: str):
r"_`{|}~]).{12,}", secret)


def valid_display_name(display_name: str):
"""
Validates a display name based on length and character set.
Args:
display_name (str): Display name to validate.
Returns:
bool: True if the display name is valid, False otherwise.
"""
if not 2 <= len(display_name) <= 30:
return False
if not re.match(r'^[a-zA-Z0-9_\-]+$', display_name):
return False
if display_name != display_name.strip():
return False
return True


def login(email: str, password: str):
"""
Logs a user into the system, initializing a session.
Expand All @@ -149,8 +177,7 @@ def login(email: str, password: str):
"""
if not is_user(email):
raise LoginError('This email is not attached to an account.')
stored_password = get_user(email).password.password.encode('utf-8')
if bcrypt.checkpw(password.encode('utf-8'), stored_password):
if is_users_password(email, password):
flask_login.login_user(UserSession(email))
session['groupname'] = None
session['groupid'] = None
Expand All @@ -175,3 +202,15 @@ def is_user(email: str):
:returns: **bool** Whether the user record is present in the database.
"""
return get_user(email) is not None


def is_users_password(email: str, password):
"""
Checks if a provided password matches the stored password attached to the email's account.
:param email: The email of the user.
:param password: The plain-text password provided.
:return:
"""
stored_password = get_user(email).password.password.encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored_password)
87 changes: 87 additions & 0 deletions app/hindsite/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Template route testing for base page.
"""

import os
from datetime import datetime

from flask import Blueprint, render_template, session
from flask_login import login_required, current_user

from app.hindsite.common_model import get_boards
from app.hindsite.group.group_model import get_invitations

static_dir = os.path.abspath('static')
common = Blueprint('common',
__name__,
template_folder='templates', # relative route to templates dir
static_folder=static_dir)


@common.route('/bubble')
@login_required
def bubble():
"""
Updates the notification badge on the menu button.
:return:
"""

count = get_num_of_invites()
# Only return one for active retrospective.
if get_retro_active() > 0:
count += 1
return render_template("partials/bubble.html", count=count)


@common.route('/retro_active')
@login_required
def retro_active():
"""
Returns whether a retrospective is active for the current group.
:return:
"""
count = get_retro_active()
return render_template("partials/retro_active.html", count=count)


@common.route('/invite_count')
@login_required
def invite_count():
"""
Returns a number current invites.
:return:
"""
count = get_num_of_invites()
return render_template("partials/invite_count.html", count=count)


def get_retro_active():
"""
Checks for any active retrospectives.
:return: **int**
"""
retro_count = 0
if 'groupid' not in session \
or session['groupid'] is None \
or session['groupid'] == 'Select a Group':
# User has not selected a group..
return 0
boards = get_boards(session['groupid'])
for board in boards:
if datetime.now() > board.start_time:
if board.end_time is not None:
if datetime.now() < board.end_time:
retro_count += 1
else:
retro_count += 1
return retro_count


def get_num_of_invites():
"""
Returns the number of active invites attached to the cou
:return:
"""
return len(get_invitations(current_user.id))
Loading

0 comments on commit 8d0588c

Please sign in to comment.