diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e69de29 diff --git a/app/__init__.py b/app/__init__.py index 8dd0dff..8b8151d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -4,12 +4,18 @@ from flask_migrate import Migrate from app.config import Config -application = Flask(__name__) -# application.config['SECRET_KEY'] = 'amber_pearl_latte_is_the_best' -application.config.from_object(Config) -db = SQLAlchemy(application) -migrate = Migrate(application, db) -login = LoginManager(application) +db = SQLAlchemy() +login = LoginManager() login.login_view = 'login' -from app import routes, models \ No newline at end of file +def create_application(config): + application = Flask(__name__) + application.config.from_object(config) + + db.init_app(application) + login.init_app(application) + + from app.blueprints import blueprint + application.register_blueprint(blueprint) + + return application \ No newline at end of file diff --git a/app/blueprints.py b/app/blueprints.py new file mode 100644 index 0000000..63d8bb8 --- /dev/null +++ b/app/blueprints.py @@ -0,0 +1,6 @@ + +from flask import Blueprint + +blueprint = Blueprint('main', __name__) + +from app import models, routes \ No newline at end of file diff --git a/app/config.py b/app/config.py index c9e4b38..e21075b 100644 --- a/app/config.py +++ b/app/config.py @@ -4,5 +4,12 @@ default_database_uri = 'sqlite:///' + os.path.join(basedir, 'app.db') class Config: - SECRET_KEY = os.environ.get('SECRET_KEY') or 'amber_pearl_latte_is_the_best' - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or default_database_uri \ No newline at end of file + SECRET_KEY = os.environ.get('SECRET_KEY') + +class DeploymentConfig(Config): + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or default_database_uri + +class TestConfig(Config): + SQLALCHEMY_DATABASE_URI = 'sqlite:///memory' + TESTING = True + diff --git a/app/controllers.py b/app/controllers.py index c8904b6..1f0b17c 100644 --- a/app/controllers.py +++ b/app/controllers.py @@ -1,11 +1,10 @@ -from app import application -from app.forms import LoginForm, RegisterProjectForm +from flask import session from app.models import Project, Student from app import db def try_to_login_user(student_id, password, registering): - student = Student.query.get(student_id) + student = db.session.get(Student, student_id) if registering: if student: return f"User {student.id} already exists" diff --git a/app/routes.py b/app/routes.py index c90313a..7328a40 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,14 +1,14 @@ from flask import flash, redirect, render_template, request, url_for from flask_login import login_required, login_user, logout_user -from app import application from app.controllers import try_to_login_user from app.forms import LoginForm, RegisterProjectForm from app.models import Project, Student from app import db +from app.blueprints import blueprint -@application.route('/') -@application.route("/login", methods=['GET', 'POST']) +@blueprint.route('/') +@blueprint.route("/login", methods=['GET', 'POST']) def login(): form = LoginForm() @@ -20,19 +20,19 @@ def login(): result = try_to_login_user(student_id, password, registering) if isinstance(result, Student): login_user(result) - return redirect(url_for('view_projects')) + return redirect(url_for('main.view_projects')) else: flash(result, 'error') return render_template('login.html', form=form) -@application.route("/logout") +@blueprint.route("/logout") def logout(): logout_user() - return redirect(url_for('login')) + return redirect(url_for('main.login')) -@application.route('/projects') +@blueprint.route('/projects') @login_required def view_projects(): projects = Project.query.all() @@ -40,7 +40,7 @@ def view_projects(): print(p) return render_template('index.html', projects=projects) -@application.route('/register', methods=['GET', 'POST']) +@blueprint.route('/register', methods=['GET', 'POST']) @login_required def register_project(): form = RegisterProjectForm() @@ -67,6 +67,6 @@ def register_project(): db.session.commit() - return redirect(url_for('view_projects')) + return redirect(url_for('main.view_projects')) return render_template('register.html', form=form) diff --git a/app/templates/index.html b/app/templates/index.html index e587ee6..aebe729 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -6,7 +6,7 @@ @@ -31,9 +31,9 @@ {{ student.id }} {% endfor %} - {{ if current_user.project_id == project.id }} + {% if current_user.project_id == project.id %} - {{ endif }} + {% endif %} {% endfor %} diff --git a/app/templates/login.html b/app/templates/login.html index 7b1f6ae..07c1564 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -7,7 +7,7 @@ {% block body %}

Login

-
+ {{ form.hidden_tag() }}
{{ form.student_id.label }} @@ -22,7 +22,7 @@

Login

{{ form.register }}
- +
diff --git a/app/templates/register.html b/app/templates/register.html index 74943f9..111e4a3 100644 --- a/app/templates/register.html +++ b/app/templates/register.html @@ -6,7 +6,7 @@ @@ -15,7 +15,7 @@ {% block body %}

Group project sign-up

-
+

diff --git a/project-signup.py b/project-signup.py index 9cf1df4..4140e54 100644 --- a/project-signup.py +++ b/project-signup.py @@ -1,4 +1,9 @@ -from app import application +from flask_migrate import Migrate +from app import create_application, db +from app.config import DeploymentConfig if __name__ == "__main__": + application = create_application(DeploymentConfig) + migrate = Migrate(application, db) + application.run(debug=True, use_debugger=False, use_reloader=False) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f205604 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +alembic==1.15.2 +attrs==25.3.0 +blinker==1.9.0 +certifi==2025.4.26 +click==8.1.8 +exceptiongroup==1.3.0 +Flask==3.1.0 +Flask-Login==0.6.3 +Flask-Migrate==4.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-WTF==1.2.2 +greenlet==3.2.0 +h11==0.16.0 +idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +Mako==1.3.10 +MarkupSafe==3.0.2 +outcome==1.3.0.post0 +PySocks==1.7.1 +selenium==4.32.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +SQLAlchemy==2.0.40 +trio==0.30.0 +trio-websocket==0.12.2 +typing_extensions==4.13.2 +urllib3==2.4.0 +websocket-client==1.8.0 +Werkzeug==3.1.3 +wsproto==1.2.0 +WTForms==3.2.1 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/systemTests.py b/test/systemTests.py new file mode 100644 index 0000000..9b96bc3 --- /dev/null +++ b/test/systemTests.py @@ -0,0 +1,62 @@ + +import multiprocessing +import time +import unittest +from flask import url_for +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions + +from app.config import TestConfig +from app.controllers import try_to_login_user +from app.models import Student +from app import create_application, db + +localHost = "http://localhost:5000/" + +class SystemTests(unittest.TestCase): + + def setUp(self): + self.testApp = create_application(TestConfig) + self.app_context = self.testApp.app_context() + self.app_context.push() + db.create_all() + + self.server_thread = multiprocessing.Process(target=self.testApp.run) + self.server_thread.start() + + + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + self.driver = webdriver.Chrome(options=options) + + def test_successful_login(self): + student = Student(id=1) + student.set_password("a") + db.session.add(student) + db.session.commit() + + self.driver.get(localHost) + id_input = self.driver.find_element(By.ID, 'student_id') + id_input.send_keys("1") + + password_input = self.driver.find_element(By.ID, 'password') + password_input.send_keys("a") + + login_btn = self.driver.find_element(By.ID, 'submitBtn') + login_btn.click() + + WebDriverWait(self.driver, 5).until(expected_conditions.url_changes(self.driver.current_url)) + + self.assertEqual(self.driver.current_url, localHost + 'projects') + + def tearDown(self): + self.server_thread.terminate() + self.driver.close() + db.session.remove() + db.drop_all() + self.app_context.pop() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/test/unitTests.py b/test/unitTests.py index 1486363..73eba50 100644 --- a/test/unitTests.py +++ b/test/unitTests.py @@ -1,17 +1,32 @@ import unittest +from app.config import TestConfig from app.controllers import try_to_login_user from app.models import Student -from app import db +from app import create_application, db class UnitTests(unittest.TestCase): + def setUp(self): + self.testApp = create_application(TestConfig) + self.app_context = self.testApp.app_context() + self.app_context.push() + db.create_all() + def test_successful_login(self): student = Student(id=1) student.set_password("a") db.session.add(student) db.session.commit() + self.assertEqual(student, try_to_login_user(1,"a",False)) + self.assertEqual('Wrong password', try_to_login_user(1,"b",False)) + + def tearDown(self): + db.session.remove() + db.drop_all() + self.app_context.pop() - self.assertThat(try_to_login_user(1,"a",False)) +if __name__ == '__main__': + unittest.main() \ No newline at end of file