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: password:
from_secret: REGISTRY_PASSWORD from_secret: REGISTRY_PASSWORD
steps: steps:
- name: brand commit before build
image: bash
commands:
- echo ${DRONE_COMMIT_SHA:0:7} > app/__commit__
- name: build app-testing - name: build app-testing
image: plugins/docker image: plugins/docker
settings: settings:
@@ -35,6 +39,10 @@ globals:
password: password:
from_secret: REGISTRY_PASSWORD from_secret: REGISTRY_PASSWORD
steps: steps:
- name: brand commit before build
image: bash
commands:
- echo ${DRONE_COMMIT_SHA:0:7} > app/__commit__
- name: build-app-prod - name: build-app-prod
image: plugins/docker image: plugins/docker
settings: settings:
@@ -43,7 +51,6 @@ steps:
dockerfile: ./Dockerfile dockerfile: ./Dockerfile
tags: ["${DRONE_COMMIT_SHA:0:7}", "latest-prod"] tags: ["${DRONE_COMMIT_SHA:0:7}", "latest-prod"]
<<: *docker_creds <<: *docker_creds
trigger: trigger:
branch: branch:
- master - master
+1
View File
@@ -1,5 +1,6 @@
### Database file ### Database file
database.db database.db
database_mysql/
### Python template ### Python template
# Byte-compiled / optimized / DLL files # 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 COPY ./app /app
WORKDIR /app WORKDIR /app
RUN apk add gcc musl-dev mariadb-connector-c-dev
RUN pip3 install -r requirements.txt RUN pip3 install -r requirements.txt
ENV FLASK_APP=app.py 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 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 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 forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm) from sqlalchemy import inspect
from create_events import createEvents 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__)) basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'HwG55rpe83jcaglifXm8NuF4WEeXyJV4' app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
app.config['SQLALCHEMY_DATABASE_URI'] =\ 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 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app) db.init_app(app)
@@ -27,8 +38,11 @@ login_manager = LoginManager()
login_manager.login_view = 'login' login_manager.login_view = 'login'
login_manager.init_app(app) login_manager.init_app(app)
if os.environ['SIGNUP_ENABLED'] == "YES": if 'SIGNUP_ENABLED' in os.environ:
signup_enabled = True if os.environ['SIGNUP_ENABLED'] == "YES":
signup_enabled = True
else:
signup_enabled = False
else: else:
signup_enabled = False signup_enabled = False
@@ -36,30 +50,80 @@ else:
def load_user(user_id): def load_user(user_id):
return User.query.get(int(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 # Index route
@app.route('/') @app.route('/', methods=['GET', 'POST'])
def index(): def index():
createEvents(db, currDay, Period, Event) # Check for empty database, go to setup page if not setup
return redirect('/events') 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
password = form.password.data password = form.password.data
remember = form.rememberMe.data
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) 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():
@@ -71,17 +135,35 @@ 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'))
@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 # Periods routes
@app.route('/periods') @app.route('/periods')
@login_required @login_required
@@ -92,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,
@@ -99,7 +182,9 @@ def newPeriod():
) )
db.session.add(period) db.session.add(period)
db.session.commit() 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 redirect(f'/period/edit/{period.period}')
return render_template('newPeriod.html', form=form) return render_template('newPeriod.html', form=form)
@@ -107,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')
@@ -131,13 +220,13 @@ def delete_period(periodNum):
def events(): def events():
events = Event.query.all() events = Event.query.all()
periods = Period.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')) @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():
@@ -146,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)
@@ -154,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
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): def createEvents(db, currDay, Period, Event):
periods = Period.query.all() periods = Period.query.all()
for period in periods: for period in periods:
@@ -9,3 +12,5 @@ def createEvents(db, currDay, Period, Event):
) )
db.session.add(event) db.session.add(event)
db.session.commit() 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) 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}">'
@@ -43,3 +44,4 @@ class User(UserMixin, db.Model):
email = db.Column(db.String(100), unique=True) email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100)) password = db.Column(db.String(100))
realName = db.Column(db.String(1000)) realName = db.Column(db.String(1000))
timezone = db.Column(db.String(20), default='UTC')
+12 -5
View File
@@ -1,8 +1,9 @@
import pytz
from db import Task from db import Task
from flask_wtf import FlaskForm 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) 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():
@@ -10,8 +11,8 @@ def get_tasks():
class TaskForm(FlaskForm): 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=[InputRequired(), description = TextAreaField('Description', validators=[Length(max=200)])
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()])
@@ -21,6 +22,11 @@ class PeriodForm(FlaskForm):
weekendSchedule = BooleanField(label='Include on Weekends?', false_values=None) weekendSchedule = BooleanField(label='Include on Weekends?', false_values=None)
periodTime = TimeField('Time', format="%H:%M") 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): class SignupForm(FlaskForm):
userName = StringField('Username', validators=[InputRequired()]) userName = StringField('Username', validators=[InputRequired()])
password = PasswordField('Password', validators=[InputRequired()]) password = PasswordField('Password', validators=[InputRequired()])
@@ -29,4 +35,5 @@ class SignupForm(FlaskForm):
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
userName = StringField('Username', validators=[InputRequired()]) userName = StringField('Username', validators=[InputRequired()])
password = PasswordField('Password', 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, task1 = Task(id=1,
title="Sexy ERP time", title="Sexy ERP time",
description="Be the dominant partner and fuck so much XD", 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", task2 = Task(id=2, title="Test task",
description="Test", description="Test",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task3 = Task(id=3, title="La Nager", task3 = Task(id=3, title="La Nager",
description="Francis stuff", description="Francis stuff",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task4 = Task(id=4, title="Ops/Tech Meeting", task4 = Task(id=4, title="Ops/Tech Meeting",
description="HEIL GEORGE!", description="HEIL GEORGE!",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task5 = Task(id=5, title="Tech Team Meeting", task5 = Task(id=5, title="Tech Team Meeting",
description="Awkward AF", description="Awkward AF",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task6 = Task(id=6, title="Write BellScheduler", task6 = Task(id=6, title="Write BellScheduler",
description="heh", description="heh",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task7 = Task(id=7, title="Fap to cunny porn", task7 = Task(id=7, title="Fap to cunny porn",
description="FBI OPEN UP!", description="FBI OPEN UP!",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task8 = Task(id=8, title="Go on vrchat", task8 = Task(id=8, title="Go on vrchat",
description="Mmm... virtual headpats!", description="Mmm... virtual headpats!",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
task9 = Task(id=9, title="Brush teeth", task9 = Task(id=9, title="Brush teeth",
description="Ya dont do that more often, ya gross fuck", description="Ya dont do that more often, ya gross fuck",
created_timestamp='1668278862', created_timestamp=1668278862,
due_timestamp='') due_timestamp=None)
event1 = Event(id=1, scheduled_date='11-12-2022', period_num=1, task_id=4) event1 = Event(id=1, scheduled_date='03-17-2023', period_num=1, task_id=4)
event2 = Event(id=2, scheduled_date='11-12-2022', period_num=2, task_id=2) event2 = Event(id=2, scheduled_date='03-17-2023', period_num=2, task_id=2)
event3 = Event(id=3, scheduled_date='11-12-2022', period_num=3, task_id=5) event3 = Event(id=3, scheduled_date='03-17-2023', period_num=3, task_id=5)
event4 = Event(id=4, scheduled_date='11-12-2022', period_num=4, task_id=6) event4 = Event(id=4, scheduled_date='03-17-2023', period_num=4, task_id=6)
event5 = Event(id=5, scheduled_date='11-12-2022', period_num=5, task_id=1) event5 = Event(id=5, scheduled_date='03-17-2023', period_num=5, task_id=1)
event6 = Event(id=6, scheduled_date='11-12-2022', period_num=6, task_id=3) event6 = Event(id=6, scheduled_date='03-17-2023', period_num=6, task_id=3)
event7 = Event(id=7, scheduled_date='11-12-2022', period_num=7, task_id=7) event7 = Event(id=7, scheduled_date='03-17-2023', period_num=7, task_id=7)
event8 = Event(id=8, scheduled_date='11-12-2022', period_num=8, task_id=8) event8 = Event(id=8, scheduled_date='03-17-2023', period_num=8, task_id=8)
event9 = Event(id=9, scheduled_date='11-12-2022', period_num=9, task_id=9) 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([period1, period2, period3, period4, period5, period6, period7, period8, period9])
db.session.add_all([task1, task2, task3, task4, task5, task6, task7, task8, task9]) 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 ###
+20 -2
View File
@@ -1,4 +1,22 @@
import time 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 = 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()
+23 -7
View File
@@ -1,25 +1,41 @@
# This file is used by pip to install required python packages # This file is used by pip to install required python packages
# Usage: pip install -r requirements.txt # Usage: pip install -r requirements.txt
# Gunicorn WSGI Server
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==3.10.1
# Celery Task Queue
celery[redis]==5.2.7
redis==4.5.4
# Python Time packages
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
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> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <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 --> <!-- Required meta tags -->
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@@ -10,14 +14,14 @@
<title>{% block title %} {% endblock %} - BellScheduler</title> <title>{% block title %} {% endblock %} - BellScheduler</title>
</head> </head>
<body> <body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-md navbar-light bg-light"> <nav class="navbar navbar-expand-md navbar-light bg-light">
<a class="navbar-brand" href="{{ url_for('index') }}">BellScheduler</a> <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"> <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> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <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"> <li class="nav-item">
<a class="nav-link" href="/events">Events</a> <a class="nav-link" href="/events">Events</a>
</li> </li>
@@ -27,14 +31,17 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/periods">Periods</a> <a class="nav-link" href="/periods">Periods</a>
</li> </li>
</ul>
</div>
<div class="float-right">
{% if current_user.is_authenticated %} {% 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 %} {% endif %}
{% if not current_user.is_authenticated %} {% if not current_user.is_authenticated %}
<a href="{{ url_for('login') }}" class="nav-link"> Login </a> <a href="{{ url_for('login') }}"> Login </a>
{% endif %} {% endif %}
</div> </div>
</nav> </nav>
@@ -42,7 +49,9 @@
<div class="container"> <div class="container">
{% block content %} {% endblock %} {% block content %} {% endblock %}
</div> </div>
<footer class="card-footer mt-auto">
Version {{ currVersion }} &nbsp; &nbsp; Commit: {{ currCommit }}
</footer>
<!-- Optional JavaScript --> <!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS --> <!-- 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> <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> <p>
<b><a href="/task/{{ event.tasks.id }}"> <b><a href="/task/{{ event.tasks.id }}">
{{ event.tasks.title }} {{ event.tasks.title }}
</a></b> <br> {{ event.tasks.description }}
</a></b> <br>{% if event.tasks.description != None %}
<p>{{ event.tasks.description }}</p>
{% endif %}
</p> </p>
{% else %} {% else %}
<div><p>(no task assigned... yet)</p></div> <div><p>(no task assigned... yet)</p></div>
+4
View File
@@ -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>
+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' %} {% extends 'base.html' %}
{% set currDay = datetime.now() %} {% 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') %} {% set currDay = currDay.strftime('%m-%d-%Y') %}
{% block content %} {% block content %}
<span><h1>{% block title %} Events {% endblock %}</h1></span> <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> <div>
{% for period in periods %} {% for period in periods %}
<div> <div>
@@ -18,7 +20,19 @@
</a> </a>
</b> </b>
<div> <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> </div>
{% else %} {% else %}
<div><p>(no task assigned... yet)</p></div> <div><p>(no task assigned... yet)</p></div>
+3
View File
@@ -19,6 +19,9 @@
{{ form.password.label }} {{ form.password.label }}
{{ form.password }} {{ form.password }}
</p> </p>
<p>
{{ form.rememberMe.label}} {{ form.rememberMe }}
</p>
<p> <p>
<button class="btn btn-primary" type="submit">Submit</button> <button class="btn btn-primary" type="submit">Submit</button>
</p> </p>
+4
View File
@@ -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>
+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' %} {% 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>
@@ -8,7 +9,19 @@
<div> <div>
<div> <div>
<br> <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>
<div> <div>
<p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }} </p> <p>Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }} </p>
@@ -16,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>
+18 -1
View File
@@ -6,17 +6,34 @@
{% 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>
{{ 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> </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>
{% endfor %} {% endfor %}
</div>
{% endblock %} {% 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 image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing
restart: always restart: always
environment: environment:
- SQLITE_DB=database.db - MYSQL_USER=root
volumes: - MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment
- ./database.db:/app/database.db - 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: 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