Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DF-3071] Enhancement for Digital Forms Admin Page #370

Merged
merged 12 commits into from
Sep 9, 2024
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ services:
FLASK_BASIC_AUTH_PASS: "${FLASK_BASIC_AUTH_PASS}"
MINIO_SK: "${MINIO_SK}"
MINIO_AK: "${MINIO_AK}"
REACT_APP_BASE_URL: "${REACT_APP_BASE_URL}"
command: bash -c "cd /home/appuser/python/prohibition_web_svc && flask db upgrade && gunicorn --bind 0.0.0.0:5000 --pythonpath /home/appuser/python/prohibition_web_svc 'app:create_app()'"
networks:
- docker-network
Expand Down
13 changes: 13 additions & 0 deletions python/common/rsi_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ def send_email_to_admin(**args):
template.render(subject=subject, body=body, message=json.dumps(message)), 'admin'), args


def send_new_user_admin_notification(**args):
subject = args.get('subject')
config = args.get('config')
message = args.get('message')
body = args.get('body')
template = get_jinja2_env().get_template('admin_notice_new_user_approval_request.html')
return common_email_services.send_email(
[config.ADMIN_EMAIL_ADDRESS],
subject,
config,
template.render(subject=subject, body=body, message=message), 'admin'), args


def applicant_prohibition_served_more_than_7_days_ago(**args):
config = args.get('config')
prohibition_number = args.get('prohibition_number')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{% extends "email_alert_base.html" %}

{% block primary_text_1 %}
<p style="font-family: Arial, sans-serif; font-size: 16px; color: #333333; line-height: 1.5;">
{{ body }}
</p>
{% endblock %}

{% block primary_text_2 %}
<h3 style="font-family: Arial, sans-serif; font-size: 18px; color: #2c3e50; margin-top: 20px; margin-bottom: 10px;">User Application Details:</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<tr style="background-color: #f2f2f2;">
<th style="font-family: Arial, sans-serif; font-size: 14px; color: #2c3e50; text-align: left; padding: 10px; border: 1px solid #ddd;">Field</th>
<th style="font-family: Arial, sans-serif; font-size: 14px; color: #2c3e50; text-align: left; padding: 10px; border: 1px solid #ddd;">Value</th>
</tr>
<tr>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">First Name</td>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">{{ message.first_name }}</td>
</tr>
<tr>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">Last Name</td>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">{{ message.last_name }}</td>
</tr>
<tr>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">Badge Number</td>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">{{ message.badge_number }}</td>
</tr>
<tr>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">Agency</td>
<td style="font-family: Arial, sans-serif; font-size: 14px; color: #333333; padding: 10px; border: 1px solid #ddd;">{{ message.agency }}</td>
</tr>
</table>
{% endblock %}

{% block primary_text_3 %}
<p style="font-family: Arial, sans-serif; font-size: 16px; color: #333333; line-height: 1.5; margin-bottom: 20px;">
Please review this user application at your earliest convenience. To access the administrative interface for processing this request, please click the button below:
</p>
<a href="{{ message.admin_link }}" style="display: inline-block; padding: 10px 20px; background-color: #3498db; color: #ffffff; text-decoration: none; font-family: Arial, sans-serif; font-size: 16px; border-radius: 5px; margin-bottom: 20px;">Review Application</a>
<p style="font-family: Arial, sans-serif; font-size: 14px; color: #7f8c8d; line-height: 1.5;">
If you're unable to click the button, you can copy and paste the following URL into your web browser:
<br>
<span style="color: #3498db;">{{ message.admin_link }}</span>
</p>
{% endblock %}

{% block salutation_block %}{% endblock %}
{% block callout_block %}{% endblock %}
{% block timeline_image %}{% endblock %}
{% block sign_off %}{% endblock %}
{% block email_not_monitored %}{% endblock %}
28 changes: 26 additions & 2 deletions python/prohibition_web_svc/blueprints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from flask_cors import CORS
import logging.config
import python.prohibition_web_svc.middleware.user_middleware as user_middleware
import python.prohibition_web_svc.middleware.notification_middleware as notification_middleware


logging.config.dictConfig(Config.LOGGING)
Expand Down Expand Up @@ -55,7 +56,9 @@ def create():
{"try": user_middleware.create_user_role, "fail": [
{"try": http_responses.server_error_response, "fail": []},
]},
# TODO - email admin with notice that user has applied
{"try": notification_middleware.send_new_user_admin_notification, "fail": [
{"try": logging.warning, "args": ["Failed to send admin notification email"], "fail": []}
]},
]},
{"try": http_responses.role_already_exists, "fail": []},
]},
Expand All @@ -68,7 +71,9 @@ def create():
{"try": user_middleware.create_user_role, "fail": [
{"try": http_responses.server_error_response, "fail": []},
]},
# TODO - email admin with notice that user has applied
{"try": notification_middleware.send_new_user_admin_notification, "fail": [
{"try": logging.warning, "args": ["Failed to send admin notification email"], "fail": []}
]},
]},
{"try": http_responses.role_already_exists, "fail": []},
],
Expand Down Expand Up @@ -105,3 +110,22 @@ def delete(user_guid):
if request.method == 'DELETE':
return make_response({"error": "method not implemented"}, 405)


@bp.route('/users/<string:user_guid>/update-last-active', methods=['POST'])
def update_last_active(user_guid):
if request.method == 'POST':
kwargs = middle_logic(
keycloak_logic.get_keycloak_user() + [
{"try": user_middleware.validate_update_last_active_request, "fail": [
{"try": http_responses.failed_validation, "fail": []}
]},
{"try": splunk_middleware.update_user_last_active_splunk, "fail": []},
{"try": splunk.log_to_splunk, "fail": []},
{"try": user_middleware.update_user_last_active, "fail": [
{"try": http_responses.server_error_response, "fail": []}
]},
],
request=request,
config=Config,
user_guid=user_guid)
return kwargs.get('response')
2 changes: 2 additions & 0 deletions python/prohibition_web_svc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ class Config(BaseConfig):

ENCRYPT_KEY = os.environ.get('ENCRYPT_KEY')
ENCRYPT_KEY_SALT = os.environ.get('ENCRYPT_KEY_SALT')

REACT_APP_BASE_URL = os.environ.get('REACT_APP_BASE_URL', 'http://localhost:3000/roadside-forms')
18 changes: 18 additions & 0 deletions python/prohibition_web_svc/middleware/notification_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from python.prohibition_web_svc.config import Config
import python.common.rsi_email as rsi_email


def send_new_user_admin_notification(**kwargs):
subject = "Digital Forms: New User Application"
body = "A new user has applied for access to Digital Forms:"
message = {
"first_name": kwargs.get('payload')['first_name'],
"last_name": kwargs.get('payload')['last_name'],
"badge_number": kwargs.get('payload')['badge_number'],
"agency": kwargs.get('payload')['agency'],
"admin_link": Config.REACT_APP_BASE_URL + "/admin-console",
}

rsi_email.send_new_user_admin_notification(config=Config, subject=subject, body=body, message=message)

return True, kwargs
3 changes: 2 additions & 1 deletion python/prohibition_web_svc/middleware/role_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ def query_all_users(**kwargs) -> tuple:
User.badge_number,
User.first_name,
User.last_name,
User.login)\
User.login,
User.last_active)\
.join(User) \
.limit(Config.MAX_RECORDS_RETURNED)\
.all()
Expand Down
12 changes: 11 additions & 1 deletion python/prohibition_web_svc/middleware/splunk_middleware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@


def log_static_get(**kwargs) -> tuple:
kwargs['splunk_data'] = {
"event": "get static resource",
Expand Down Expand Up @@ -181,3 +180,14 @@ def basic_authentication_failed(**kwargs) -> tuple:
}
return True, kwargs


def update_user_last_active_splunk(**kwargs):
user_guid = kwargs.get('user_guid', '')
username = kwargs.get('username', '')

kwargs['splunk_data'] = {
"event": "update user last active",
"user_guid": user_guid,
"username": username,
}
return True, kwargs
32 changes: 30 additions & 2 deletions python/prohibition_web_svc/middleware/user_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from datetime import datetime
import pytz
from python.prohibition_web_svc.models import db, User, UserRole
import python.common.rsi_email as rsi_email
from python.prohibition_web_svc.config import Config

class CustomErrorHandler(errors.BasicErrorHandler):
messages = errors.BasicErrorHandler.messages.copy()
Expand Down Expand Up @@ -111,7 +113,7 @@ def validate_create_user_payload(**kwargs) -> tuple:
schema = {
"badge_number": {
"type": "string",
"regex": "^([A-Z]{2}\d{2,4})|(\d{6})$",
"regex": r"^([A-Z]{2}\d{2,4})|(\d{6})$",
"required": True
},
"agency": {
Expand Down Expand Up @@ -141,6 +143,7 @@ def validate_create_user_payload(**kwargs) -> tuple:
kwargs['validation_errors'] = cerberus.errors
return False, kwargs


def get_user(**kwargs) -> tuple:
try:
user = db.session.query(User) \
Expand All @@ -151,4 +154,29 @@ def get_user(**kwargs) -> tuple:
except Exception as e:
logging.warning(str(e))
return False, kwargs
return True, kwargs
return True, kwargs


def validate_update_last_active_request(**kwargs):
user_guid = kwargs.get('user_guid')
if not user_guid:
kwargs['response'] = {"error": "user_guid is required"}, 400
return False, kwargs
return True, kwargs

def update_user_last_active(**kwargs):
try:
user_guid = kwargs.get('user_guid')
user = User.query.get(user_guid)
if user:
user.last_active = datetime.now()
db.session.commit()
kwargs['response'] = {"message": "Last active time updated successfully"}, 200
return True, kwargs
else:
kwargs['response'] = {"error": "User not found"}, 404
return False, kwargs
except Exception as e:
db.session.rollback()
kwargs['response'] = {"error": f"An error occurred: {str(e)}"}, 500
return False, kwargs
32 changes: 32 additions & 0 deletions python/prohibition_web_svc/migrations/versions/0fab578072b7_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""empty message

Revision ID: 0fab578072b7
Revises: 666113694229
Create Date: 2024-09-04 12:30:29.394957

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '0fab578072b7'
down_revision = '666113694229'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('last_active', sa.DateTime(), nullable=True))

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('last_active')

# ### end Alembic commands ###
6 changes: 5 additions & 1 deletion python/prohibition_web_svc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class User(db.Model):
first_name = db.Column(db.String(40), nullable=True)
display_name = db.Column(db.String(80), nullable=True)
login = db.Column(db.String(80), nullable=False)
last_active = db.Column(db.DateTime, nullable=True)

def __init__(self, username, user_guid, agency, badge_number, last_name, login, business_guid='', display_name='', first_name=''):
self.username = username
Expand All @@ -78,6 +79,7 @@ def __init__(self, username, user_guid, agency, badge_number, last_name, login,
self.business_guid = business_guid
self.display_name = display_name
self.login = login
self.last_active = datetime.now()

@staticmethod
def serialize(user):
Expand All @@ -89,7 +91,8 @@ def serialize(user):
"first_name": user.first_name,
"last_name": user.last_name,
"display_name": user.display_name,
"login": user.login
"login": user.login,
"last_active": user.last_active,
}


Expand Down Expand Up @@ -128,6 +131,7 @@ def serialize_all_users(rows):
"user_guid": rows.user_guid,
"username": rows.username,
"login": rows.login,
"last_active": rows.last_active,
}

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion python/prohibition_web_svc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Flask-Cors>=3.0.10
Flask-SQLAlchemy==3.1.1
Flask-Migrate
gunicorn==20.1.0
holidays==0.10.4
holidays==0.55
idna==2.10
iniconfig==1.1.1
iso8601==2.1.0
Expand Down
2 changes: 1 addition & 1 deletion roadside-forms-frontend/frontend_web_app/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
// Other Jest configuration options...

moduleFileExtensions: ["js", "jsx", "json", "node"],
transform: {
"^.+\\.js$": "babel-jest",
},
Expand Down
Loading
Loading