Merge pull request 'Version 1.1.0' (#32) from testing into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #32
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11.0-alpine3.16
|
||||
FROM python:3.11.2-alpine3.17
|
||||
COPY ./app /app
|
||||
WORKDIR /app
|
||||
RUN apk add gcc musl-dev mariadb-connector-c-dev
|
||||
|
@@ -1 +1 @@
|
||||
1.0.1
|
||||
1.1.0
|
73
app/app.py
73
app/app.py
@@ -1,12 +1,19 @@
|
||||
import os
|
||||
import re
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||
from flask_migrate import Migrate
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_login import (LoginManager, login_user, login_required, logout_user, current_user)
|
||||
from misc import datetime, date, time, currDay, prevDay, ZoneInfo, currVersion, currCommit
|
||||
from db import (db, Period, Task, Event, User)
|
||||
from sqlalchemy import inspect
|
||||
from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm)
|
||||
from create_events import createEvents
|
||||
from log import appLogger
|
||||
|
||||
from worker import runCreateEvents
|
||||
|
||||
# init logger
|
||||
logger = appLogger('app')
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
@@ -25,10 +32,6 @@ db.init_app(app)
|
||||
migrate = Migrate()
|
||||
migrate.init_app(app, db)
|
||||
|
||||
# Schedule creation of events every hour
|
||||
#with app.app_context():
|
||||
# scheduleCreateEvents(app, db, currDay, Period, Event, createEvents)
|
||||
|
||||
# Authentication stuff
|
||||
|
||||
login_manager = LoginManager()
|
||||
@@ -70,14 +73,38 @@ def forbidden(e):
|
||||
def ISEerror(e):
|
||||
return render_template('errors/500.html', NODE_NAME=NODE_NAME, POD_NAME=POD_NAME), 500
|
||||
|
||||
# Pod Status route
|
||||
@app.route('/podStatus')
|
||||
def podStatus():
|
||||
blankPage = ""
|
||||
try:
|
||||
db.engine.connect().close()
|
||||
return blankPage, 200
|
||||
except:
|
||||
return blankPage, 500
|
||||
|
||||
# Index route
|
||||
@app.route('/')
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
return redirect('/events')
|
||||
# Check for empty database, go to setup page if not setup
|
||||
global tablesSetup
|
||||
required_tables = ['period', 'task', 'event', 'user']
|
||||
inspector = inspect(db.engine)
|
||||
tables = inspector.get_table_names()
|
||||
tablesSetup = all(table in tables for table in required_tables)
|
||||
if not tablesSetup:
|
||||
# Initial setup page, should only appear if database is ready but not set up
|
||||
db.create_all()
|
||||
logger.info('DB initialized on first run')
|
||||
return redirect(url_for('createAccount'))
|
||||
# Otherwise, redirect to /events
|
||||
else:
|
||||
return redirect('/events')
|
||||
|
||||
# Authentication routes
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
userName = form.userName.data
|
||||
@@ -87,13 +114,16 @@ def login():
|
||||
user = User.query.filter_by(userName=userName).first()
|
||||
if not user or not check_password_hash(user.password, password):
|
||||
flash('Credentials incorrect! Please try again')
|
||||
logger.info(f'User \'{userName}\' logon FAILED (bad password) from {sourceIP}')
|
||||
return redirect(url_for('login'))
|
||||
login_user(user, remember=remember)
|
||||
logger.info(f'User \'{userName}\' logged in successfully from {sourceIP}')
|
||||
return redirect(url_for('events'))
|
||||
return render_template('login.html', form=form)
|
||||
|
||||
@app.route('/createaccount', methods=['GET', 'POST'])
|
||||
def createAccount():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
if signup_enabled == True:
|
||||
form = SignupForm()
|
||||
if form.validate_on_submit():
|
||||
@@ -105,20 +135,24 @@ def createAccount():
|
||||
password=generate_password_hash(form.password.data, method='sha256'))
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
logger.info(f'New user \'{new_user.userName}\' created from {sourceIP}')
|
||||
|
||||
return redirect(url_for('login'))
|
||||
return render_template('createAccount.html', form=form)
|
||||
else:
|
||||
return 'Account creation is currently disabled'
|
||||
return 'Account creation is currently disabled. <br> Set env var SIGNUP_ENABLED=YES to enable account creation'
|
||||
@app.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
logger.info(f'User \'{current_user.userName}\' logged out from {sourceIP} ')
|
||||
logout_user()
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/settings', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def settings():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
user = User.query.get_or_404(current_user.id)
|
||||
form = SettingsForm(obj=user)
|
||||
if form.validate_on_submit():
|
||||
@@ -127,6 +161,7 @@ def settings():
|
||||
if form.password.data != '':
|
||||
user.password = generate_password_hash(form.password.data, method='sha256')
|
||||
db.session.commit()
|
||||
logger.info(f'User \'{current_user.userName}\' settings changed from {sourceIP}')
|
||||
return render_template('settings.html', form=form)
|
||||
|
||||
# Periods routes
|
||||
@@ -139,6 +174,7 @@ def periods():
|
||||
@app.route('/period/new', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def newPeriod():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
form = PeriodForm()
|
||||
if form.validate_on_submit():
|
||||
period = Period(periodTime=form.periodTime.data,
|
||||
@@ -146,8 +182,9 @@ def newPeriod():
|
||||
)
|
||||
db.session.add(period)
|
||||
db.session.commit()
|
||||
logger.info(f'New period added by \'{current_user.userName}\' from {sourceIP}')
|
||||
# Run createEvents upon adding new period
|
||||
createEvents(db, currDay, Period, Event)
|
||||
runCreateEvents()
|
||||
return redirect(f'/period/edit/{period.period}')
|
||||
return render_template('newPeriod.html', form=form)
|
||||
|
||||
@@ -155,21 +192,25 @@ def newPeriod():
|
||||
@app.route('/period/edit/<int:periodNum>', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def editPeriod(periodNum):
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
period = Period.query.get_or_404(periodNum)
|
||||
form = PeriodForm(obj=period)
|
||||
if form.validate_on_submit():
|
||||
period.periodTime = form.periodTime.data
|
||||
period.weekendSchedule = form.weekendSchedule.data
|
||||
db.session.commit()
|
||||
logger.info(f'Period {periodNum} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect(f'/period/edit/{periodNum}')
|
||||
return render_template('editPeriod.html', period=period, form=form, datetime=datetime)
|
||||
|
||||
@app.post('/period/delete/<int:periodNum>')
|
||||
@login_required
|
||||
def delete_period(periodNum):
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
period = Period.query.get_or_404(periodNum)
|
||||
db.session.delete(period)
|
||||
db.session.commit()
|
||||
logger.info(f'Period {periodNum} deleted by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect('/periods')
|
||||
|
||||
|
||||
@@ -180,11 +221,12 @@ def events():
|
||||
events = Event.query.all()
|
||||
periods = Period.query.all()
|
||||
|
||||
return render_template('events.html', events=events, periods=periods, datetime=datetime, date=date, ZoneInfo=ZoneInfo)
|
||||
return render_template('events.html', events=events, periods=periods, datetime=datetime, date=date, ZoneInfo=ZoneInfo, re=re)
|
||||
|
||||
@app.route('/event/edit/<int:event_id>/', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def editEvent(event_id):
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
event = Event.query.get_or_404(event_id)
|
||||
form = EventForm(obj=event)
|
||||
if form.validate_on_submit():
|
||||
@@ -193,6 +235,7 @@ def editEvent(event_id):
|
||||
else:
|
||||
event.task_id = None
|
||||
db.session.commit()
|
||||
logger.info(f'Event {event_id} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect('/events')
|
||||
return render_template('editEvent.html', event=event, form=form, datetime=datetime)
|
||||
|
||||
@@ -201,17 +244,18 @@ def editEvent(event_id):
|
||||
@login_required
|
||||
def tasks():
|
||||
tasks = Task.query.all()
|
||||
return render_template('tasks.html', str=str, tasks=tasks, datetime=datetime, date=date)
|
||||
return render_template('tasks.html', str=str, tasks=tasks, datetime=datetime, date=date, re=re)
|
||||
|
||||
@app.route('/task/<int:task_id>/')
|
||||
@login_required
|
||||
def task(task_id):
|
||||
task = Task.query.get_or_404(task_id)
|
||||
return render_template('task.html', str=str, task=task, datetime=datetime, date=date)
|
||||
return render_template('task.html', str=str, task=task, datetime=datetime, date=date, re=re)
|
||||
|
||||
@app.route('/task/new', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def newTask():
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
form = TaskForm()
|
||||
if form.validate_on_submit():
|
||||
task = Task(title=form.title.data,
|
||||
@@ -219,24 +263,29 @@ def newTask():
|
||||
created_timestamp=int(time.time()))
|
||||
db.session.add(task)
|
||||
db.session.commit()
|
||||
logger.info(f'New task added by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect(f'/task/{task.id}')
|
||||
return render_template('newtask.html', form=form)
|
||||
|
||||
@app.route('/task/<int:task_id>/edit', methods=('GET', 'POST'))
|
||||
@login_required
|
||||
def editTask(task_id):
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
task = Task.query.get_or_404(task_id)
|
||||
form = TaskForm(obj=task)
|
||||
if form.validate_on_submit():
|
||||
task.title=form.title.data
|
||||
task.description=form.description.data
|
||||
db.session.commit()
|
||||
logger.info(f'Task {task_id} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect(f'/task/{task_id}')
|
||||
return render_template('edittask.html', task=task, form=form)
|
||||
@app.post('/task/<int:task_id>/delete')
|
||||
@login_required
|
||||
def delete_task(task_id):
|
||||
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
task = Task.query.get_or_404(task_id)
|
||||
db.session.delete(task)
|
||||
db.session.commit()
|
||||
logger.info(f'Task {task_id} deleted by \'{current_user.userName}\' from {sourceIP}')
|
||||
return redirect('/tasks')
|
16
app/cleanup_events.py
Normal file
16
app/cleanup_events.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def cleanupEvents(db, Event):
|
||||
# calculate the date one month ago
|
||||
one_month_ago = datetime.now() - timedelta(days=30)
|
||||
|
||||
# get events older than one month
|
||||
old_events = db.session.query(Event).filter(
|
||||
db.func.STR_TO_DATE(Event.scheduled_date, '%m-%d-%Y') < one_month_ago.date()
|
||||
).all()
|
||||
|
||||
# delete old events
|
||||
for event in old_events:
|
||||
db.session.delete(event)
|
||||
|
||||
db.session.commit()
|
@@ -1,3 +1,6 @@
|
||||
from log import appLogger
|
||||
|
||||
logger = appLogger('app')
|
||||
def createEvents(db, currDay, Period, Event):
|
||||
periods = Period.query.all()
|
||||
for period in periods:
|
||||
@@ -10,4 +13,4 @@ def createEvents(db, currDay, Period, Event):
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
|
||||
print("createEvents script ran successfully")
|
||||
logger.info('createEvents script ran successfully')
|
14
app/log.py
Normal file
14
app/log.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
def appLogger(name):
|
||||
formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
screen_handler = logging.StreamHandler(stream=sys.stdout)
|
||||
screen_handler.setFormatter(formatter)
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if (logger.hasHandlers()):
|
||||
logger.handlers.clear()
|
||||
logger.addHandler(screen_handler)
|
||||
return logger
|
@@ -6,31 +6,35 @@ gunicorn==20.1.0
|
||||
|
||||
# Flask Framework
|
||||
click==8.1.3
|
||||
Flask==2.2.2
|
||||
Flask==2.2.3
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
MarkupSafe==2.1.1
|
||||
Werkzeug==2.2.2
|
||||
MarkupSafe==2.1.2
|
||||
Werkzeug==2.2.3
|
||||
|
||||
# Flask Packages
|
||||
Flask-Login==0.6.2
|
||||
Flask-Migrate==3.1.0
|
||||
Flask-Migrate==4.0.4
|
||||
Flask-Script==2.0.6
|
||||
Flask-SQLAlchemy==3.0.2
|
||||
Flask-WTF==1.0.1
|
||||
Flask-SQLAlchemy==3.0.3
|
||||
Flask-WTF==1.1.1
|
||||
Flask-User==1.0.2.2
|
||||
|
||||
# WTForms Extensions
|
||||
WTForms-SQLAlchemy==0.3.0
|
||||
|
||||
# APScheduler automated scheduler
|
||||
APScheduler==3.9.1.post1
|
||||
APScheduler==3.10.1
|
||||
|
||||
# Celery Task Queue
|
||||
celery[redis]==5.2.7
|
||||
redis==4.5.4
|
||||
|
||||
# Python Time packages
|
||||
pytz==2022.6
|
||||
pytz==2022.7.1
|
||||
|
||||
# Automated tests
|
||||
pytest==7.2.0
|
||||
pytest==7.2.2
|
||||
pytest-cov==4.0.0
|
||||
|
||||
# MySQL Package
|
||||
|
@@ -21,7 +21,17 @@
|
||||
</b>
|
||||
<div>
|
||||
{% if event.tasks.description != None %}
|
||||
<p>{{ event.tasks.description }}</p>
|
||||
{% set description = event.tasks.description %}
|
||||
{% set url_regex = '(https?://[^\s]+)' %}
|
||||
|
||||
{% set match = re.search(url_regex, description) %}
|
||||
{% if match %}
|
||||
{% set url = match.group(0) %}
|
||||
{% set rest = description[match.end(0):] %}
|
||||
<p>{{ description[:match.start(0)] }}<a href="{{ url }}">{{ url }}</a>{{ rest }}</p>
|
||||
{% else %}
|
||||
<p>{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
||||
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
||||
{% set createdTime = createdTime.astimezone(ZoneInfo(current_user.timezone)) %}
|
||||
|
||||
{% block content %}
|
||||
<span><h1>{% block title %} {{ task.title }} {% endblock %}</h1></span>
|
||||
@@ -9,7 +10,17 @@
|
||||
<div>
|
||||
<br>
|
||||
{% if task.description != None %}
|
||||
<p>{{ task.description }}</p>
|
||||
{% set description = task.description %}
|
||||
{% set url_regex = '(https?://[^\s]+)' %}
|
||||
|
||||
{% set match = re.search(url_regex, description) %}
|
||||
{% if match %}
|
||||
{% set url = match.group(0) %}
|
||||
{% set rest = description[match.end(0):] %}
|
||||
<p>{{ description[:match.start(0)] }}<a href="{{ url }}">{{ url }}</a>{{ rest }}</p>
|
||||
{% else %}
|
||||
<p>{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
|
@@ -6,13 +6,24 @@
|
||||
{% for task in tasks %}
|
||||
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
||||
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
||||
{% set createdTime = createdTime.astimezone(ZoneInfo(current_user.timezone)) %}
|
||||
<div>
|
||||
<a href="/task/{{ task.id }}">
|
||||
<p><b>{{ task.title }}</b> </p>
|
||||
</a>
|
||||
<b>
|
||||
{% if task.description != None %}
|
||||
{{ task.description }}
|
||||
{% set description = task.description %}
|
||||
{% set url_regex = '(https?://[^\s]+)' %}
|
||||
|
||||
{% set match = re.search(url_regex, description) %}
|
||||
{% if match %}
|
||||
{% set url = match.group(0) %}
|
||||
{% set rest = description[match.end(0):] %}
|
||||
<p>{{ description[:match.start(0)] }}<a href="{{ url }}">{{ url }}</a>{{ rest }}</p>
|
||||
{% else %}
|
||||
<p>{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</b>
|
||||
<div>
|
||||
|
@@ -1,9 +1,11 @@
|
||||
from celery import Celery
|
||||
from celery.schedules import crontab
|
||||
from create_events import createEvents
|
||||
from cleanup_events import cleanupEvents
|
||||
import os
|
||||
from flask import Flask
|
||||
from misc import currDay, datetime, time, timedelta
|
||||
from db import db, Period, Event
|
||||
from create_events import createEvents
|
||||
from apscheduler.schedulers.background import BlockingScheduler
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
@@ -18,18 +20,33 @@ app.config['SQLALCHEMY_DATABASE_URI'] =\
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
db.init_app(app)
|
||||
|
||||
# Define function to run create_events with app context
|
||||
def run_create_events():
|
||||
redis_url = 'redis://' + \
|
||||
os.environ['REDIS_HOST'] + \
|
||||
':' + os.environ['REDIS_PORT'] + \
|
||||
'/' + os.environ['REDIS_DBNUM']
|
||||
|
||||
celerymsg = Celery('tasks', backend=redis_url, broker=redis_url)
|
||||
|
||||
# Task definitions
|
||||
@celerymsg.task
|
||||
def runCreateEvents():
|
||||
with app.app_context():
|
||||
createEvents(db, currDay, Period, Event)
|
||||
|
||||
# Call createEvents on initial launch of the script
|
||||
run_create_events()
|
||||
def runCleanupEvents():
|
||||
with app.app_context():
|
||||
cleanupEvents(db, Event)
|
||||
|
||||
|
||||
# Set up scheduler to run function at 59th minute of every hour
|
||||
scheduler = BlockingScheduler()
|
||||
scheduler.add_job(run_create_events, 'cron', minute=59)
|
||||
|
||||
# Start scheduler
|
||||
scheduler.start()
|
||||
# Scheduled tasks
|
||||
celerymsg.conf.beat_schedule = {
|
||||
'hourly-createevents': {
|
||||
'task': 'worker.runCreateEvents',
|
||||
# Run hourly
|
||||
'schedule': crontab(hour="*", minute="59"),
|
||||
},
|
||||
'monthly-cleanupevents': {
|
||||
'task': 'worker.runCleanupEvents',
|
||||
# Run monthly
|
||||
'schedule': crontab(day_of_month="29")
|
||||
}
|
||||
}
|
@@ -9,31 +9,43 @@ services:
|
||||
- MYSQL_HOST=db
|
||||
- MYSQL_PORT=3306
|
||||
- MYSQL_DB=bellscheduler
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_DBNUM=0
|
||||
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||
- NODE_NAME=local
|
||||
- POD_NAME=local
|
||||
- FLASK_ENV=development
|
||||
- FLASK_DEBUG=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
- SIGNUP_ENABLED=YES
|
||||
ports:
|
||||
- 127.0.0.1:80:80
|
||||
worker:
|
||||
image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing
|
||||
restart: always
|
||||
entrypoint: python3
|
||||
command: "-m worker"
|
||||
entrypoint: celery
|
||||
command: "-A worker.celerymsg worker --loglevel=DEBUG -B"
|
||||
environment:
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||
- MYSQL_HOST=db
|
||||
- MYSQL_PORT=3306
|
||||
- MYSQL_DB=bellscheduler
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_DBNUM=0
|
||||
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||
- NODE_NAME=local
|
||||
- POD_NAME=local
|
||||
- FLASK_ENV=development
|
||||
- FLASK_DEBUG=1
|
||||
- PYTHONUNBUFFERED=1
|
||||
redis:
|
||||
image: redis:7.0.10-alpine3.17
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:6379:6379
|
||||
db:
|
||||
image: mariadb:10.7.8-focal
|
||||
restart: always
|
||||
|
Reference in New Issue
Block a user