71 Commits

Author SHA1 Message Date
6329a647e4 hide expiry date unless has value 2023-04-18 19:45:26 -04:00
86ed978098 fix 2023-04-18 19:41:45 -04:00
b5c49bc46f update templates 2023-04-18 19:41:13 -04:00
dc6fb52b58 update templates with task expiry 2023-04-18 17:26:49 -04:00
b783946273 update task form to include expiry date field 2023-04-18 17:25:59 -04:00
dca5b35395 expiry date migration in alembic 2023-04-18 17:13:56 -04:00
5d6695d608 add expiry_date to task model 2023-04-18 17:13:34 -04:00
1597a322bf brand version 1.1.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-17 20:19:49 -04:00
256c4dddb1 Merge pull request 'garbage-collection' (#31) from garbage-collection into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2023-04-16 19:38:41 +00:00
c4e1a7ab75 add cleanupEvents to worker 2023-04-16 15:24:51 -04:00
723421160f create cleanupEvents function 2023-04-16 15:24:25 -04:00
8d51a75275 bugfix, have hourly run occur at the 59th minute
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-16 10:54:43 -04:00
61f6ea3502 Merge pull request 'Redis Messaging implementation' (#30) from redis-messaging into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #30
2023-04-16 14:44:42 +00:00
e46b1ef4b9 edit docker-compose for celery 2023-04-13 23:56:02 -04:00
85753c5afe rewrite worker to run on celery and redis 2023-04-13 02:11:59 -04:00
b07a9c70d2 Merge pull request '10-detect-empty-db' (#29) from 10-detect-empty-db into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #29
2023-03-24 02:20:17 +00:00
99dc76b25a check for empty database before begin 2023-03-23 22:19:03 -04:00
20af991841 add table detection logic 2023-03-23 20:57:37 -04:00
961573ade1 Merge pull request ''created' time to appear in user's defined tz' (#28) from 22-created-time-localtime into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2023-03-23 23:55:03 +00:00
3a3fd2da9a 'created' time to appear in user's defined tz 2023-03-23 19:53:38 -04:00
ed9f7864e8 Merge pull request 'add podStatus' (#27) from issue-24-liveness-page into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #27
2023-03-23 22:59:03 +00:00
55161f0330 add podStatus 2023-03-23 18:56:59 -04:00
10d0a472b0 Merge pull request 'update base docker image from alpine 3.16/python3.11.0 to alpine 3.17/python3.11.2' (#25) from upgrade-image-python-3.11.2-alpine-3.17 into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #25
2023-03-23 02:15:39 +00:00
f4106f2045 update base docker image from alpine 3.16/python3.11.0 to alpine 3.17/python3.11.2 2023-03-22 22:14:42 -04:00
94bb4c7ac7 Merge pull request 'make links clickable in task descriptions' (#23) from 21-make-links-clickable-tasks into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2023-03-23 02:07:02 +00:00
a58cc0ffcb make links clickable in task descriptions 2023-03-22 21:59:09 -04:00
437c093adf logging bugfix
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-21 22:44:51 -04:00
d0a6b98ddc Merge pull request 'server-logs' (#18) from server-logs into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2023-03-21 23:24:54 +00:00
f1e9c492de Merge branch 'testing' into server-logs 2023-03-21 23:24:48 +00:00
2490fb6698 Merge pull request 'change version branding to 1.1.0' (#17) from version-branding into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2023-03-21 23:24:43 +00:00
fb7df832dc Merge branch 'testing' into version-branding 2023-03-21 23:24:07 +00:00
7372eb625c Merge pull request 'update dependencies' (#16) from dependency-updates into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #16
2023-03-21 23:23:15 +00:00
c21cb03546 add logging to apps 2023-03-21 19:18:08 -04:00
b68e48b1cf create logging service 2023-03-21 19:17:35 -04:00
d9391c73e9 change version to 1.1.0 2023-03-21 18:15:01 -04:00
c9b924a1e3 update dependencies 2023-03-21 18:12:48 -04:00
79592c39fb Merge pull request 'fix worker app' (#14) from worker-context-bugfix into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #14
2023-03-21 00:25:52 +00:00
c2ba1ca5e7 fix worker app 2023-03-20 20:00:40 -04:00
3830bdf240 Merge pull request 'adjust worker for mysql, add more stable background process' (#9) from stable-cron into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #9
2023-03-20 02:34:56 +00:00
c502f359e7 adjust worker for mysql, add more stable background process 2023-03-19 22:34:00 -04:00
a1bfc827d4 Merge pull request 'mysql-migration' (#8) from mysql-migration into testing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #8
2023-03-17 04:58:27 +00:00
ccdc9940df update dockerfile and docker-compose for mysql 2023-03-17 00:56:12 -04:00
a4a3193e05 change sqlite config to mysql 2023-03-17 00:38:09 -04:00
d970954c1c adjust init_db for mysql 2023-03-17 00:37:40 -04:00
0e8565dd2a delete alembic versions from sqlite 2023-03-17 00:27:18 -04:00
80a9266431 update requirements to include mysqlclient 2023-03-17 00:24:19 -04:00
d5d18014a7 add mariadb to docker-compose file 2023-03-17 00:23:08 -04:00
aa87962b24 modify gitignore for local db directory 2023-03-16 23:25:15 -04:00
b3de7df4fc manifest url typo fix
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-12-01 23:06:14 -05:00
cab022e2cb Remove input requirement for task description
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-01 22:53:00 -05:00
da225224b0 adjust jinja2 templates to handle cases of no description 2022-12-01 22:51:34 -05:00
2b315caf9e favicon support
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-01 01:16:10 -05:00
f15d145144 add prevDay variable for previous day, primarily for queries 2022-12-01 00:54:41 -05:00
a441b4612a implement gunicorn for prod WSGI server
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-30 01:09:22 -05:00
645fc4a56b docker-compose file update 2 electric boogalo 2022-11-30 00:46:17 -05:00
71fa0bec22 change docker-compose for new env variables 2022-11-29 22:51:13 -05:00
727941edd3 Error handling and custom error pages
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-29 22:23:01 -05:00
ddc90da012 Add "remember me" option to login
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-29 20:55:32 -05:00
dfa8eed12d remove hardcoded secret key, rely on env variable
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-29 20:41:56 -05:00
bf539fca09 create separate worker app for background tasks
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is passing
2022-11-29 18:12:55 -05:00
130825d3dc re-add createEvents script to newPeriod function
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-26 11:57:08 -05:00
1a1d530b47 fix formatting issue on tasks html template
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-25 20:50:24 -05:00
bbd44e9ec6 add scheduler to generate events, remove hacky workaround 2022-11-25 20:48:16 -05:00
04c8dfb5b4 update docker-compose file to reflect port change
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-25 15:18:33 -05:00
56f7f907df modify drone to brand commits before docker build
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-25 15:12:34 -05:00
ac8bed1bad add footer and support showing version/commit 2022-11-25 15:01:13 -05:00
00c4bc960e prevent app from crashing when SIGNUP_ENABLED env variable does not exist
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-24 19:06:42 -05:00
cc57dfdb32 change flask to use port 80
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-24 18:07:36 -05:00
ec31206127 Add settings page, fix responsive login/logout link
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-24 13:36:37 -05:00
8ed6336e20 create settings page, full timezone support
All checks were successful
continuous-integration/drone/push Build is passing
2022-11-23 20:36:38 -05:00
5c26838c50 begin timezone support 2022-11-22 05:25:11 -05:00
37 changed files with 563 additions and 114 deletions

View File

@@ -9,6 +9,10 @@ globals:
password:
from_secret: REGISTRY_PASSWORD
steps:
- name: brand commit before build
image: bash
commands:
- echo ${DRONE_COMMIT_SHA:0:7} > app/__commit__
- name: build app-testing
image: plugins/docker
settings:
@@ -35,6 +39,10 @@ globals:
password:
from_secret: REGISTRY_PASSWORD
steps:
- name: brand commit before build
image: bash
commands:
- echo ${DRONE_COMMIT_SHA:0:7} > app/__commit__
- name: build-app-prod
image: plugins/docker
settings:
@@ -43,7 +51,6 @@ steps:
dockerfile: ./Dockerfile
tags: ["${DRONE_COMMIT_SHA:0:7}", "latest-prod"]
<<: *docker_creds
trigger:
branch:
- master

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
### Database file
database.db
database_mysql/
### Python template
# Byte-compiled / optimized / DLL files

View File

@@ -1,6 +1,7 @@
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
RUN pip3 install -r requirements.txt
ENV FLASK_APP=app.py
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"]
CMD ["gunicorn", "-b", "0.0.0.0:80", "-w", "4", "app:app"]

1
app/__commit__ Normal file
View File

@@ -0,0 +1 @@
unknown

1
app/__version__ Normal file
View File

@@ -0,0 +1 @@
1.1.1

View File

@@ -1,19 +1,30 @@
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
from misc import datetime, date, time, currDay, prevDay, ZoneInfo, currVersion, currCommit, convDay
from db import (db, Period, Task, Event, User)
from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm)
from create_events import createEvents
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')
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'HwG55rpe83jcaglifXm8NuF4WEeXyJV4'
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, os.environ['SQLITE_DB'])
'mysql://' + os.environ['MYSQL_USER'] + \
':' + os.environ['MYSQL_PASSWORD'] + \
'@' + os.environ['MYSQL_HOST'] + \
':' + os.environ['MYSQL_PORT'] + \
'/' + os.environ['MYSQL_DB']
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
@@ -27,8 +38,11 @@ login_manager = LoginManager()
login_manager.login_view = 'login'
login_manager.init_app(app)
if os.environ['SIGNUP_ENABLED'] == "YES":
signup_enabled = True
if 'SIGNUP_ENABLED' in os.environ:
if os.environ['SIGNUP_ENABLED'] == "YES":
signup_enabled = True
else:
signup_enabled = False
else:
signup_enabled = False
@@ -36,30 +50,80 @@ else:
def load_user(user_id):
return User.query.get(int(user_id))
# Context processor injects current version and commit
@app.context_processor
def injectVerCommit():
return dict(currVersion=currVersion, currCommit=currCommit, datetime=datetime, ZoneInfo=ZoneInfo)
# Error handling
# Pass env variables for debugging in prod
NODE_NAME = os.environ['NODE_NAME']
POD_NAME = os.environ['POD_NAME']
# Error Routes
@app.errorhandler(404)
def notFound(e):
return render_template('errors/404.html', NODE_NAME=NODE_NAME, POD_NAME=POD_NAME), 404
@app.errorhandler(403)
def forbidden(e):
return render_template('errors/403.html', NODE_NAME=NODE_NAME, POD_NAME=POD_NAME), 403
@app.errorhandler(500)
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():
createEvents(db, currDay, Period, Event)
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
password = form.password.data
remember = form.rememberMe.data
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)
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():
@@ -71,17 +135,35 @@ 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():
user.realName = form.realName.data
user.timezone = form.timezone.data
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
@app.route('/periods')
@login_required
@@ -92,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,
@@ -99,7 +182,9 @@ def newPeriod():
)
db.session.add(period)
db.session.commit()
createEvents(db, currDay, Period, Event)
logger.info(f'New period added by \'{current_user.userName}\' from {sourceIP}')
# Run createEvents upon adding new period
runCreateEvents()
return redirect(f'/period/edit/{period.period}')
return render_template('newPeriod.html', form=form)
@@ -107,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')
@@ -131,13 +220,13 @@ def delete_period(periodNum):
def events():
events = Event.query.all()
periods = Period.query.all()
createEvents(db, currDay, Period, Event)
return render_template('events.html', events=events, periods=periods, datetime=datetime, date=date)
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():
@@ -146,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)
@@ -154,42 +244,56 @@ 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():
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()))
created_timestamp=int(time.time()),
expiry_date=expiryDate)
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')

16
app/cleanup_events.py Normal file
View 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()

View File

@@ -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:
@@ -9,3 +12,5 @@ def createEvents(db, currDay, Period, Event):
)
db.session.add(event)
db.session.commit()
logger.info('createEvents script ran successfully')

View File

@@ -20,6 +20,7 @@ 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}">'
@@ -43,3 +44,4 @@ class User(UserMixin, db.Model):
email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100))
realName = db.Column(db.String(1000))
timezone = db.Column(db.String(20), default='UTC')

View File

@@ -1,8 +1,9 @@
import pytz
from db import Task
from flask_wtf import FlaskForm
from wtforms import (StringField, DateField, TimeField, TextAreaField, IntegerField, BooleanField,
from wtforms import (StringField, DateField, TimeField, TextAreaField, IntegerField, SelectField, BooleanField,
RadioField, EmailField, PasswordField)
from wtforms.validators import InputRequired, Length
from wtforms.validators import InputRequired, Length, Optional
from wtforms_sqlalchemy.orm import QuerySelectField
def get_tasks():
@@ -10,8 +11,8 @@ def get_tasks():
class TaskForm(FlaskForm):
title = StringField('Title', validators=[InputRequired(),
Length(min=5, max=100)])
description = TextAreaField('Description', validators=[InputRequired(),
Length(max=200)])
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()])
@@ -21,6 +22,11 @@ class PeriodForm(FlaskForm):
weekendSchedule = BooleanField(label='Include on Weekends?', false_values=None)
periodTime = TimeField('Time', format="%H:%M")
class SettingsForm(FlaskForm):
password = PasswordField('Password')
realName = StringField('Real Name')
timezone = SelectField('Time Zone', choices=pytz.all_timezones)
class SignupForm(FlaskForm):
userName = StringField('Username', validators=[InputRequired()])
password = PasswordField('Password', validators=[InputRequired()])
@@ -29,4 +35,5 @@ class SignupForm(FlaskForm):
class LoginForm(FlaskForm):
userName = StringField('Username', validators=[InputRequired()])
password = PasswordField('Password', validators=[InputRequired()])
password = PasswordField('Password', validators=[InputRequired()])
rememberMe = BooleanField(label='Remember me?')

View File

@@ -16,49 +16,49 @@ period9 = Period(period=9, periodTime='15:30:00', weekendSchedule=False)
task1 = Task(id=1,
title="Sexy ERP time",
description="Be the dominant partner and fuck so much XD",
created_timestamp='1668278862', due_timestamp='')
created_timestamp=1668278862, due_timestamp=None)
task2 = Task(id=2, title="Test task",
description="Test",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task3 = Task(id=3, title="La Nager",
description="Francis stuff",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task4 = Task(id=4, title="Ops/Tech Meeting",
description="HEIL GEORGE!",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task5 = Task(id=5, title="Tech Team Meeting",
description="Awkward AF",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task6 = Task(id=6, title="Write BellScheduler",
description="heh",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task7 = Task(id=7, title="Fap to cunny porn",
description="FBI OPEN UP!",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task8 = Task(id=8, title="Go on vrchat",
description="Mmm... virtual headpats!",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
task9 = Task(id=9, title="Brush teeth",
description="Ya dont do that more often, ya gross fuck",
created_timestamp='1668278862',
due_timestamp='')
created_timestamp=1668278862,
due_timestamp=None)
event1 = Event(id=1, scheduled_date='11-12-2022', period_num=1, task_id=4)
event2 = Event(id=2, scheduled_date='11-12-2022', period_num=2, task_id=2)
event3 = Event(id=3, scheduled_date='11-12-2022', period_num=3, task_id=5)
event4 = Event(id=4, scheduled_date='11-12-2022', period_num=4, task_id=6)
event5 = Event(id=5, scheduled_date='11-12-2022', period_num=5, task_id=1)
event6 = Event(id=6, scheduled_date='11-12-2022', period_num=6, task_id=3)
event7 = Event(id=7, scheduled_date='11-12-2022', period_num=7, task_id=7)
event8 = Event(id=8, scheduled_date='11-12-2022', period_num=8, task_id=8)
event9 = Event(id=9, scheduled_date='11-12-2022', period_num=9, task_id=9)
event1 = Event(id=1, scheduled_date='03-17-2023', period_num=1, task_id=4)
event2 = Event(id=2, scheduled_date='03-17-2023', period_num=2, task_id=2)
event3 = Event(id=3, scheduled_date='03-17-2023', period_num=3, task_id=5)
event4 = Event(id=4, scheduled_date='03-17-2023', period_num=4, task_id=6)
event5 = Event(id=5, scheduled_date='03-17-2023', period_num=5, task_id=1)
event6 = Event(id=6, scheduled_date='03-17-2023', period_num=6, task_id=3)
event7 = Event(id=7, scheduled_date='03-17-2023', period_num=7, task_id=7)
event8 = Event(id=8, scheduled_date='03-17-2023', period_num=8, task_id=8)
event9 = Event(id=9, scheduled_date='03-17-2023', period_num=9, task_id=9)
db.session.add_all([period1, period2, period3, period4, period5, period6, period7, period8, period9])
db.session.add_all([task1, task2, task3, task4, task5, task6, task7, task8, task9])

14
app/log.py Normal file
View 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

View 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 ###

View File

@@ -1,36 +0,0 @@
"""empty message
Revision ID: bf65c9f77f9f
Revises:
Create Date: 2022-11-19 09:49:20.565127
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bf65c9f77f9f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('userName', sa.String(length=1000), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('password', sa.String(length=100), nullable=True),
sa.Column('realName', sa.String(length=1000), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###

View File

@@ -1,4 +1,22 @@
import time
from datetime import datetime, date
from datetime import datetime, date, timedelta
from zoneinfo import ZoneInfo
# Get current day in UTC and put it in str format
currDay = datetime.now()
currDay = currDay.strftime('%m-%d-%Y')
currDay = currDay.strftime('%m-%d-%Y')
# Get previous day in UTC and put it in str format
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()
with open('__commit__','r') as file:
currCommit = file.read()

View File

@@ -1,25 +1,41 @@
# This file is used by pip to install required python packages
# Usage: pip install -r requirements.txt
# Gunicorn WSGI Server
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.10.1
# Celery Task Queue
celery[redis]==5.2.7
redis==4.5.4
# Python Time packages
pytz==2022.7.1
# Automated tests
pytest==7.2.0
pytest==7.2.2
pytest-cov==4.0.0
# MySQL Package
mysqlclient==2.1.1

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
app/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
app/static/manifest.json Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/static/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/static/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -1,6 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Favicon manifest tags -->
<link rel="manifest" href="/static/manifest.json" />
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@@ -10,14 +14,14 @@
<title>{% block title %} {% endblock %} - BellScheduler</title>
</head>
<body>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-md navbar-light bg-light">
<a class="navbar-brand" href="{{ url_for('index') }}">BellScheduler</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<ul class="navbar-nav mr-auto mt-3 mt-lg-0">
<li class="nav-item">
<a class="nav-link" href="/events">Events</a>
</li>
@@ -27,14 +31,17 @@
<li class="nav-item">
<a class="nav-link" href="/periods">Periods</a>
</li>
</ul>
</div>
<div class="float-right">
{% if current_user.is_authenticated %}
<a href="{{ url_for('logout') }}" class="nav-link"> Logout {{current_user.userName}} </a>
<li class="nav-item">
<a class="nav-link" href="/settings">Settings</a>
</li>
{% endif %}
</ul>
{% if current_user.is_authenticated %}
<a href="{{ url_for('logout') }}"> Logout {{current_user.userName}} </a>
{% endif %}
{% if not current_user.is_authenticated %}
<a href="{{ url_for('login') }}" class="nav-link"> Login </a>
<a href="{{ url_for('login') }}"> Login </a>
{% endif %}
</div>
</nav>
@@ -42,7 +49,9 @@
<div class="container">
{% block content %} {% endblock %}
</div>
<footer class="card-footer mt-auto">
Version {{ currVersion }} &nbsp; &nbsp; Commit: {{ currCommit }}
</footer>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>

View File

@@ -9,7 +9,10 @@
<p>
<b><a href="/task/{{ event.tasks.id }}">
{{ event.tasks.title }}
</a></b> <br> {{ event.tasks.description }}
</a></b> <br>{% if event.tasks.description != None %}
<p>{{ event.tasks.description }}</p>
{% endif %}
</p>
{% else %}
<div><p>(no task assigned... yet)</p></div>

View File

@@ -13,6 +13,10 @@
{{ 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

@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% set currDay = datetime.now() %}
{% set currTime = currDay.strftime('%I:%M %p') %}
{% set currDay = currDay.strftime('%m-%d-%Y') %}
{% block content %}
<span><h1><b>{% block title %} Error 403 {% endblock %}</h1></b></span>
<div>
<div>
<h2> Forbidden <br> <br> </h2>
</div>
<div>
Date: {{ currDay }} <br>
Time {{ currTime }} (UTC) <br> <br>
</div>
<div>
Node: {{ NODE_NAME }} <br>
Pod: {{ POD_NAME }}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% set currDay = datetime.now() %}
{% set currTime = currDay.strftime('%I:%M %p') %}
{% set currDay = currDay.strftime('%m-%d-%Y') %}
{% block content %}
<span><h1><b>{% block title %} Error 404 {% endblock %}</h1></b></span>
<div>
<div>
<h2> Page Not Found <br> <br> </h2>
</div>
<div>
Date: {{ currDay }} <br>
Time {{ currTime }} (UTC) <br> <br>
</div>
<div>
Node: {{ NODE_NAME }} <br>
Pod: {{ POD_NAME }}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% set currDay = datetime.now() %}
{% set currTime = currDay.strftime('%I:%M %p') %}
{% set currDay = currDay.strftime('%m-%d-%Y') %}
{% block content %}
<span><h1><b>{% block title %} Error 500 {% endblock %}</b></h1></span>
<div>
<div>
<h2> Internal Server Error <br> <br> </h2>
</div>
<div>
Date: {{ currDay }} <br>
Time {{ currTime }} (UTC) <br> <br>
</div>
<div>
Node: {{ NODE_NAME }} <br>
Pod: {{ POD_NAME }}
</div>
</div>
{% endblock %}

View File

@@ -1,10 +1,12 @@
{% extends 'base.html' %}
{% set currDay = datetime.now() %}
{% set currDay = currDay.astimezone(ZoneInfo(current_user.timezone)) %}
{% set currTime = currDay.strftime('%I:%M %p') %}
{% set currDay = currDay.strftime('%m-%d-%Y') %}
{% block content %}
<span><h1>{% block title %} Events {% endblock %}</h1></span>
<b>Current Date: {{ currDay }} </b> <br> <br>
<b>Current Date: {{ currDay }} </b> <br> <b>Current Time: {{ currTime }}</b> <br> <br>
<div>
{% for period in periods %}
<div>
@@ -18,7 +20,19 @@
</a>
</b>
<div>
<p>{{ event.tasks.description }}</p>
{% 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 %}
{% endif %}
</div>
{% else %}
<div><p>(no task assigned... yet)</p></div>

View File

@@ -19,6 +19,9 @@
{{ form.password.label }}
{{ form.password }}
</p>
<p>
{{ form.rememberMe.label}} {{ form.rememberMe }}
</p>
<p>
<button class="btn btn-primary" type="submit">Submit</button>
</p>

View File

@@ -13,6 +13,10 @@
{{ 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

@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block content %}
<span><h1>{% block title %} Settings {% endblock %}</h1></span>
<form method="post">
{{ form.csrf_token }}
<p>
<b>User: {{ current_user.userName }}</b>
<div>
{{ form.realName.label }} {{ form.realName }}
</div> <br>
<div>
{{ form.password.label }} {{ form.password }}
</div> <br>
<div>
{{ form.timezone.label }} {{ form.timezone }}
</div>
</p>
<p>
<button class="btn btn-primary" type="submit">Submit</button>
</p>
</form>
{% endblock %}

View File

@@ -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>
@@ -8,7 +9,19 @@
<div>
<div>
<br>
<p>{{ task.description }}</p>
{% 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 %}
{% endif %}
</div>
<div>
<p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }} </p>
@@ -16,6 +29,11 @@
<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,17 +6,34 @@
{% 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>
{{ task.description }}
{% 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 %}
{% 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>
{% endfor %}
</div>
{% endblock %}

52
app/worker.py Normal file
View File

@@ -0,0 +1,52 @@
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
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
app.config['SQLALCHEMY_DATABASE_URI'] =\
'mysql://' + os.environ['MYSQL_USER'] + \
':' + os.environ['MYSQL_PASSWORD'] + \
'@' + os.environ['MYSQL_HOST'] + \
':' + os.environ['MYSQL_PORT'] + \
'/' + os.environ['MYSQL_DB']
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():
with app.app_context():
createEvents(db, currDay, Period, Event)
def runCleanupEvents():
with app.app_context():
cleanupEvents(db, Event)
# 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")
}
}

View File

@@ -4,8 +4,54 @@ services:
image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing
restart: always
environment:
- SQLITE_DB=database.db
volumes:
- ./database.db:/app/database.db
- 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
- SIGNUP_ENABLED=YES
ports:
- 127.0.0.1:5000:5000
- 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"
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
environment:
- MARIADB_ROOT_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment
volumes:
- ./database_mysql:/var/lib/mysql
ports:
- 127.0.0.1:3306:3306