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 Feb 20, 2024
2 parents 334ea46 + f563fde commit bf8a72a
Show file tree
Hide file tree
Showing 19 changed files with 516 additions and 143 deletions.
8 changes: 6 additions & 2 deletions app/hindsite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

def create_app():
"""
Application factory to create app.
Application factory to create app. Initializes the configuration which loads the
environment variables, then initializes bootstrap, the login_manager, and the
database.
"""

app = Flask(__name__)
Expand All @@ -19,7 +21,9 @@ def create_app():
login_manager.init_app(app)
db.init_app(app)


# Blueprints are registered and imported as shown below. The registration order doesn't
# matter, so long as the pylint statements are around the import and the blueprint is
# registered directly below it.

# pylint: disable=wrong-import-position,import-outside-toplevel
from app.hindsite.auth.auth import auth
Expand Down
13 changes: 12 additions & 1 deletion app/hindsite/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"""
import os
from flask import Blueprint, flash, redirect, render_template, url_for, request
from app.hindsite.auth.authenticate_model import LoginError, login, register_user, RegistrationError
from flask_login import login_required
from app.hindsite.auth.authenticate_model import LoginError, \
login, register_user, logout, RegistrationError

static_dir = os.path.abspath('static')
auth = Blueprint('auth',
Expand Down Expand Up @@ -47,3 +49,12 @@ def sign_up():
flash(error)
title = 'Sign up!'
return render_template('sign-up.html', title=title)

@auth.route('/sign-out')
@login_required
def sign_out():
"""
Allows the user to sign-out
"""
logout()
return redirect(url_for('auth.sign_in'))
6 changes: 4 additions & 2 deletions app/hindsite/auth/authenticate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re # For serverside validation of secrets.

import bcrypt
from flask import session
import flask_login
from sqlalchemy import select

Expand All @@ -22,7 +23,6 @@ class UserSession(flask_login.UserMixin):
def __init__(self, user_id):
self.id = user_id


class RegistrationError(Exception):
"""
Definition for errors raised by the register_user function
Expand Down Expand Up @@ -54,7 +54,6 @@ class QueryError(Exception):
def __init__(self, message):
self.message = message


@login_manager.user_loader
def user_loader(email: str):
"""
Expand Down Expand Up @@ -135,6 +134,9 @@ def login(email: str, password: str):
user_id = get_user(email).id
if bcrypt.checkpw(password.encode('utf-8'), get_hashword(user_id)):
flask_login.login_user(UserSession(email))
session['groupname'] = None
session['groupid'] = None
session['facilitator'] = False
return True
return False

Expand Down
30 changes: 29 additions & 1 deletion app/hindsite/common_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from sqlalchemy import select

from app.hindsite.extensions import db
from app.hindsite.tables import User
from app.hindsite.tables import User, Group


def get_user(email: str):
Expand All @@ -20,3 +20,31 @@ def get_user(email: str):
if user is not None:
return db.session.execute(stmt).first()[0]
return None


def get_groups(email: str):
"""
Gets all group records belonging to a user.
:param email: **str** Email to check against the database
:returns: **list** A list of the user's current groups.
"""
user = get_user(email)
groups = []
for membership in user.groups:
if membership.invitation_accepted is True:
groups.append(membership.group)
return groups

def get_group(group_id: int):
"""
Gets a group record belonging to a user.
:param group_id: **int** id to check against the database
:returns: **group** or **None**
"""
stmt = select(Group).where(Group.id == group_id)
groups = db.session.execute(stmt).first()
if groups is not None:
return db.session.execute(stmt).first()[0]
return None
41 changes: 40 additions & 1 deletion app/hindsite/group/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
Template route testing for development
"""
import os
from flask import Blueprint, render_template
from flask import Blueprint, flash, redirect, render_template, request, session, url_for
from flask_login import login_required
from app.hindsite.group.group_model import send_invitation

from app.hindsite.group.group_model import UserSearchError, get_users

static_dir = os.path.abspath('static')
group = Blueprint('group',
Expand All @@ -18,3 +21,39 @@ def group_page():
Loads group.html, sets the title
"""
return render_template('group.html', title='Group')

@group.route('/search-users', methods=['GET', 'POST'])
@login_required
def search_users():
"""
Loads the user search results
"""

error = None
if request.method == 'POST':
try:
# get users
users = get_users(request.form['search'])
except UserSearchError as e:
error = e.message
if error is not None:
flash(error)
return redirect(url_for('group_page'))
if request.form['search'] == "":
users = ""
return render_template('partials/search-results.html', users=users)

@group.route('/send-invite', methods=['GET', 'POST'])
@login_required
def send_invite():
"""
POST route to send invite codes to other users.
"""
if request.method == 'POST':
try:
user = request.args['user']
send_invitation(session['groupid'], user)
except UserSearchError as ex:
flash(ex.message)
return "Invitation Sent!"
return render_template('partials/search-results.html')
49 changes: 49 additions & 0 deletions app/hindsite/group/group_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Allows the user to search for other users and send invites, while displaying user cards
for all users who belong to the current group.
"""
from app.hindsite.extensions import db
from app.hindsite.common_model import get_user, get_group
from app.hindsite.tables import User, Membership

class UserSearchError(Exception):
"""
Definition for errors raised by the login function
"""

message = None

def __init__(self, message):
self.message = message

def get_users(term: str):
"""
Gets a single user record.
:param email: **str** Email to check against the database
:returns: **User** or **None**
"""
users = None
if term is not None:
users = db.session.query(User) \
.filter(User.display_name.icontains(term) \
| User.email.icontains(term) \
| User.first_name.icontains(term) \
| User.last_name.icontains(term))
return users

def send_invitation(group_id: int, email: str):
"""
Creates a Membership that signals an invitation to a user, by default the membership
is not an ownership membership and will have <code>invitation_accepted</code> set to False
:param group_id: The id of the group attached to the membership.
:param email: The email of the user to be added to the membership.
:returns: **Membership** A reference to the membership object.
"""
user = get_user(email)
group = get_group(group_id)
membership = Membership(user, group)
db.session.add(membership)
db.session.commit()
return membership
30 changes: 18 additions & 12 deletions app/hindsite/group/templates/group.html
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
{% extends 'base.html' %}

{% block content %}

<div class="card m-5">
<div class="card-body">
<h5 class="card-title">Hello Authenticated User</h5>
<p class="card-text">
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Fuga reprehenderit molestiae, vel corporis tempore ipsa mollitia!
Consectetur officiis magni ullam dolores corrupti porro eaque esse.
Illum repudiandae quidem commodi accusamus!
</p>
<a href="{{ url_for('group.group_page')}}" class="btn btn-primary">HOME</a>
</div>
<div class="container-fluid w-100 p-5">
<h5 class="h-5">Search for users to invite</h5>
<input class="form-control-lg" type="search"
name="search" placeholder="Begin Typing to Search..."
hx-post="/search-users"
hx-trigger="input changed delay:300ms, search"
hx-target="#search-results"
hx-indicator=".htmx-indicator">
</input>
<div class="htmx-indicator text-dark">
<img alt="Loading..."
src="{{url_for('static', filename='img/three-dots.svg')}}">
Searching...
</div>
</div>
<div id="search-results" class="container-sm">
</div>
<div class="container-sm" id="invite-code">

</div>
{% endblock %}
42 changes: 42 additions & 0 deletions app/hindsite/group/templates/partials/dropdown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<div class="container-fluid m-0 dropdown-center" id="content">
<button class="btn btn-secondary container-fluid m-0 dropdown-toggle"
type="button"
id="groupDropDown"
data-bs-toggle="dropdown"
aria-expanded="false">
{{ selected }}
</button>
<ul id="group-dropdown" class="dropdown-menu dropdown-menu-dark container-sm text-center" aria-labelledby="groupDropDown">
{% for group in groups %}
<li>
<form>
<a class="dropdown-item"
href="#"
hx-post="/home?groupname={{ group.name }}"
hx-target="#content">
{{ group.name }}
</a>
</form>
</li>
{% endfor %}

<li>
<span class="container-fluid d-flex align-items-center">
<a class="dropdown-item"
href="#"
hx-get="/modal"
hx-target="#modal"
hx-trigger="click"
data-bs-toggle="modal"
data-bs-target="#modal">
<img
class="img-fluid"
height="50"
width="50"
src="{{ url_for('static', filename='img/plus.svg') }}">
</img>
</a>
</span>
</li>
</ul>
</div>
27 changes: 27 additions & 0 deletions app/hindsite/group/templates/partials/search-results.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<div class="overflow-auto w-30 text-light">
{% if users != "" %}
{% for user in users %}
<div class="content-sm bg-secondary">
<div class="btn btn-secondary w-100 d-flex justify-content-between align-items-center">
<p class="px-5 py-1 text-align-center">
{{ user.email }}
</p>
<p class="px-5 py-1 text-align-center">
{{ user.display_name }}
</p>
<a href="#" class="px-5 py-1 btn btn-light"
hx-post="/send-invite?user={{ user.email }}"
hx-target="#invite-code">
Send Invite
<img
class="img-fluid"
height="25"
width="25"
src="{{ url_for('static', filename='img/plus.svg') }}">
</img>
</a>
</div>
</div>
{% endfor %}
{% endif %}
</div>
42 changes: 39 additions & 3 deletions app/hindsite/home/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
Template route testing for development
"""
import os
from flask import Blueprint, render_template, request, flash, redirect, url_for
from flask import Blueprint, render_template, request, flash, redirect, url_for, session
from flask_login import login_required, current_user
from app.hindsite.home.models import create_group, get_groups, GroupAddError
from app.hindsite.home.home_model import accept_invitation, create_group, \
GroupAddError, get_invitation, get_invitations
from app.hindsite.common_model import get_groups

static_dir = os.path.abspath('static')
home = Blueprint('home',
Expand All @@ -25,8 +27,42 @@ def homepage():
"""
Loads home.html, sets the title
"""
if session['groupname'] is not None:
selected = session['groupname']
else:
selected = "Select Group"
groups = get_groups(current_user.id)
return render_template('home.html', title='Home', groups=groups)
if request.method == 'POST':
try:
session['groupname'] = request.args['groupname']
session['groupid'] = request.args['group_id']
except GroupAddError as ex:
flash(ex.message)
if session['groupname'] is not None:
selected = session['groupname']
return render_template('partials/dropdown.html', title='Home', \
groups=groups, selected=selected)
return render_template('home.html', title='Home', groups=groups, selected=selected)

@home.route('/invites', methods=['POST', 'GET'])
@login_required
def invites():
"""
Loads all the invite codes to be accepted or rejected
"""
error = None
if request.method == 'GET':
invitations = get_invitations(current_user.id)
return render_template('partials/invites.html', invitations=invitations)
if request.method == 'POST':
try:
group = request.args['group']
membership = get_invitation(group, current_user.id)
accept_invitation(membership)
except GroupAddError as e:
error = e.message
flash(error)
return render_template('partials/accepted.html', group=group)

@home.route('/add-group', methods=['GET', 'POST'])
@login_required
Expand Down
Loading

0 comments on commit bf8a72a

Please sign in to comment.