71 Commits

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+1
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"}
+17 -8
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>
+4 -1
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>
+4
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>
+22
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 %}
+22
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 %}
+22
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 %}
+16 -2
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>
+3
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>
+4
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>
+23
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 %}
+19 -1
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>
+18 -1
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
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")
}
}
+50 -4
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