From 5c26838c507b459f3c5c3410c855e89c4d48b016 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 22 Nov 2022 05:25:11 -0500 Subject: [PATCH 01/32] begin timezone support --- app/db.py | 1 + app/migrations/versions/d4e71a6e9479_.py | 28 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 app/migrations/versions/d4e71a6e9479_.py diff --git a/app/db.py b/app/db.py index 6d380b0..27fe326 100644 --- a/app/db.py +++ b/app/db.py @@ -43,3 +43,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)) diff --git a/app/migrations/versions/d4e71a6e9479_.py b/app/migrations/versions/d4e71a6e9479_.py new file mode 100644 index 0000000..14e1572 --- /dev/null +++ b/app/migrations/versions/d4e71a6e9479_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: d4e71a6e9479 +Revises: bf65c9f77f9f +Create Date: 2022-11-22 05:24:06.596342 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd4e71a6e9479' +down_revision = 'bf65c9f77f9f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('timezone', sa.String(length=20), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'timezone') + # ### end Alembic commands ### -- 2.49.1 From 8ed6336e208ef8994cba8263719c4ba452c7bbf7 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Wed, 23 Nov 2022 20:36:38 -0500 Subject: [PATCH 02/32] create settings page, full timezone support --- app/app.py | 19 ++++++++++++--- app/db.py | 2 +- app/forms.py | 8 ++++++- .../{d4e71a6e9479_.py => d92ccc005d22_.py} | 8 +++---- app/misc.py | 1 + app/requirements.txt | 3 +++ app/templates/events.html | 4 +++- app/templates/settings.html | 23 +++++++++++++++++++ 8 files changed, 58 insertions(+), 10 deletions(-) rename app/migrations/versions/{d4e71a6e9479_.py => d92ccc005d22_.py} (80%) create mode 100644 app/templates/settings.html diff --git a/app/app.py b/app/app.py index a7421dd..911034b 100644 --- a/app/app.py +++ b/app/app.py @@ -3,9 +3,9 @@ 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, ZoneInfo from db import (db, Period, Task, Event, User) -from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm) +from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm) from create_events import createEvents basedir = os.path.abspath(os.path.dirname(__file__)) @@ -82,6 +82,19 @@ def logout(): logout_user() return redirect(url_for('index')) +@app.route('/settings', methods=('GET', 'POST')) +@login_required +def settings(): + 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() + return render_template('settings.html', form=form) + # Periods routes @app.route('/periods') @login_required @@ -133,7 +146,7 @@ def events(): 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) @app.route('/event/edit//', methods=('GET', 'POST')) @login_required diff --git a/app/db.py b/app/db.py index 27fe326..4bb6bff 100644 --- a/app/db.py +++ b/app/db.py @@ -43,4 +43,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)) + timezone = db.Column(db.String(20), default='UTC') diff --git a/app/forms.py b/app/forms.py index 928c5fb..8548404 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,6 +1,7 @@ +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_sqlalchemy.orm import QuerySelectField @@ -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()]) diff --git a/app/migrations/versions/d4e71a6e9479_.py b/app/migrations/versions/d92ccc005d22_.py similarity index 80% rename from app/migrations/versions/d4e71a6e9479_.py rename to app/migrations/versions/d92ccc005d22_.py index 14e1572..fcd70ff 100644 --- a/app/migrations/versions/d4e71a6e9479_.py +++ b/app/migrations/versions/d92ccc005d22_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: d4e71a6e9479 +Revision ID: d92ccc005d22 Revises: bf65c9f77f9f -Create Date: 2022-11-22 05:24:06.596342 +Create Date: 2022-11-23 20:32:41.868230 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'd4e71a6e9479' +revision = 'd92ccc005d22' down_revision = 'bf65c9f77f9f' branch_labels = None depends_on = None @@ -18,7 +18,7 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('user', sa.Column('timezone', sa.String(length=20), nullable=True)) + op.add_column('user', sa.Column('timezone', sa.String(length=20), nullable=True, server_default='UTC')) # ### end Alembic commands ### diff --git a/app/misc.py b/app/misc.py index 9124938..b0933f7 100644 --- a/app/misc.py +++ b/app/misc.py @@ -1,4 +1,5 @@ import time from datetime import datetime, date +from zoneinfo import ZoneInfo currDay = datetime.now() currDay = currDay.strftime('%m-%d-%Y') \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt index 8ed3ceb..1158712 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -20,6 +20,9 @@ Flask-User==1.0.2.2 # WTForms Extensions WTForms-SQLAlchemy==0.3.0 +# Python Time packages +pytz==2022.6 + # Automated tests pytest==7.2.0 pytest-cov==4.0.0 diff --git a/app/templates/events.html b/app/templates/events.html index b4b1c0d..fc9cfe1 100644 --- a/app/templates/events.html +++ b/app/templates/events.html @@ -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 %}

{% block title %} Events {% endblock %}

- Current Date: {{ currDay }}

+ Current Date: {{ currDay }}
Current Time: {{ currTime }}

{% for period in periods %}
diff --git a/app/templates/settings.html b/app/templates/settings.html new file mode 100644 index 0000000..04c0611 --- /dev/null +++ b/app/templates/settings.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block content %} +

{% block title %} Settings {% endblock %}

+
+ {{ form.csrf_token }} +

+ User: {{ current_user.userName }} +

+ {{ form.realName.label }} {{ form.realName }} +

+
+ {{ form.password.label }} {{ form.password }} +

+
+ {{ form.timezone.label }} {{ form.timezone }} +
+

+

+ +

+
+{% endblock %} \ No newline at end of file -- 2.49.1 From ec3120612755ff81f09caa231acf0ea93f6cc779 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 24 Nov 2022 13:36:37 -0500 Subject: [PATCH 03/32] Add settings page, fix responsive login/logout link --- app/templates/base.html | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/templates/base.html b/app/templates/base.html index ba71787..3c17a12 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -17,7 +17,7 @@ -
{% if current_user.is_authenticated %} - Logout {{current_user.userName}} + + {% endif %} + + {% if current_user.is_authenticated %} + Logout {{current_user.userName}} {% endif %} {% if not current_user.is_authenticated %} - Login + Login {% endif %}
-- 2.49.1 From cc57dfdb325c8cf8ef6e28ca4424ab1948383867 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 24 Nov 2022 18:07:36 -0500 Subject: [PATCH 04/32] change flask to use port 80 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bc2d72d..4c3adc7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ COPY ./app /app WORKDIR /app RUN pip3 install -r requirements.txt ENV FLASK_APP=app.py -CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"] \ No newline at end of file +CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0", "--port=80"] \ No newline at end of file -- 2.49.1 From 00c4bc960e4ac57779a0b8a7f35f3a91a3796e9d Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 24 Nov 2022 19:06:42 -0500 Subject: [PATCH 05/32] prevent app from crashing when SIGNUP_ENABLED env variable does not exist --- app/app.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/app.py b/app/app.py index 911034b..f1d61b0 100644 --- a/app/app.py +++ b/app/app.py @@ -27,8 +27,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 -- 2.49.1 From ac8bed1bad7c7e1641ccd362217a5c94222f9908 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 25 Nov 2022 15:01:13 -0500 Subject: [PATCH 06/32] add footer and support showing version/commit --- app/__commit__ | 1 + app/__version__ | 1 + app/app.py | 7 ++++++- app/misc.py | 7 ++++++- app/templates/base.html | 6 ++++-- 5 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 app/__commit__ create mode 100644 app/__version__ diff --git a/app/__commit__ b/app/__commit__ new file mode 100644 index 0000000..87edf79 --- /dev/null +++ b/app/__commit__ @@ -0,0 +1 @@ +unknown \ No newline at end of file diff --git a/app/__version__ b/app/__version__ new file mode 100644 index 0000000..7f20734 --- /dev/null +++ b/app/__version__ @@ -0,0 +1 @@ +1.0.1 \ No newline at end of file diff --git a/app/app.py b/app/app.py index f1d61b0..257bf35 100644 --- a/app/app.py +++ b/app/app.py @@ -3,7 +3,7 @@ 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, ZoneInfo +from misc import datetime, date, time, currDay, ZoneInfo, currVersion, currCommit from db import (db, Period, Task, Event, User) from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm) from create_events import createEvents @@ -39,6 +39,11 @@ 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) + # Index route @app.route('/') def index(): diff --git a/app/misc.py b/app/misc.py index b0933f7..cfc9589 100644 --- a/app/misc.py +++ b/app/misc.py @@ -2,4 +2,9 @@ import time from datetime import datetime, date from zoneinfo import ZoneInfo currDay = datetime.now() -currDay = currDay.strftime('%m-%d-%Y') \ No newline at end of file +currDay = currDay.strftime('%m-%d-%Y') + +with open('__version__','r') as file: + currVersion = file.read() +with open('__commit__','r') as file: + currCommit = file.read() \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 3c17a12..983983f 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -10,7 +10,7 @@ {% block title %} {% endblock %} - BellScheduler - +
{% endfor %} +
{% endblock %} \ No newline at end of file -- 2.49.1 From 130825d3dca2bb050c4c6898d5d9f8daa0fbe6ea Mon Sep 17 00:00:00 2001 From: William Peebles Date: Sat, 26 Nov 2022 11:57:08 -0500 Subject: [PATCH 11/32] re-add createEvents script to newPeriod function --- app/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/app.py b/app/app.py index f8971a7..96edaca 100644 --- a/app/app.py +++ b/app/app.py @@ -124,6 +124,8 @@ def newPeriod(): ) db.session.add(period) db.session.commit() + # Run createEvents upon adding new period + createEvents(db, currDay, Period, Event) return redirect(f'/period/edit/{period.period}') return render_template('newPeriod.html', form=form) -- 2.49.1 From bf539fca09414a7893dcd472b96726dc19aebea5 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 29 Nov 2022 18:12:55 -0500 Subject: [PATCH 12/32] create separate worker app for background tasks --- app/app.py | 5 ++--- app/backgroundTasks.py | 14 -------------- app/worker.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 17 deletions(-) delete mode 100644 app/backgroundTasks.py create mode 100644 app/worker.py diff --git a/app/app.py b/app/app.py index 96edaca..6d7ecec 100644 --- a/app/app.py +++ b/app/app.py @@ -6,7 +6,6 @@ from flask_login import (LoginManager, login_user, login_required, logout_user, from misc import datetime, date, time, currDay, ZoneInfo, currVersion, currCommit from db import (db, Period, Task, Event, User) from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm) -from backgroundTasks import scheduleCreateEvents from create_events import createEvents basedir = os.path.abspath(os.path.dirname(__file__)) @@ -23,8 +22,8 @@ migrate = Migrate() migrate.init_app(app, db) # Schedule creation of events every hour -with app.app_context(): - scheduleCreateEvents(app, db, currDay, Period, Event, createEvents) +#with app.app_context(): +# scheduleCreateEvents(app, db, currDay, Period, Event, createEvents) # Authentication stuff diff --git a/app/backgroundTasks.py b/app/backgroundTasks.py deleted file mode 100644 index f8d6458..0000000 --- a/app/backgroundTasks.py +++ /dev/null @@ -1,14 +0,0 @@ -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.cron import CronTrigger - -def scheduleCreateEvents(app, db, currDay, Period, Event, createEvents): - # create events upon application launch - createEvents(db, currDay, Period, Event) - - # schedule createEvents task every hour - sched = BackgroundScheduler() - def eventsTask(): - with app.app_context(): - createEvents(db, currDay, Period, Event) - sched.add_job(eventsTask, CronTrigger.from_crontab('00 * * * *')) - sched.start() \ No newline at end of file diff --git a/app/worker.py b/app/worker.py new file mode 100644 index 0000000..f9de303 --- /dev/null +++ b/app/worker.py @@ -0,0 +1,31 @@ +import os +from flask import Flask +from misc import currDay +from db import db, Period, Event +from create_events import createEvents +from apscheduler.schedulers.background import BlockingScheduler +from apscheduler.triggers.cron import CronTrigger + +basedir = os.path.abspath(os.path.dirname(__file__)) + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'HwG55rpe83jcaglifXm8NuF4WEeXyJV4' +app.config['SQLALCHEMY_DATABASE_URI'] =\ + 'sqlite:///' + os.path.join(basedir, os.environ['SQLITE_DB']) +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +db.init_app(app) + +# declare BlockingScheduler +sched = BlockingScheduler() + +# run createEvents upon app launch +with app.app_context(): + createEvents(db, currDay, Period, Event) + +def eventsTask(): + with app.app_context(): + createEvents(db, currDay, Period, Event) +sched.add_job(eventsTask, CronTrigger.from_crontab('00 * * * *')) +print("Background worker started") +sched.start() + -- 2.49.1 From dfa8eed12d836f912ca33b54dbbff754b2376212 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 29 Nov 2022 20:41:56 -0500 Subject: [PATCH 13/32] remove hardcoded secret key, rely on env variable --- app/app.py | 2 +- app/worker.py | 2 +- docker-compose.yaml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/app.py b/app/app.py index 6d7ecec..09ace99 100644 --- a/app/app.py +++ b/app/app.py @@ -11,7 +11,7 @@ from create_events import createEvents 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']) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False diff --git a/app/worker.py b/app/worker.py index f9de303..3593008 100644 --- a/app/worker.py +++ b/app/worker.py @@ -9,7 +9,7 @@ from apscheduler.triggers.cron import CronTrigger 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']) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False diff --git a/docker-compose.yaml b/docker-compose.yaml index 889859d..90e42b2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,6 +5,7 @@ services: restart: always environment: - SQLITE_DB=database.db + - SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment volumes: - ./database.db:/app/database.db ports: -- 2.49.1 From ddc90da0127036f8a39a56aa2e457881e8570709 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 29 Nov 2022 20:55:32 -0500 Subject: [PATCH 14/32] Add "remember me" option to login --- app/app.py | 3 ++- app/forms.py | 3 ++- app/templates/login.html | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/app.py b/app/app.py index 09ace99..6c9f3b8 100644 --- a/app/app.py +++ b/app/app.py @@ -60,12 +60,13 @@ def login(): 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') return redirect(url_for('login')) - login_user(user) + login_user(user, remember=remember) return redirect(url_for('events')) return render_template('login.html', form=form) diff --git a/app/forms.py b/app/forms.py index 8548404..e276de7 100644 --- a/app/forms.py +++ b/app/forms.py @@ -35,4 +35,5 @@ class SignupForm(FlaskForm): class LoginForm(FlaskForm): userName = StringField('Username', validators=[InputRequired()]) - password = PasswordField('Password', validators=[InputRequired()]) \ No newline at end of file + password = PasswordField('Password', validators=[InputRequired()]) + rememberMe = BooleanField(label='Remember me?') \ No newline at end of file diff --git a/app/templates/login.html b/app/templates/login.html index ed2c19e..a2df10d 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -19,6 +19,9 @@ {{ form.password.label }} {{ form.password }}

+

+ {{ form.rememberMe.label}} {{ form.rememberMe }} +

-- 2.49.1 From 727941edd3728d6ac95aa77aa7bb20f5228cca04 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 29 Nov 2022 22:23:01 -0500 Subject: [PATCH 15/32] Error handling and custom error pages --- app/app.py | 20 +++++++++++++++++++- app/templates/errors/403.html | 22 ++++++++++++++++++++++ app/templates/errors/404.html | 22 ++++++++++++++++++++++ app/templates/errors/500.html | 22 ++++++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 app/templates/errors/403.html create mode 100644 app/templates/errors/404.html create mode 100644 app/templates/errors/500.html diff --git a/app/app.py b/app/app.py index 6c9f3b8..0da03e0 100644 --- a/app/app.py +++ b/app/app.py @@ -46,7 +46,25 @@ def load_user(user_id): # Context processor injects current version and commit @app.context_processor def injectVerCommit(): - return dict(currVersion=currVersion, currCommit=currCommit) + 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 # Index route @app.route('/') diff --git a/app/templates/errors/403.html b/app/templates/errors/403.html new file mode 100644 index 0000000..5b703b1 --- /dev/null +++ b/app/templates/errors/403.html @@ -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 %} +

{% block title %} Error 403 {% endblock %}

+
+
+

Forbidden

+
+ +
+ Date: {{ currDay }}
+ Time {{ currTime }} (UTC)

+
+
+ Node: {{ NODE_NAME }}
+ Pod: {{ POD_NAME }} +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html new file mode 100644 index 0000000..a6fa5f8 --- /dev/null +++ b/app/templates/errors/404.html @@ -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 %} +

{% block title %} Error 404 {% endblock %}

+
+
+

Page Not Found

+
+ +
+ Date: {{ currDay }}
+ Time {{ currTime }} (UTC)

+
+
+ Node: {{ NODE_NAME }}
+ Pod: {{ POD_NAME }} +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/errors/500.html b/app/templates/errors/500.html new file mode 100644 index 0000000..690f0d3 --- /dev/null +++ b/app/templates/errors/500.html @@ -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 %} +

{% block title %} Error 500 {% endblock %}

+
+
+

Internal Server Error

+
+ +
+ Date: {{ currDay }}
+ Time {{ currTime }} (UTC)

+
+
+ Node: {{ NODE_NAME }}
+ Pod: {{ POD_NAME }} +
+
+{% endblock %} \ No newline at end of file -- 2.49.1 From 71fa0bec22cc1c6eade56b552f752b462d2a4618 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Tue, 29 Nov 2022 22:51:13 -0500 Subject: [PATCH 16/32] change docker-compose for new env variables --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 90e42b2..8513e4f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,6 +6,8 @@ services: environment: - SQLITE_DB=database.db - SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment + - NODE_NAME=local + - POD_NAME=local volumes: - ./database.db:/app/database.db ports: -- 2.49.1 From 645fc4a56b26d5157d0f98ab3db68b5d25c84d8d Mon Sep 17 00:00:00 2001 From: William Peebles Date: Wed, 30 Nov 2022 00:46:17 -0500 Subject: [PATCH 17/32] docker-compose file update 2 electric boogalo --- docker-compose.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 8513e4f..2ee00c2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,6 +8,9 @@ services: - SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment - NODE_NAME=local - POD_NAME=local + - FLASK_ENV=development + - FLASK_DEBUG=1 + - PYTHONUNBUFFERED=1 volumes: - ./database.db:/app/database.db ports: -- 2.49.1 From a441b4612a285ae8f81b82cbd08160d7982f8cfb Mon Sep 17 00:00:00 2001 From: William Peebles Date: Wed, 30 Nov 2022 01:09:22 -0500 Subject: [PATCH 18/32] implement gunicorn for prod WSGI server --- Dockerfile | 2 +- app/requirements.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4c3adc7..c77976c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,4 +3,4 @@ COPY ./app /app WORKDIR /app RUN pip3 install -r requirements.txt ENV FLASK_APP=app.py -CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0", "--port=80"] \ No newline at end of file +CMD ["gunicorn", "-b", "0.0.0.0:80", "-w", "4", "app:app"] \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt index fb83cc6..b935201 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,6 +1,9 @@ # 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 -- 2.49.1 From f15d145144783e43eee887881e1d0fa3586647ac Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 1 Dec 2022 00:54:41 -0500 Subject: [PATCH 19/32] add prevDay variable for previous day, primarily for queries --- app/app.py | 2 +- app/misc.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/app.py b/app/app.py index 0da03e0..a26430f 100644 --- a/app/app.py +++ b/app/app.py @@ -3,7 +3,7 @@ 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, ZoneInfo, currVersion, currCommit +from misc import datetime, date, time, currDay, prevDay, ZoneInfo, currVersion, currCommit from db import (db, Period, Task, Event, User) from forms import (TaskForm, EventForm, PeriodForm, SignupForm, LoginForm, SettingsForm) from create_events import createEvents diff --git a/app/misc.py b/app/misc.py index cfc9589..f4667bb 100644 --- a/app/misc.py +++ b/app/misc.py @@ -1,9 +1,16 @@ 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') + + with open('__version__','r') as file: currVersion = file.read() with open('__commit__','r') as file: -- 2.49.1 From 2b315caf9e5bdbdc0921b8c958d013b728541edc Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 1 Dec 2022 01:16:10 -0500 Subject: [PATCH 20/32] favicon support --- app/static/android-chrome-192x192.png | Bin 0 -> 14923 bytes app/static/android-chrome-512x512.png | Bin 0 -> 38829 bytes app/static/apple-touch-icon.png | Bin 0 -> 13467 bytes app/static/favicon-16x16.png | Bin 0 -> 816 bytes app/static/favicon-32x32.png | Bin 0 -> 1910 bytes app/static/favicon.ico | Bin 0 -> 15406 bytes app/static/manifest.json | 1 + app/templates/base.html | 4 ++++ 8 files changed, 5 insertions(+) create mode 100644 app/static/android-chrome-192x192.png create mode 100644 app/static/android-chrome-512x512.png create mode 100644 app/static/apple-touch-icon.png create mode 100644 app/static/favicon-16x16.png create mode 100644 app/static/favicon-32x32.png create mode 100644 app/static/favicon.ico create mode 100644 app/static/manifest.json diff --git a/app/static/android-chrome-192x192.png b/app/static/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..972695927845fcde30a363d0a3d74e2797dc644f GIT binary patch literal 14923 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 z

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 N4K*FrGG&|4{{X->m9GE* literal 0 HcmV?d00001 diff --git a/app/static/apple-touch-icon.png b/app/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ef51cf0b30f28b1072a391bb20c915d4305239 GIT binary patch literal 13467 zcmV;MG-S((P)PyA07*naRCr$Poe6kcRr&uvcV?0_Ns~6+(lssJpsZynyU3=%50G6^6cI!aWf74@ zL<9vCK|~N`QQ(ib;Pw*)`LW4X%1)szTcLZCZb{lEX_{na?*IATnUpMdotZnCw0V8{ z6g%gfd(L~_d+vMQ<@*{GA{l^Lh!g8Jrz(h@1IhEi7z-qTIGD}A1~3|dHDJ~OOTkzS z#uA9H2U>uQKvSyE)TM+pR#2-qX?zE>*{+v9jFwW4NEX8q<0fEklzmaRf zxGJNr{yIr@nFq!k;3Y`t@5TK>5%agc&{_=$bVjU=Tnfg|GU^(P(j|7KCXIT3vN;q};x1Uft>x!gdeW5!Mg zvj!*uih;`1A9FBJ1cv@izYPLPQ~#Dzk{~|@i5q~o(f7TMihR%}J2o#Rhs>hMY-v z%dmH#8G1C9x+2&5u3(-A9GTX`gFd4%^@8wT(2o7J{Z=Z_{dGHR=dC@O0+F*)f^A6m z)&rVpAr4addDq=7Gat>_9F^C-o=3S{5QDn}ADasejPXo7?H*9bn99gi83NuZ2cJ$ zjWIZ)KT(Q|2r&#I2ErF%a1)#AT5vdzTJnR_RdoEFHqM;&P&fK@`LKJ7vey|G?~m(yK4+SiAa6=0n7_G+S6Ul+zNLYW+goxW-cm)DLqaOmbG@u0g?Z?NE;#`O zy5P7Jh1lU>UIXlt3AbW~K}}I9+ZB~Dv$T?Z%c_`BJQ$tIVbSV%8*gvk$ec}$yw}#u z;`U9{C)zU=@+L5T3vub!a@zE_XwjUqIiNu2^x4@CHZ-j#OT7r3+b6Z*@bcjtU0H3N zz@uU%loIvMP?R9L+wZRg8=&RHJZ7Li~PPGtaiv z^Vbb^EbC|uGFd^3Jp-`|pzVVo9k+(Ig)h*ba9QyR*MoU+I>y42{}Bu(6qRz#sOcP7 zJ}kSNqUY1XY0zdSxvhRNzi(VlyBX-bZUf_3h(A?udUf_+X-8%kzCfoIFT1j`nw5L! zIcGK4p`?OShm7UK%F)Pb5gz42oA#dF!uA&auzn>EH?3l0!Z*I-Dlo6^S7}(@tZ;4N zDI8gjpNsCJAo{<+zJ7#Rdh(f76F9r7h9S`+Z|Tp)3sJV`?Rb4dO)JKPTU=M)0_GT3!b=H;rvt`PYH z7`u24s6~9OY64%cs-ZeQU1z7qx$v$S0$Qan6P@rWYS1$oH*Z&K>nj ztr!vy+6?Bcki0q!o!aZ+VgKG|0`0u8sLV`M-@4_%;rzFaYk*fp4nMQ}DxXsj;`8O!A(r&3`hTz99w_}u4n zv-~_ut`}Vr&sZ^+mZVS=T&V#fheO9(UJd)p?_C7ir|5;Uvgpwu5N`{L){#C}QOz~g z+f!*H%%M!H9}gpPV6^Q}x85BdTm{#tNxA6~OAtP?tNF&NjmK?zJOD&>x` zyRbuvu*2O+nIUmZOTX^xH`Qyd`TYHX^(*-Kx{tkd>Q#`C>i1^vxfPI3;la4R-1kWk zou7i#vY*BvBf>vxW--0Q3A!z{I(f~mU-uFJ+PI1i(`2vGA^d#w42IdLCxv`*3~O$R z0r~#w_xZ<0&$^!SBz~=L3lU}a^7NHJSEU1v1TSDqN`bm^G8v3bN;cwN>+72!PvuTnPXxl{G(k~WMjQ-^?UrMNfm_cNg3OBkKN6J z9vGlh_U!$Q%lXOLg&v7r=1pmkbWcBeq4Bj31Uhmx7>XmXeRg>y$}dOn#G!+oVKsCz z-&((zI~$g|@^inkDt0II=A(l^s>e&48r&Ya3=*FO`dtcFW&)l0XF{1L z3*8{Oj;pBVhS4)zO>|E+ujPWe1xT>n=$v6STry$`O2rzWPdK0erh^yHv9V8g<&tqT z#4qV@d<$i#EqU)P^hFT60nG0@F-ca9hbHW41?oG|du>gex$+&>#NCrG1Qc`2n4JdV zJscSMn}#L)vVJjf=b~@TkWf~AF9g)g&$IAfkAMQ5)l!6r{}h^UGTkW-hx;pH0tGbFy$pPVfL1v(q?h1^!?NNTCQ62zS{+HB_xEW z{Z4;bZ@vV&6r#Fd*kWl&h_4til{2cGp*94cb=v4o ziCjElisPyJWb<0icV#*pS23C&S8qR{HwTm|U9P{r@*Ng*!XotUp9JGbh(8%{=sszl z2Z1h{1<8kY`B4?B|1&_Da6su=M=p8AEo*!eBvd8r-Au;MrccoGDGvf|1@EgkWG_;f=UwA= zwZO%l=(Oc;@^X*Bqpoc~J9wmJZlU0us9c z3krGlVG8uPR7MK63bK-2pP#%RgA9B3w7|a4tDWZ)jK#c=0|Hj;DPn_r$xjtxUqT$Ck!}Iaw=FK zwzqJ?(%0M)YF|zVEamjMxg0D+fsR}N#xI=Ab#hUEx_r3pS`@2s*y1^?jN4;Mj4c|( zedBgxO0j#cVJ;c9EsSOHeaReFS{W_2H-k7Ngo^1JOzp|5+^NjH9u<~AM$y_-kx=$MtFPu`(R=GfXql@0#vr2QBXb|tGG zLZ<4kO8a_gQzMPNkchf-NG_hq#pRqbWDH*!I=){4y^uNcU}j628qXl)-(5YfCQL)XHz*;RMphhCU;iV1g;o4&Gl{i;hGOn zyl&UJPbxcc-tb9Q*@=9SG)X}voW5eN^>-!_8StTUgNXYc}G%iExM?<`w zk+BkvsTgI&Oxq!k24t4+<}Ytr&xLjKts?ZnD0Qilo_~DBXsfhbnB|s(&CR{_S5~}b zeTf|CMMxay<6;;AE2%?uUA$qh>>t*ej>o>;FZfMJ0v%PB!=rX{)CS)&dPk1-Q9?rY z&{i`+!o(7c(n!QAAfGRi5PxIkJ60`|Y)CUcv#N$GMo!J+>@%OSN?fQ+vpt#fASuYO z&SrK+hx9Uam@I5eoch`U<&spcG z!R_@*Bwgegjp~O#idVUF+^)~2a;{w?E1E6ULG%b{cQ!*2`z)A`xFKT&UAu-P(9wGUMP~IP zMaVrpXvAHmtPFW9_DiLPWnhbi6~KfUNP^{5K6txoMs5wVzP8ua*&^ zs%khjd?@2|aoH%3* zGMkE=HvgX+SMj}7?>c>%)>5N14X57O69PnqM~9(4zFg+=m{8xLEWn5tXW$} zN8GhA*DH|N-^=O|mO%dvILU4`B}RlflMi&XLCa((X1>xR26|d?Ik%79nW=g34$5Qp zwUu+3<9sn<>f{;hG-DX^ORR$jF3xR$HIK=M3UIHy(?!P$1 z5*$!g#jo=KzUU>;QaAM3w4;`mCLVo48dr7#?IrMDzYE7i0_{Y%j@ikv`tH*$>$srS zv5ZALEzpW}x1p#C5dFhZpl2OS&3Jpq*s3)vd4Bd2Zc7t(^xn^l{(_CxL!_ z%0ZMx>~jDg+E9zUgm&mi=TuGPdm~eCcSxTWYiy|;axN@%P&q0qhH&`7#}bPb^;+Ub zi{IzXxpN$Oo-dU{>cY`WUu98y?@OuKdWTTt$3RF}g2m{g-nVd_{iO_L?dKC{fKb0KU4=n<_#+!vb64Y13p!VKk8_7j;T{I z94p!o{pkyUg1Y!-j#~1npJMQ?!qN3d#joge%s1&!*qDe@5smqZ{PS?EtcK`kq1`3M zH0O>Lne60lmn!Q@SqZeWZXNkH7`8F9`sDp#{O+jue4&%VMSoTAW=8VA0u?d3YwWIh zu|fNvm3>RO`)_k}eX|&1M?#yI>syu%$NG~2=lCon1SRKLY%wc=?mDUN!tE8Dv=c}7 z1EeEe4$rC0O>mMv^$L4+g9{G0$)3$sw^61?sc;jDN`2RwQSF+)IhantcW{zURGHX< zhK=@Im%LP``Eh%zW6lJ+$1J8n)bW(7q1a{NQ$q?ZA zCs&TKYLw>_-ANu-3FgAK7E5ibipH$+9G`KpJ$GG>oJzAhy`;TA!@{}yoR5r>7(`D4 ze&>`|>Vt(QnBRuv-GHlKJY*eOf&Ee4sBHBLKLP$e7Mr&w^EPkfyQ|){OkhE9Aaxoa zTT#tbBd0l9b;4MLQo~MM{(3JgZCwf#KA@aTH}VMZe-siY`bSW*lBKT#{K8vxPOToy zA45{f0Y1qH?Sxhe)*LW)vYUwt(ce7AIR(q->~qu1r;%MLSifsrZp8_^)Fs(6r<$Tt z%QACh#Yo=?3E6rF^>B?Lz28aqKe2K&H;$fx)7HxxviHX?I(is^yC88+0Ol8iWt&#z zGZk9(y5nmJFM@Fm#N~|)lvxp$%Aq7#`G0LAuDp_D128M^O_d}oVNvB;A>d0tO<7_L ziVVX#i6t}&743`qh|S5kg{A#`i#^hS0;5!z$w{hY|37=BYm)3m!A{LBTT}$mqzXZN*Cvn>7$0qL6uPapP!nm|!lNHG(p;mfTb*#h+ z(d^4qE4l53QCH_sCG61h5f&&_k zRO=2~tS>j9nT%I6(9wd#6*R7su7Bb1?YO`p$yheA13#Y4>V%_n$b^IxaOe0~&pqh3 z0&N-BWsA%R3e1x)TT=s{dc4NQ=`e)57Mmog92)bA*hcj5CE_UX%+132$6M^m}5F;Ua7a;p%=Y8UFNPH7;Db5j_ z4W`m&Z0W%XyR&bpJ)G{gDUzA5TG5H$S+xKK7CF)9hm7Hd>KV2_ONf8u4yP+zyFIh7 zS=jdiT>@5FiJtMz`Y!zu5-Qo|5LFWS78t@*uq6SW|8Ugw&}@g9i%0+KId5|lXV<>% zSjRDjVc`i*bx2P320S<3hqw->ECJ7h2y}2=%*UlLb`Y4)Ic)J}NQ`w^WMw*((6;8p zu7fJLXWVWGD|&#kXfx3x!iv_W+zv- z_eI`CgD!-mE!Ik~mF7^OZ4PiB0v+2AOy{gmtV*N1J1@!tosrsM`z*zD^arepV4Ie| z)^hHgvg{4F!kHFq!9C-4b^Hh0@kkC!zefw>6FTAO+$|ol6n<|>QwNP-?FQb)Pmzeb{tSosqcl_0(rlb3c9chf) zJi}y-7a)GHpYToZg=8tv9uM$wLTiM`9bm|!;XtOFJG3l)sgxhzmQ)(~yCdu~fxQ_N zq(v^GC4xM!vu!xfl{fIzX9_0N@p z5a{BaAu-n_4Dew{d=YSf4YD9r6-H=FLKOO2&7O>jWrgeKpQl?vOF@*KxneGFY_@lz zXo+&c-#>09d)ffobiQIfiq{LAao60jtRMthw&&PeU^*>q1tj!I>W-cD2^VDTcpDJ% zO)pZIrOKz=@u4KFZ{PH(=#~EEUnTE6dnfI*x zwkQmNHXwErm=`-w+dS8;$R;?aWogXqO9u1riL*jSMe@t`LT;xdRsr(3>CM6r{MW=; zOm~4!Q!N#F23=@nI+ss}(H6??iclB=Ep6ji;7@=<=_%vgbXKlYWXnW@$Wtc9R3`>1 zqvG6Q69*hE4h)h2|GQ~5SFQN~rB^wTg!xCKrt^g%auL{*k^oL$IhVyYxE)=3JP3)e z`OOybcL)@WKqL3}YrsT@iAyaLRFjGY*-W z>JQkEbk9ceg8_JF!%_>T!3ioX6_1=yn5QGIZf1PWw?MmoPa;1B<1&|pJd@5hvqw3E zogn&(^C^#2OjCNFZT?-axrJjr$ZVs+>!p0g|2EfKYKD{Zr1R;rY6vcEVv%!g9${af zwCr{Cx69#lJ0vd1{i^aU&^9aRB1dD>$*)coFB`x(0pe-gvtANtK;-*i{KQEoE007G z?%E;cK~vCj&-y|bQ7EE!H!kCj#-+5n02#tSWX^RP&SS5X+$j--*2T5ZJnaZro4O)Ra_6N#O(1orO&0L?5)Z zSU4l@<>|~?NGhssX*dfmIDtksW!2Ac^!x$;O(Wen9V}%Ko8w~Fwl?Pa>K&|v(`|_q zoAK+0#oXDj%vW}~#P_GwGpte*Zf;-MoKIZ#8t=9>yRlO#R>2ThQvd)INl8ROR4NFh zfU=Otr$A?D8}#Y=?zy(;m-Y2>S+TNrPl3e0o!b=6#7I7Y$w;TUE6zi}-2DMIEZg1J zWpB==1`DL}&DIUxm{+R#5Q%c}@X4Gpbi8ZbbD8+AU$@XIWa_3kr_8cee7i4D7q1{7 z`)BzS=nNT}p+KvChE@95CCW}H?v^`T!tFp-BJE91(5vb+zGx74kKK*wfvRR@$c;Yg z_FPLnDpDjs8@qfyyBA)u8N$~%tbBwgD(jy&tmMaQ7P{pMYUPa(ITSh+;t(R)3G^0g zRpwdWX(nYV(2~JJ!Bj03rvMBE|0@d5B}GeUI#D@m4tL|=LFL1^W%N!c4{>WCIY<-~ zG_S3RdmESg(v!QcTR3%BkDBIb2~yJR3oQ+tS3A#3nA(G}+*my$H$t7EewOV{t1|7JmPTIK)L@;) zi#j&>(vv%%t7-3FHjK+gPGLq#g(n2j^DXs!Z}oeq-iiw;UgI~Al+D=_-7{MrK;a6s z?$;_1BhR4S(J4&dnSe`y#x3kQ9HRg7LaLPIJ8zF^bZDN`!iO|WY zq|-R5lUQ}L71btqflNUk<<35-ag46&{?_h&D4}Ri4%V*QqGL7X9sILw1uQZP3nxDmlBVhR+Tj;cAw% z#1pLgikGc=kJ`AGFjEJ{Db6L@x_%q7Sw+_sdI@yGy}W?OE0?W6Yk1+<>68{8aOZd{ zAaV?Jbc@pRn2W}dInY&~)&mYKAI7btckUDHxXv{^+fvUZtKPMQH&yf_cJvQ{_F`a=-?xX@|AM)LSEnh(WfpLXT}Fmi-)w%rr*RoK zto#WSgZ#E(Df%OKxd#bE{3=NP0%-MWoc-_p$I6lYTAfFj z0@MK1gAgs*;rpl1Fr8w3xk0hjQNViKwP|5Lz2=%%-hm&8ewTl9`opNLU)(>Gib73K9x|5mhfPAo z*}GHjuZrFn%3d>8iHIUTRX&VQl?`X+ph_kdms&9yLFna8joe(nh__ld_|fCV2!(|H z2(Sl_4)XAf+vZy3=>A4@;b88xq>R_vyW?bts=(tYKVu4;`8Q*CvEZ)4E^z;~m2dMx zOTCLArCua)HDVand@3SQs~p5hL&mUQSyf;G?&N{IiT5=w=jZF3QaC$XZh4s#DQM%k zTP(XD7U!T%t_8aLl!6@zIap2xL#P1Joq&>XJ?A?(663U?@r>OL%5Y=OG#;;J&llZwl-DjcNPXqn{hUB80an;KkDLc4Lf z9n2dbDXeZctbz=-CX+4$0xhmpLsUh}KkbKw`g9MA74exfY$!5^^(T^8FKutNg!s^e?#d~}kEo=W?5FOv$-4BC`D>?=3Qig_n*SMGjncgOgaTYsseU6`HvoHlZys~atF50vEt`NC59lq+JS9kw>Pbn_v8&wlH zuzWa*w^ocqA&_R-l8mFGKf=rS>xQ~a;y#+edeB^>DbPMHS$@|Z;PCM~ zq*uEM5INVtIL~xW2Xh=_Y-|v-N{3jg$U$Yp85i&ZnJZ(UE_&~5ZsMg)4OSSb{@aIh z#AaQ_JE=mIR!KD9O12l1bN_ot0-b$4bncg$!H8PMJ`mL=W|XUCnbj~XP{S`*jpvxb zqw-Rg?W6VPcv9HQ_n}=^ov;q zk?#Os1*0aL=f;c(+n1DEI=5WwI>|@HN~~JhN-$7%wU?TrP|{7wqy-2g^ImOtcq{WR=JhcLX}p3l}>dl;8VHlj`TZ$?gQl`#V?sKY9M-PO0XR>X?)AO zWm`5P)`=~*y8gS^Fsw^uRkYZ$IhRDDmYGh;tqKs5y3i2spo7%^Z%ZbvY>`$o5#(a$ zHAf`}UJ8lFvafmK^{8C`o^OHfGkNUBIL%@-lvF~S;;HKbl*-jD?*NGY0N4WnNQ;8GCG%)qD zF9!1;kW~6ymKCis;Xt1YbS9Gy+((Qa3z18~I7eUl-XoX`A^G`qN>@;Qu^M8Rg4rq9 zjsQ1-XP(0jU5*YJQRL7Q&@) zXc|dLg6vh7q7Ka8rcNu?W*|$WAgbg6)m%Kn1xYo7p>x(n@DPs!&q7>iuMT-sj@v7g z*Zsj1l$Vl}-BRUs(m$RKTzV#FD{vhs@?q+0VN0tZF(&hQgyLB7^B!%P@`N;u!M@$f zUAPaSY>P#ZJYDH?87@8wkxRh%X@>p7>XwAaKG5+-h8{k_tX&1hB|s2N;>{^u#08=m z^y_EK3+FA2tOCO!Z_wITuS3n1_kDFjD=tH)ru1AvnpsNmx(_11OXn^I&?_}! zgLphQF+GN1P+XkSblTb+lUI`v-5uKB3HZh+BtQwtI+bSy z?AzH~g8Tp^6k_UJUpc#UET!t{PUI9+GaC{IW;lPVTVDvY0Tz^)Qh>6>%$2UG%;mp9 z{06{R+D3|D&H_DXP$_%w`6&jKl+xO|iKgZav}|gkxuu!5wpQBO+Gy|COd^panT~vu zz(yibVzC${#e*m*DWP;w8H39!DKD>}qGAZeMa3*$`Vk*2bVPOtD$ptwBEX+(0F{ZA zH6t66P`?EU65F^*$Su8Dn4k2ND9Wk@-vY7rN|bzY%Di zzU2^+JzdogoNC-SZnBaM4lN(S4@OSor|Ukl;;Wr1X!STm9!j6o?HL>{J?En&4M9}w z<1o>+knoji=O*x32y~z&>STE+rEa*BYOH*F_;x5iP^4<9_cSiG!YKn}YIJ>w^ukxb zcnWw9;;*MXWd$$LlOTFP8lyzuJAq?Vv_m@%s^F_b$MZiGqbN0^2q#(s{hNj*yx-RB zbzbY@eIX7+*=O@3%4g*uk22OhZKOrIYDJ#lCpo2U&ay=NAcfEx6d>x{O!IdMX zaq7^qz2FG635pX|(HvnXXGm7aams#eJPfAr0&Gib1l6H~%*R{h&;>8@DKG_9EYq8f zlgayuQZLRKR>N6UHC9kn&y5y8zg0WW3jp0ZQy{q`m~KT`yeE{3h?L4{M>54;2+7-h z4CLA*4+34ZCnWW$8Rs+;1^-++Vv1vd_0E1ep&!~%%ilNFv9P@*5OQblXFEqU_n|;% zyPsCzlrqb|F>Im*kKjg%1iYwj0kbzXdg<0GeJz#86D-gEc~Lod1b*)%Z)?)fadISO zMIj5chF9Tx&IQwOTF(_Dr}CAd<9!3gs2rI-o4;;Y;sq6(5p!a(BBo55&ah#ls2rR! zyXoIDAz46V>T7ClqN#a3OP4QV?YaQgMV}AV7fy)^XAhgeE+v(|QI`bhcwyZFuP>kQ zcs~yb9ZW%~1|koGA+w?r>8^7^`koVX{BG?-5$G=J#6*bdLS`E~q0izeL&jLhN14!O zGr{fkOL@3yl?7WX)g&w`E-t35bg<=ck%L4w*51bE z&6_MccEW#_cHrp?GGSuTATAm)nNODucP&lfH28wHCQe)amRF|CUm+pyY>-nr0#5aRoyy1g5=)0PBI8fperEqg44}* z$C3&j7(dfDDt)#a)!APHzN}*t|7~gDl}(M7?!7kN&iZ7kq(sgr;0q(OrC@%Y*GdF(-#&jQZUIGclqkrh5ZJ;J0 z2(-o3(GXpfLiX=XUmP-yAB~#sj5(Ksjq&1wkcjFN?W|TUl=x=rF=9D9o6=8=s=Tf;l6#SK!fUFh^Q9q zbm||%39Wl<wl*h|)+w%&ULq%>nFLvg zqY`9+u&Z=&5C-R8bnRL7i*<`Er&W$gZ^&g~3bW=Lgx>+y7roc%La2+)lauzz%Jv*! zBHK*UT)qrKbh>Qyd%5J@CNPeH__LuX1YKqtmOww8zUlU=o+QAQSEn4<=elApcWY}K z>+$BbR*Z*>`Ofc>^`1Qn2?eIP-ZcFV=+!PPfmQ~wat3>m1p4i%2YD8d^qTm<@8NOa z>6Ud|P&?nNKDgHe1ZX1_Lx)@5@IS&5=;wfg>{cw@^6jag4h>b`i^~JQdvoBwE%lsV zJKwr51SDB4gmU#PB$VeUq^D5G>AqjU0-Z~(Fch9{wS#6L&_Y0c4h? zf)MBd(ap_f=4uol0C6AKbf8v1ey;)Hfu<-o}u`u^KKz(b%_^YBoJKcC6$x*l*D`n_Bq zThIcX3Bx-3^!JycFXx|Yfeu4(on)hL1v;NA&F1ac#`@-3p!3PTz7^=uR@#^6TLz0N zm=`<6w*i6fwTGWv3VZ&vEnN!x*=x0BtMV_1F0tptmSd17^G6VpKRi3PPYWRLFBH&|CJT zZv=YlJ1hG}$O}TCGhBNA66g%y+6;N)v)k798Vv|^08gRvppUK`h2RXfS}MpF&p)>X zSNvM*dMl&dhO&yb^FNEsT81Q5&MHS{(;VnkKpfaM1iJg7G$7Ey1+d?!F58Acci+Im z66k)e0XV)JeYCD^QlM2&<7nHR=%ZZ-eqs*vaNvOIf*%g?*?~Ber+p7ehqjR0Zw<8I zfyVuX14|%wB($m2NWoD^0L}dT~ literal 0 HcmV?d00001 diff --git a/app/static/favicon-16x16.png b/app/static/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..f959cccad01c2bba785e57c30c32ede515f01300 GIT binary patch literal 816 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 literal 0 HcmV?d00001 diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f044edd46fc0fb9c96ef84c42e44dd71cda20fa9 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/app/static/manifest.json b/app/static/manifest.json new file mode 100644 index 0000000..258ecd0 --- /dev/null +++ b/app/static/manifest.json @@ -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"} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 983983f..3e7e558 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,6 +1,10 @@ + + + + -- 2.49.1 From da225224b0811beddd3216a35b463e2981399f9e Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 1 Dec 2022 22:51:34 -0500 Subject: [PATCH 21/32] adjust jinja2 templates to handle cases of no description --- app/templates/editEvent.html | 5 ++++- app/templates/events.html | 2 ++ app/templates/task.html | 4 +++- app/templates/tasks.html | 4 +++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/templates/editEvent.html b/app/templates/editEvent.html index cb48cb5..2c5b78e 100644 --- a/app/templates/editEvent.html +++ b/app/templates/editEvent.html @@ -9,7 +9,10 @@

{{ event.tasks.title }} -
{{ event.tasks.description }} + +
{% if event.tasks.description != None %} +

{{ event.tasks.description }}

+ {% endif %}

{% else %}

(no task assigned... yet)

diff --git a/app/templates/events.html b/app/templates/events.html index fc9cfe1..f0dea65 100644 --- a/app/templates/events.html +++ b/app/templates/events.html @@ -20,7 +20,9 @@
+ {% if event.tasks.description != None %}

{{ event.tasks.description }}

+ {% endif %}
{% else %}

(no task assigned... yet)

diff --git a/app/templates/task.html b/app/templates/task.html index 71ff660..886659b 100644 --- a/app/templates/task.html +++ b/app/templates/task.html @@ -8,7 +8,9 @@

-

{{ task.description }}

+ {% if task.description != None %} +

{{ task.description }}

+ {% endif %}

Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }}

diff --git a/app/templates/tasks.html b/app/templates/tasks.html index dbe5e03..e7f0db8 100644 --- a/app/templates/tasks.html +++ b/app/templates/tasks.html @@ -11,7 +11,9 @@

{{ task.title }}

- {{ task.description }} + {% if task.description != None %} + {{ task.description }} + {% endif %}

Created: {{ createdTime.strftime('%Y-%m-%d %I:%M %p') }}

-- 2.49.1 From cab022e2cba0be784dcc5f185fbf4b320d184c2b Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 1 Dec 2022 22:53:00 -0500 Subject: [PATCH 22/32] Remove input requirement for task description --- app/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/forms.py b/app/forms.py index e276de7..31a44b6 100644 --- a/app/forms.py +++ b/app/forms.py @@ -11,8 +11,7 @@ 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)]) class EventForm(FlaskForm): # eventDate = DateField('Date', validators=[InputRequired()], format='m-%d-%Y') # period_num = IntegerField(validators=[InputRequired()]) -- 2.49.1 From b3de7df4fcbb278d192120f4d000f020f03b46ab Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 1 Dec 2022 23:06:14 -0500 Subject: [PATCH 23/32] manifest url typo fix --- app/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/base.html b/app/templates/base.html index 3e7e558..85b67fc 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -2,7 +2,7 @@ - + -- 2.49.1 From aa87962b248c6c4e52f4064b6e1b54be34ca6724 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Thu, 16 Mar 2023 23:25:15 -0400 Subject: [PATCH 24/32] modify gitignore for local db directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 87fe0d8..6f9de4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ ### Database file database.db +database_mysql/ ### Python template # Byte-compiled / optimized / DLL files -- 2.49.1 From d5d18014a75344a104ccfb1d95e2334898938435 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:23:08 -0400 Subject: [PATCH 25/32] add mariadb to docker-compose file --- docker-compose.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 2ee00c2..c03ccc4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,3 +15,12 @@ services: - ./database.db:/app/database.db ports: - 127.0.0.1:80:80 + 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 \ No newline at end of file -- 2.49.1 From 80a9266431d53eaf31021bbf8e4a34d96713b0f4 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:24:19 -0400 Subject: [PATCH 26/32] update requirements to include mysqlclient --- app/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/requirements.txt b/app/requirements.txt index b935201..bd4eaad 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -32,3 +32,6 @@ pytz==2022.6 # Automated tests pytest==7.2.0 pytest-cov==4.0.0 + +# MySQL Package +mysqlclient==2.1.1 \ No newline at end of file -- 2.49.1 From 0e8565dd2a3edf0eac94342936cdbf38cbcf216b Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:27:18 -0400 Subject: [PATCH 27/32] delete alembic versions from sqlite --- app/migrations/versions/bf65c9f77f9f_.py | 36 ------------------------ app/migrations/versions/d92ccc005d22_.py | 28 ------------------ 2 files changed, 64 deletions(-) delete mode 100644 app/migrations/versions/bf65c9f77f9f_.py delete mode 100644 app/migrations/versions/d92ccc005d22_.py diff --git a/app/migrations/versions/bf65c9f77f9f_.py b/app/migrations/versions/bf65c9f77f9f_.py deleted file mode 100644 index ed45c69..0000000 --- a/app/migrations/versions/bf65c9f77f9f_.py +++ /dev/null @@ -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 ### diff --git a/app/migrations/versions/d92ccc005d22_.py b/app/migrations/versions/d92ccc005d22_.py deleted file mode 100644 index fcd70ff..0000000 --- a/app/migrations/versions/d92ccc005d22_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: d92ccc005d22 -Revises: bf65c9f77f9f -Create Date: 2022-11-23 20:32:41.868230 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'd92ccc005d22' -down_revision = 'bf65c9f77f9f' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('user', sa.Column('timezone', sa.String(length=20), nullable=True, server_default='UTC')) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('user', 'timezone') - # ### end Alembic commands ### -- 2.49.1 From d970954c1cad38a8e34ca69ba8d42f973e22bbb9 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:37:40 -0400 Subject: [PATCH 28/32] adjust init_db for mysql --- app/init_db.py | 52 +++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/app/init_db.py b/app/init_db.py index ece185e..3e47332 100644 --- a/app/init_db.py +++ b/app/init_db.py @@ -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]) -- 2.49.1 From a4a3193e05ca0fba2af8910715f7d3a12de1fc51 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:38:09 -0400 Subject: [PATCH 29/32] change sqlite config to mysql --- app/app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/app.py b/app/app.py index a26430f..79351ef 100644 --- a/app/app.py +++ b/app/app.py @@ -13,7 +13,11 @@ basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) 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) -- 2.49.1 From ccdc9940dfe2e0b716749d795a59eb1cbae62ff2 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Fri, 17 Mar 2023 00:56:12 -0400 Subject: [PATCH 30/32] update dockerfile and docker-compose for mysql --- Dockerfile | 1 + docker-compose.yaml | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c77976c..95e5a63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.11.0-alpine3.16 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 ["gunicorn", "-b", "0.0.0.0:80", "-w", "4", "app:app"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index c03ccc4..c0fc347 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,15 +4,17 @@ services: image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing restart: always environment: - - SQLITE_DB=database.db + - MYSQL_USER=root + - MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment + - MYSQL_HOST=db + - MYSQL_PORT=3306 + - MYSQL_DB=bellscheduler - SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment - NODE_NAME=local - POD_NAME=local - FLASK_ENV=development - FLASK_DEBUG=1 - PYTHONUNBUFFERED=1 - volumes: - - ./database.db:/app/database.db ports: - 127.0.0.1:80:80 db: -- 2.49.1 From c502f359e78bedf47994ee6b8973e161d04502f5 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Sun, 19 Mar 2023 22:34:00 -0400 Subject: [PATCH 31/32] adjust worker for mysql, add more stable background process --- app/worker.py | 34 ++++++++++++++++++++-------------- docker-compose.yaml | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/app/worker.py b/app/worker.py index 3593008..34e292f 100644 --- a/app/worker.py +++ b/app/worker.py @@ -1,31 +1,37 @@ import os from flask import Flask -from misc import currDay +from misc import currDay, datetime, time, timedelta from db import db, Period, Event from create_events import createEvents -from apscheduler.schedulers.background import BlockingScheduler -from apscheduler.triggers.cron import CronTrigger +from apscheduler.schedulers.background import BackgroundScheduler basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) 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) -# declare BlockingScheduler -sched = BlockingScheduler() +# Calculate the next time the function should run (at the :59 mark of the next hour) +now = datetime.now() +next_hour = now.replace(hour=now.hour + 1, minute=0, second=0, microsecond=0) +next_run = next_hour - timedelta(minutes=1) -# run createEvents upon app launch +# Create scheduler and add job to call createEvents at the specified time +scheduler = BackgroundScheduler() +scheduler.add_job(createEvents, 'date', run_date=next_run, args=[db, currDay, Period, Event]) +scheduler.start() + +# Call createEvents on initial launch of the script with app.app_context(): createEvents(db, currDay, Period, Event) -def eventsTask(): - with app.app_context(): - createEvents(db, currDay, Period, Event) -sched.add_job(eventsTask, CronTrigger.from_crontab('00 * * * *')) -print("Background worker started") -sched.start() - +# Keep the program running indefinitely +while True: + time.sleep(60) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index c0fc347..c811c2a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,6 +17,23 @@ services: - PYTHONUNBUFFERED=1 ports: - 127.0.0.1:80:80 + worker: + image: container-registry.infra.dubyatp.xyz/bellscheduler/app:latest-testing + restart: always + entrypoint: python3 + command: "-m worker" + environment: + - MYSQL_USER=root + - MYSQL_PASSWORD=notasecuresecretkeyonlyuseforlocaldevelopment + - MYSQL_HOST=db + - MYSQL_PORT=3306 + - MYSQL_DB=bellscheduler + - SECRET_KEY=notasecuresecretkeyonlyuseforlocaldevelopment + - NODE_NAME=local + - POD_NAME=local + - FLASK_ENV=development + - FLASK_DEBUG=1 + - PYTHONUNBUFFERED=1 db: image: mariadb:10.7.8-focal restart: always -- 2.49.1 From c2ba1ca5e7de9e19417d661abfe2443048c8b604 Mon Sep 17 00:00:00 2001 From: William Peebles Date: Mon, 20 Mar 2023 20:00:40 -0400 Subject: [PATCH 32/32] fix worker app --- app/worker.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/app/worker.py b/app/worker.py index 34e292f..288a29e 100644 --- a/app/worker.py +++ b/app/worker.py @@ -3,7 +3,7 @@ from flask import Flask from misc import currDay, datetime, time, timedelta from db import db, Period, Event from create_events import createEvents -from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.schedulers.background import BlockingScheduler basedir = os.path.abspath(os.path.dirname(__file__)) @@ -18,20 +18,18 @@ app.config['SQLALCHEMY_DATABASE_URI'] =\ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) -# Calculate the next time the function should run (at the :59 mark of the next hour) -now = datetime.now() -next_hour = now.replace(hour=now.hour + 1, minute=0, second=0, microsecond=0) -next_run = next_hour - timedelta(minutes=1) - -# Create scheduler and add job to call createEvents at the specified time -scheduler = BackgroundScheduler() -scheduler.add_job(createEvents, 'date', run_date=next_run, args=[db, currDay, Period, Event]) -scheduler.start() +# Define function to run create_events with app context +def run_create_events(): + with app.app_context(): + createEvents(db, currDay, Period, Event) # Call createEvents on initial launch of the script -with app.app_context(): - createEvents(db, currDay, Period, Event) +run_create_events() -# Keep the program running indefinitely -while True: - time.sleep(60) \ No newline at end of file + +# Set up scheduler to run function at 59th minute of every hour +scheduler = BlockingScheduler() +scheduler.add_job(run_create_events, 'cron', minute=59) + +# Start scheduler +scheduler.start() \ No newline at end of file -- 2.49.1