Compare commits
36 Commits
1.0.1
...
task-expir
Author | SHA1 | Date | |
---|---|---|---|
6329a647e4 | |||
86ed978098 | |||
b5c49bc46f | |||
dc6fb52b58 | |||
b783946273 | |||
dca5b35395 | |||
5d6695d608 | |||
1597a322bf | |||
256c4dddb1 | |||
c4e1a7ab75 | |||
723421160f | |||
8d51a75275 | |||
61f6ea3502 | |||
e46b1ef4b9 | |||
85753c5afe | |||
b07a9c70d2 | |||
99dc76b25a | |||
20af991841 | |||
961573ade1 | |||
3a3fd2da9a | |||
ed9f7864e8 | |||
55161f0330 | |||
10d0a472b0 | |||
f4106f2045 | |||
94bb4c7ac7 | |||
a58cc0ffcb | |||
437c093adf | |||
d0a6b98ddc | |||
f1e9c492de | |||
2490fb6698 | |||
fb7df832dc | |||
7372eb625c | |||
c21cb03546 | |||
b68e48b1cf | |||
d9391c73e9 | |||
c9b924a1e3 |
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.0-alpine3.16
|
FROM python:3.11.2-alpine3.17
|
||||||
COPY ./app /app
|
COPY ./app /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add gcc musl-dev mariadb-connector-c-dev
|
RUN apk add gcc musl-dev mariadb-connector-c-dev
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.0.1
|
1.1.1
|
||||||
|
85
app/app.py
85
app/app.py
@@ -1,12 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask_login import (LoginManager, login_user, login_required, logout_user, current_user)
|
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 misc import datetime, date, time, currDay, prevDay, ZoneInfo, currVersion, currCommit, convDay
|
||||||
from db import (db, Period, Task, Event, User)
|
from db import (db, Period, Task, Event, User)
|
||||||
|
from sqlalchemy import inspect
|
||||||
from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm)
|
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__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
@@ -25,10 +32,6 @@ db.init_app(app)
|
|||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
|
|
||||||
# Schedule creation of events every hour
|
|
||||||
#with app.app_context():
|
|
||||||
# scheduleCreateEvents(app, db, currDay, Period, Event, createEvents)
|
|
||||||
|
|
||||||
# Authentication stuff
|
# Authentication stuff
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
@@ -70,14 +73,38 @@ def forbidden(e):
|
|||||||
def ISEerror(e):
|
def ISEerror(e):
|
||||||
return render_template('errors/500.html', NODE_NAME=NODE_NAME, POD_NAME=POD_NAME), 500
|
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
|
# Index route
|
||||||
@app.route('/')
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
def index():
|
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
|
# Authentication routes
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
form = LoginForm()
|
form = LoginForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
userName = form.userName.data
|
userName = form.userName.data
|
||||||
@@ -87,13 +114,16 @@ def login():
|
|||||||
user = User.query.filter_by(userName=userName).first()
|
user = User.query.filter_by(userName=userName).first()
|
||||||
if not user or not check_password_hash(user.password, password):
|
if not user or not check_password_hash(user.password, password):
|
||||||
flash('Credentials incorrect! Please try again')
|
flash('Credentials incorrect! Please try again')
|
||||||
|
logger.info(f'User \'{userName}\' logon FAILED (bad password) from {sourceIP}')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
login_user(user, remember=remember)
|
login_user(user, remember=remember)
|
||||||
|
logger.info(f'User \'{userName}\' logged in successfully from {sourceIP}')
|
||||||
return redirect(url_for('events'))
|
return redirect(url_for('events'))
|
||||||
return render_template('login.html', form=form)
|
return render_template('login.html', form=form)
|
||||||
|
|
||||||
@app.route('/createaccount', methods=['GET', 'POST'])
|
@app.route('/createaccount', methods=['GET', 'POST'])
|
||||||
def createAccount():
|
def createAccount():
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
if signup_enabled == True:
|
if signup_enabled == True:
|
||||||
form = SignupForm()
|
form = SignupForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@@ -105,20 +135,24 @@ def createAccount():
|
|||||||
password=generate_password_hash(form.password.data, method='sha256'))
|
password=generate_password_hash(form.password.data, method='sha256'))
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'New user \'{new_user.userName}\' created from {sourceIP}')
|
||||||
|
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
return render_template('createAccount.html', form=form)
|
return render_template('createAccount.html', form=form)
|
||||||
else:
|
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')
|
@app.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
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()
|
logout_user()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
@app.route('/settings', methods=('GET', 'POST'))
|
@app.route('/settings', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def settings():
|
def settings():
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
user = User.query.get_or_404(current_user.id)
|
user = User.query.get_or_404(current_user.id)
|
||||||
form = SettingsForm(obj=user)
|
form = SettingsForm(obj=user)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@@ -127,6 +161,7 @@ def settings():
|
|||||||
if form.password.data != '':
|
if form.password.data != '':
|
||||||
user.password = generate_password_hash(form.password.data, method='sha256')
|
user.password = generate_password_hash(form.password.data, method='sha256')
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'User \'{current_user.userName}\' settings changed from {sourceIP}')
|
||||||
return render_template('settings.html', form=form)
|
return render_template('settings.html', form=form)
|
||||||
|
|
||||||
# Periods routes
|
# Periods routes
|
||||||
@@ -139,6 +174,7 @@ def periods():
|
|||||||
@app.route('/period/new', methods=('GET', 'POST'))
|
@app.route('/period/new', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def newPeriod():
|
def newPeriod():
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
form = PeriodForm()
|
form = PeriodForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
period = Period(periodTime=form.periodTime.data,
|
period = Period(periodTime=form.periodTime.data,
|
||||||
@@ -146,8 +182,9 @@ def newPeriod():
|
|||||||
)
|
)
|
||||||
db.session.add(period)
|
db.session.add(period)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'New period added by \'{current_user.userName}\' from {sourceIP}')
|
||||||
# Run createEvents upon adding new period
|
# Run createEvents upon adding new period
|
||||||
createEvents(db, currDay, Period, Event)
|
runCreateEvents()
|
||||||
return redirect(f'/period/edit/{period.period}')
|
return redirect(f'/period/edit/{period.period}')
|
||||||
return render_template('newPeriod.html', form=form)
|
return render_template('newPeriod.html', form=form)
|
||||||
|
|
||||||
@@ -155,21 +192,25 @@ def newPeriod():
|
|||||||
@app.route('/period/edit/<int:periodNum>', methods=('GET', 'POST'))
|
@app.route('/period/edit/<int:periodNum>', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def editPeriod(periodNum):
|
def editPeriod(periodNum):
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
period = Period.query.get_or_404(periodNum)
|
period = Period.query.get_or_404(periodNum)
|
||||||
form = PeriodForm(obj=period)
|
form = PeriodForm(obj=period)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
period.periodTime = form.periodTime.data
|
period.periodTime = form.periodTime.data
|
||||||
period.weekendSchedule = form.weekendSchedule.data
|
period.weekendSchedule = form.weekendSchedule.data
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'Period {periodNum} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect(f'/period/edit/{periodNum}')
|
return redirect(f'/period/edit/{periodNum}')
|
||||||
return render_template('editPeriod.html', period=period, form=form, datetime=datetime)
|
return render_template('editPeriod.html', period=period, form=form, datetime=datetime)
|
||||||
|
|
||||||
@app.post('/period/delete/<int:periodNum>')
|
@app.post('/period/delete/<int:periodNum>')
|
||||||
@login_required
|
@login_required
|
||||||
def delete_period(periodNum):
|
def delete_period(periodNum):
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
period = Period.query.get_or_404(periodNum)
|
period = Period.query.get_or_404(periodNum)
|
||||||
db.session.delete(period)
|
db.session.delete(period)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'Period {periodNum} deleted by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect('/periods')
|
return redirect('/periods')
|
||||||
|
|
||||||
|
|
||||||
@@ -180,11 +221,12 @@ def events():
|
|||||||
events = Event.query.all()
|
events = Event.query.all()
|
||||||
periods = Period.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'))
|
@app.route('/event/edit/<int:event_id>/', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def editEvent(event_id):
|
def editEvent(event_id):
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
event = Event.query.get_or_404(event_id)
|
event = Event.query.get_or_404(event_id)
|
||||||
form = EventForm(obj=event)
|
form = EventForm(obj=event)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
@@ -193,6 +235,7 @@ def editEvent(event_id):
|
|||||||
else:
|
else:
|
||||||
event.task_id = None
|
event.task_id = None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'Event {event_id} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect('/events')
|
return redirect('/events')
|
||||||
return render_template('editEvent.html', event=event, form=form, datetime=datetime)
|
return render_template('editEvent.html', event=event, form=form, datetime=datetime)
|
||||||
|
|
||||||
@@ -201,42 +244,56 @@ def editEvent(event_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def tasks():
|
def tasks():
|
||||||
tasks = Task.query.all()
|
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>/')
|
@app.route('/task/<int:task_id>/')
|
||||||
@login_required
|
@login_required
|
||||||
def task(task_id):
|
def task(task_id):
|
||||||
task = Task.query.get_or_404(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'))
|
@app.route('/task/new', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def newTask():
|
def newTask():
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
form = TaskForm()
|
form = TaskForm()
|
||||||
if form.validate_on_submit():
|
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,
|
task = Task(title=form.title.data,
|
||||||
description=form.description.data,
|
description=form.description.data,
|
||||||
created_timestamp=int(time.time()))
|
created_timestamp=int(time.time()),
|
||||||
|
expiry_date=expiryDate)
|
||||||
db.session.add(task)
|
db.session.add(task)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'New task added by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect(f'/task/{task.id}')
|
return redirect(f'/task/{task.id}')
|
||||||
return render_template('newtask.html', form=form)
|
return render_template('newtask.html', form=form)
|
||||||
|
|
||||||
@app.route('/task/<int:task_id>/edit', methods=('GET', 'POST'))
|
@app.route('/task/<int:task_id>/edit', methods=('GET', 'POST'))
|
||||||
@login_required
|
@login_required
|
||||||
def editTask(task_id):
|
def editTask(task_id):
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
task = Task.query.get_or_404(task_id)
|
task = Task.query.get_or_404(task_id)
|
||||||
form = TaskForm(obj=task)
|
form = TaskForm(obj=task)
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
task.title=form.title.data
|
task.title=form.title.data
|
||||||
task.description=form.description.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()
|
db.session.commit()
|
||||||
|
logger.info(f'Task {task_id} edited by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect(f'/task/{task_id}')
|
return redirect(f'/task/{task_id}')
|
||||||
return render_template('edittask.html', task=task, form=form)
|
return render_template('edittask.html', task=task, form=form)
|
||||||
@app.post('/task/<int:task_id>/delete')
|
@app.post('/task/<int:task_id>/delete')
|
||||||
@login_required
|
@login_required
|
||||||
def delete_task(task_id):
|
def delete_task(task_id):
|
||||||
|
sourceIP = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||||
task = Task.query.get_or_404(task_id)
|
task = Task.query.get_or_404(task_id)
|
||||||
db.session.delete(task)
|
db.session.delete(task)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
logger.info(f'Task {task_id} deleted by \'{current_user.userName}\' from {sourceIP}')
|
||||||
return redirect('/tasks')
|
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):
|
def createEvents(db, currDay, Period, Event):
|
||||||
periods = Period.query.all()
|
periods = Period.query.all()
|
||||||
for period in periods:
|
for period in periods:
|
||||||
@@ -10,4 +13,4 @@ def createEvents(db, currDay, Period, Event):
|
|||||||
db.session.add(event)
|
db.session.add(event)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
print("createEvents script ran successfully")
|
logger.info('createEvents script ran successfully')
|
@@ -20,6 +20,7 @@ class Task(db.Model):
|
|||||||
is_completed = db.Column(db.Boolean)
|
is_completed = db.Column(db.Boolean)
|
||||||
created_timestamp = db.Column(db.Integer)
|
created_timestamp = db.Column(db.Integer)
|
||||||
due_timestamp = db.Column(db.Integer)
|
due_timestamp = db.Column(db.Integer)
|
||||||
|
expiry_date = db.Column(db.String(100))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Task "{self.title}">'
|
return f'<Task "{self.title}">'
|
||||||
|
@@ -3,7 +3,7 @@ from db import Task
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (StringField, DateField, TimeField, TextAreaField, IntegerField, SelectField, BooleanField,
|
from wtforms import (StringField, DateField, TimeField, TextAreaField, IntegerField, SelectField, BooleanField,
|
||||||
RadioField, EmailField, PasswordField)
|
RadioField, EmailField, PasswordField)
|
||||||
from wtforms.validators import InputRequired, Length
|
from wtforms.validators import InputRequired, Length, Optional
|
||||||
from wtforms_sqlalchemy.orm import QuerySelectField
|
from wtforms_sqlalchemy.orm import QuerySelectField
|
||||||
|
|
||||||
def get_tasks():
|
def get_tasks():
|
||||||
@@ -12,6 +12,7 @@ class TaskForm(FlaskForm):
|
|||||||
title = StringField('Title', validators=[InputRequired(),
|
title = StringField('Title', validators=[InputRequired(),
|
||||||
Length(min=5, max=100)])
|
Length(min=5, max=100)])
|
||||||
description = TextAreaField('Description', validators=[Length(max=200)])
|
description = TextAreaField('Description', validators=[Length(max=200)])
|
||||||
|
expiryDate = DateField('Expiry Date', format='%Y-%m-%d', validators=[Optional()])
|
||||||
class EventForm(FlaskForm):
|
class EventForm(FlaskForm):
|
||||||
# eventDate = DateField('Date', validators=[InputRequired()], format='m-%d-%Y')
|
# eventDate = DateField('Date', validators=[InputRequired()], format='m-%d-%Y')
|
||||||
# period_num = IntegerField(validators=[InputRequired()])
|
# period_num = IntegerField(validators=[InputRequired()])
|
||||||
|
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
|
32
app/migrations/versions/625eb20835b5_add_expiry_date.py
Normal file
32
app/migrations/versions/625eb20835b5_add_expiry_date.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""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 ###
|
@@ -10,6 +10,11 @@ currDay = currDay.strftime('%m-%d-%Y')
|
|||||||
prevDay = datetime.now() + timedelta(days=-1)
|
prevDay = datetime.now() + timedelta(days=-1)
|
||||||
prevDay = prevDay.strftime('%m-%d-%Y')
|
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:
|
with open('__version__','r') as file:
|
||||||
currVersion = file.read()
|
currVersion = file.read()
|
||||||
|
@@ -6,31 +6,35 @@ gunicorn==20.1.0
|
|||||||
|
|
||||||
# Flask Framework
|
# Flask Framework
|
||||||
click==8.1.3
|
click==8.1.3
|
||||||
Flask==2.2.2
|
Flask==2.2.3
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
MarkupSafe==2.1.1
|
MarkupSafe==2.1.2
|
||||||
Werkzeug==2.2.2
|
Werkzeug==2.2.3
|
||||||
|
|
||||||
# Flask Packages
|
# Flask Packages
|
||||||
Flask-Login==0.6.2
|
Flask-Login==0.6.2
|
||||||
Flask-Migrate==3.1.0
|
Flask-Migrate==4.0.4
|
||||||
Flask-Script==2.0.6
|
Flask-Script==2.0.6
|
||||||
Flask-SQLAlchemy==3.0.2
|
Flask-SQLAlchemy==3.0.3
|
||||||
Flask-WTF==1.0.1
|
Flask-WTF==1.1.1
|
||||||
Flask-User==1.0.2.2
|
Flask-User==1.0.2.2
|
||||||
|
|
||||||
# WTForms Extensions
|
# WTForms Extensions
|
||||||
WTForms-SQLAlchemy==0.3.0
|
WTForms-SQLAlchemy==0.3.0
|
||||||
|
|
||||||
# APScheduler automated scheduler
|
# 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
|
# Python Time packages
|
||||||
pytz==2022.6
|
pytz==2022.7.1
|
||||||
|
|
||||||
# Automated tests
|
# Automated tests
|
||||||
pytest==7.2.0
|
pytest==7.2.2
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
|
|
||||||
# MySQL Package
|
# MySQL Package
|
||||||
|
@@ -13,6 +13,10 @@
|
|||||||
{{ form.description.label }}
|
{{ form.description.label }}
|
||||||
</p>
|
</p>
|
||||||
{{ form.description(rows=5, cols=25) }}
|
{{ form.description(rows=5, cols=25) }}
|
||||||
|
<p>
|
||||||
|
{{ form.expiryDate.label }}
|
||||||
|
{{ form.expiryDate }}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button class="btn btn-primary" type="submit">Edit Task</button>
|
<button class="btn btn-primary" type="submit">Edit Task</button>
|
||||||
</p>
|
</p>
|
||||||
|
@@ -21,7 +21,17 @@
|
|||||||
</b>
|
</b>
|
||||||
<div>
|
<div>
|
||||||
{% if event.tasks.description != None %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@@ -13,6 +13,10 @@
|
|||||||
{{ form.description.label }}
|
{{ form.description.label }}
|
||||||
</p>
|
</p>
|
||||||
{{ form.description(rows=5, cols=25) }}
|
{{ form.description(rows=5, cols=25) }}
|
||||||
|
<p>
|
||||||
|
{{ form.expiryDate.label }}
|
||||||
|
{{ form.expiryDate }}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<button class="btn btn-primary" type="submit">Add Task</button>
|
<button class="btn btn-primary" type="submit">Add Task</button>
|
||||||
</p>
|
</p>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
||||||
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
||||||
|
{% set createdTime = createdTime.astimezone(ZoneInfo(current_user.timezone)) %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<span><h1>{% block title %} {{ task.title }} {% endblock %}</h1></span>
|
<span><h1>{% block title %} {{ task.title }} {% endblock %}</h1></span>
|
||||||
@@ -9,7 +10,17 @@
|
|||||||
<div>
|
<div>
|
||||||
<br>
|
<br>
|
||||||
{% if task.description != None %}
|
{% 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 %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -18,6 +29,11 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>Due: {{ task.due_timestamp }}</p>
|
<p>Due: {{ task.due_timestamp }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if task.expiry_date != None %}
|
||||||
|
<div>
|
||||||
|
<p>Expires: {{ task.expiry_date }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-primary" href="/task/{{task.id}}/edit">Edit Task</a>
|
<a class="btn btn-primary" href="/task/{{task.id}}/edit">Edit Task</a>
|
||||||
<span>
|
<span>
|
||||||
|
@@ -6,17 +6,31 @@
|
|||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
{% set createdTime = datetime.fromtimestamp(task.created_timestamp) %}
|
||||||
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
{% set createdTime = datetime.strptime(str(createdTime), '%Y-%m-%d %H:%M:%S') %}
|
||||||
|
{% set createdTime = createdTime.astimezone(ZoneInfo(current_user.timezone)) %}
|
||||||
<div>
|
<div>
|
||||||
<a href="/task/{{ task.id }}">
|
<a href="/task/{{ task.id }}">
|
||||||
<p><b>{{ task.title }}</b> </p>
|
<p><b>{{ task.title }}</b> </p>
|
||||||
</a>
|
</a>
|
||||||
<b>
|
<b>
|
||||||
{% if task.description != None %}
|
{% 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 %}
|
{% endif %}
|
||||||
</b>
|
</b>
|
||||||
<div>
|
<div>
|
||||||
<p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }}</p>
|
<p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }}</p>
|
||||||
|
{% if task.expiry_date != None %}
|
||||||
|
<p>Expires: {{ task.expiry_date }}</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</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
|
import os
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from misc import currDay, datetime, time, timedelta
|
from misc import currDay, datetime, time, timedelta
|
||||||
from db import db, Period, Event
|
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__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
@@ -18,18 +20,33 @@ app.config['SQLALCHEMY_DATABASE_URI'] =\
|
|||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
||||||
# Define function to run create_events with app context
|
redis_url = 'redis://' + \
|
||||||
def run_create_events():
|
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():
|
with app.app_context():
|
||||||
createEvents(db, currDay, Period, Event)
|
createEvents(db, currDay, Period, Event)
|
||||||
|
|
||||||
# Call createEvents on initial launch of the script
|
def runCleanupEvents():
|
||||||
run_create_events()
|
with app.app_context():
|
||||||
|
cleanupEvents(db, Event)
|
||||||
|
|
||||||
|
# Scheduled tasks
|
||||||
# Set up scheduler to run function at 59th minute of every hour
|
celerymsg.conf.beat_schedule = {
|
||||||
scheduler = BlockingScheduler()
|
'hourly-createevents': {
|
||||||
scheduler.add_job(run_create_events, 'cron', minute=59)
|
'task': 'worker.runCreateEvents',
|
||||||
|
# Run hourly
|
||||||
# Start scheduler
|
'schedule': crontab(hour="*", minute="59"),
|
||||||
scheduler.start()
|
},
|
||||||
|
'monthly-cleanupevents': {
|
||||||
|
'task': 'worker.runCleanupEvents',
|
||||||
|
# Run monthly
|
||||||
|
'schedule': crontab(day_of_month="29")
|
||||||
|
}
|
||||||
|
}
|
@@ -9,31 +9,43 @@ services:
|
|||||||
- MYSQL_HOST=db
|
- MYSQL_HOST=db
|
||||||
- MYSQL_PORT=3306
|
- MYSQL_PORT=3306
|
||||||
- MYSQL_DB=bellscheduler
|
- MYSQL_DB=bellscheduler
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_DBNUM=0
|
||||||
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||||
- NODE_NAME=local
|
- NODE_NAME=local
|
||||||
- POD_NAME=local
|
- POD_NAME=local
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- FLASK_DEBUG=1
|
- FLASK_DEBUG=1
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
|
- SIGNUP_ENABLED=YES
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:80:80
|
- 127.0.0.1:80:80
|
||||||
worker:
|
worker:
|
||||||
image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing
|
image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing
|
||||||
restart: always
|
restart: always
|
||||||
entrypoint: python3
|
entrypoint: celery
|
||||||
command: "-m worker"
|
command: "-A worker.celerymsg worker --loglevel=DEBUG -B"
|
||||||
environment:
|
environment:
|
||||||
- MYSQL_USER=root
|
- MYSQL_USER=root
|
||||||
- MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment
|
- MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||||
- MYSQL_HOST=db
|
- MYSQL_HOST=db
|
||||||
- MYSQL_PORT=3306
|
- MYSQL_PORT=3306
|
||||||
- MYSQL_DB=bellscheduler
|
- MYSQL_DB=bellscheduler
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- REDIS_DBNUM=0
|
||||||
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
- SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment
|
||||||
- NODE_NAME=local
|
- NODE_NAME=local
|
||||||
- POD_NAME=local
|
- POD_NAME=local
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- FLASK_DEBUG=1
|
- FLASK_DEBUG=1
|
||||||
- PYTHONUNBUFFERED=1
|
- PYTHONUNBUFFERED=1
|
||||||
|
redis:
|
||||||
|
image: redis:7.0.10-alpine3.17
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:6379:6379
|
||||||
db:
|
db:
|
||||||
image: mariadb:10.7.8-focal
|
image: mariadb:10.7.8-focal
|
||||||
restart: always
|
restart: always
|
||||||
|
Reference in New Issue
Block a user