1 Commits

Author SHA1 Message Date
c2aa010812 Merge pull request '1.0.1 release' (#15) from testing into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #15
2023-03-21 18:10:14 +00:00
18 changed files with 45 additions and 255 deletions

View File

@@ -1,4 +1,4 @@
FROM python:3.11.2-alpine3.17
FROM python:3.11.0-alpine3.16
COPY ./app /app
WORKDIR /app
RUN apk add gcc musl-dev mariadb-connector-c-dev

View File

@@ -1 +1 @@
1.1.1
1.0.1

View File

@@ -1,19 +1,12 @@
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, convDay
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 log import appLogger
from worker import runCreateEvents
# init logger
logger = appLogger('app')
from create_events import createEvents
basedir = os.path.abspath(os.path.dirname(__file__))
@@ -32,6 +25,10 @@ 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()
@@ -73,38 +70,14 @@ 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('/', methods=['GET', 'POST'])
@app.route('/')
def index():
# 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')
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
@@ -114,16 +87,13 @@ 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():
@@ -135,24 +105,20 @@ 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. <br> Set env var SIGNUP_ENABLED=YES to enable account creation'
return 'Account creation is currently disabled'
@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():
@@ -161,7 +127,6 @@ 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
@@ -174,7 +139,6 @@ 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,
@@ -182,9 +146,8 @@ 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
runCreateEvents()
createEvents(db, currDay, Period, Event)
return redirect(f'/period/edit/{period.period}')
return render_template('newPeriod.html', form=form)
@@ -192,25 +155,21 @@ 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')
@@ -221,12 +180,11 @@ 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, re=re)
return render_template('events.html', events=events, periods=periods, datetime=datetime, date=date, ZoneInfo=ZoneInfo)
@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():
@@ -235,7 +193,6 @@ 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)
@@ -244,56 +201,42 @@ 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, re=re)
return render_template('tasks.html', str=str, tasks=tasks, datetime=datetime, date=date)
@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, re=re)
return render_template('task.html', str=str, task=task, datetime=datetime, date=date)
@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():
if form.expiryDate.data is not None:
expiryDate=convDay(form.expiryDate.data)
else:
expiryDate = None
task = Task(title=form.title.data,
description=form.description.data,
created_timestamp=int(time.time()),
expiry_date=expiryDate)
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
if form.expiryDate.data is not None:
expiryDate=convDay(form.expiryDate.data)
task.expiry_date=expiryDate
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')

View File

@@ -1,16 +0,0 @@
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()

View File

@@ -1,6 +1,3 @@
from log import appLogger
logger = appLogger('app')
def createEvents(db, currDay, Period, Event):
periods = Period.query.all()
for period in periods:
@@ -13,4 +10,4 @@ def createEvents(db, currDay, Period, Event):
db.session.add(event)
db.session.commit()
logger.info('createEvents script ran successfully')
print("createEvents script ran successfully")

View File

@@ -20,7 +20,6 @@ class Task(db.Model):
is_completed = db.Column(db.Boolean)
created_timestamp = db.Column(db.Integer)
due_timestamp = db.Column(db.Integer)
expiry_date = db.Column(db.String(100))
def __repr__(self):
return f'<Task "{self.title}">'

View File

@@ -3,7 +3,7 @@ from db import Task
from flask_wtf import FlaskForm
from wtforms import (StringField, DateField, TimeField, TextAreaField, IntegerField, SelectField, BooleanField,
RadioField, EmailField, PasswordField)
from wtforms.validators import InputRequired, Length, Optional
from wtforms.validators import InputRequired, Length
from wtforms_sqlalchemy.orm import QuerySelectField
def get_tasks():
@@ -12,7 +12,6 @@ class TaskForm(FlaskForm):
title = StringField('Title', validators=[InputRequired(),
Length(min=5, max=100)])
description = TextAreaField('Description', validators=[Length(max=200)])
expiryDate = DateField('Expiry Date', format='%Y-%m-%d', validators=[Optional()])
class EventForm(FlaskForm):
# eventDate = DateField('Date', validators=[InputRequired()], format='m-%d-%Y')
# period_num = IntegerField(validators=[InputRequired()])

View File

@@ -1,14 +0,0 @@
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

View File

@@ -1,32 +0,0 @@
"""add expiry date
Revision ID: 625eb20835b5
Revises:
Create Date: 2023-04-18 17:11:23.703222
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '625eb20835b5'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.add_column(sa.Column('expiry_date', sa.String(length=100), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('task', schema=None) as batch_op:
batch_op.drop_column('expiry_date')
# ### end Alembic commands ###

View File

@@ -10,11 +10,6 @@ currDay = currDay.strftime('%m-%d-%Y')
prevDay = datetime.now() + timedelta(days=-1)
prevDay = prevDay.strftime('%m-%d-%Y')
# Convert date to str format
def convDay(inDate):
outDate = inDate.strftime('%m-%d-%Y')
return outDate
with open('__version__','r') as file:
currVersion = file.read()

View File

@@ -6,35 +6,31 @@ gunicorn==20.1.0
# Flask Framework
click==8.1.3
Flask==2.2.3
Flask==2.2.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
Werkzeug==2.2.3
MarkupSafe==2.1.1
Werkzeug==2.2.2
# Flask Packages
Flask-Login==0.6.2
Flask-Migrate==4.0.4
Flask-Migrate==3.1.0
Flask-Script==2.0.6
Flask-SQLAlchemy==3.0.3
Flask-WTF==1.1.1
Flask-SQLAlchemy==3.0.2
Flask-WTF==1.0.1
Flask-User==1.0.2.2
# WTForms Extensions
WTForms-SQLAlchemy==0.3.0
# APScheduler automated scheduler
APScheduler==3.10.1
# Celery Task Queue
celery[redis]==5.2.7
redis==4.5.4
APScheduler==3.9.1.post1
# Python Time packages
pytz==2022.7.1
pytz==2022.6
# Automated tests
pytest==7.2.2
pytest==7.2.0
pytest-cov==4.0.0
# MySQL Package

View File

@@ -13,10 +13,6 @@
{{ form.description.label }}
</p>
{{ form.description(rows=5, cols=25) }}
<p>
{{ form.expiryDate.label }}
{{ form.expiryDate }}
</p>
<p>
<button class="btn btn-primary" type="submit">Edit Task</button>
</p>

View File

@@ -21,17 +21,7 @@
</b>
<div>
{% if event.tasks.description != None %}
{% 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 %}
<p>{{ event.tasks.description }}</p>
{% endif %}
</div>
{% else %}

View File

@@ -13,10 +13,6 @@
{{ form.description.label }}
</p>
{{ form.description(rows=5, cols=25) }}
<p>
{{ form.expiryDate.label }}
{{ form.expiryDate }}
</p>
<p>
<button class="btn btn-primary" type="submit">Add Task</button>
</p>

View File

@@ -1,7 +1,6 @@
{% 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>
@@ -10,17 +9,7 @@
<div>
<br>
{% if task.description != None %}
{% 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 %}
<p>{{ task.description }}</p>
{% endif %}
</div>
<div>
@@ -29,11 +18,6 @@
<div>
<p>Due: {{ task.due_timestamp }}</p>
</div>
{% if task.expiry_date != None %}
<div>
<p>Expires: {{ task.expiry_date }}</p>
</div>
{% endif %}
<p>
<a class="btn btn-primary" href="/task/{{task.id}}/edit">Edit Task</a>
<span>

View File

@@ -6,31 +6,17 @@
{% 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 %}
{% 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 %}
{{ task.description }}
{% endif %}
</b>
<div>
<p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }}</p>
{% if task.expiry_date != None %}
<p>Expires: {{ task.expiry_date }}</p>
{% endif %}
</div>
<hr>
</div>

View File

@@ -1,11 +1,9 @@
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__))
@@ -20,33 +18,18 @@ app.config['SQLALCHEMY_DATABASE_URI'] =\
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
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():
# Define function to run create_events with app context
def run_create_events():
with app.app_context():
createEvents(db, currDay, Period, Event)
def runCleanupEvents():
with app.app_context():
cleanupEvents(db, Event)
# Call createEvents on initial launch of the script
run_create_events()
# 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")
}
}
# 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()

View File

@@ -9,43 +9,31 @@ 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: celery
command: "-A worker.celerymsg worker --loglevel=DEBUG -B"
entrypoint: python3
command: "-m worker"
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