Skip to content

Commit

Permalink
Backend/feature/db models (#19)
Browse files Browse the repository at this point in the history
* Basic db definition in flask

* main.py

* Defined official db model in SQLalchemy

* db model definitions in sqlalchemy

* flask_sqlalchemy in requirements

* foutje

* fixed db_uri

* db initialization inside create_app

* db_uri and code clean

* import order fix

* database uri added

* .env added to ignore

* Updated docs and .gitignore

* A first succesfull test for user model

* Doc cleanup and test function for courses and course_relations models added

* Project and submission test added

* added psycopg to dependencies

* dockerized tests to host postgres server

* created test script

* created test directory to test models

* waiting for postgres service to start before running test scripts and moved env variables

* changed github action to run test script instead

* constructing pytests for models

* running test script with sudo

* adding bash to run script

* fixing pytest

* pytests fixed, 1 warning left

* warning fix

* fixed: run github action job on self-hosted runner

* fixed: no longer running test script with privileges

* added: installing docker-compose to run our backend tests

* fixed: no longer running compose install with permissions

* using ubuntu-latest runner until docker-compose is installed on our self-hosted runner

---------

Co-authored-by: warre <[email protected]>
Co-authored-by: Aron Buzogany <[email protected]>
Co-authored-by: abuzogan <[email protected]>
  • Loading branch information
4 people authored Feb 23, 2024
1 parent bd8858c commit 543d961
Show file tree
Hide file tree
Showing 19 changed files with 499 additions and 10 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
working-directory: ./frontend
run: npm run lint
Backend-tests:
runs-on: self-hosted
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Expand All @@ -50,10 +50,10 @@ jobs:
- name: Install dependencies
working-directory: ./backend
run: pip3 install -r requirements.txt && pip3 install -r dev-requirements.txt

- name: Running tests
working-directory: ./backend
run: pytest
run: bash ./run_tests.sh

- name: Run linting
working-directory: ./backend
Expand Down
3 changes: 2 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ __pycache__/
htmlcov/
docs/_build/
dist/
venv/
venv/
.env
15 changes: 15 additions & 0 deletions backend/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.9-slim

# Set the working directory
WORKDIR /app

# Copy the application code into the container
COPY . /app

# Install dependencies
RUN apt-get update
RUN apt-get install -y --no-install-recommends python3-pip
RUN pip3 install --no-cache-dir -r requirements.txt -r dev-requirements.txt

# Command to run the tests
CMD ["pytest"]
21 changes: 19 additions & 2 deletions backend/project/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@
This file is the base of the Flask API. It contains the basic structure of the API.
"""

from flask import Flask, jsonify
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from .endpoints.index import index_bp

db = SQLAlchemy()

def create_app():
"""
Create a Flask application instance.
Returns:
Flask -- A Flask application instance
"""
app = Flask(__name__)

app = Flask(__name__)
app.register_blueprint(index_bp)

return app

def create_app_with_db(db_uri:str):
"""
Initialize the database with the given uri
and connect it to the app made with create_app.
Parameters:
db_uri (str): The URI of the database to initialize.
Returns:
Flask -- A Flask application instance
"""
app = create_app()
app.config["SQLALCHEMY_DATABASE_URI"] = db_uri
db.init_app(app)
return app
7 changes: 5 additions & 2 deletions backend/project/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Main entry point for the application."""
from sys import path
from os import getenv
from dotenv import load_dotenv
from project import create_app_with_db

path.append(".")

if __name__ == "__main__":
from project import create_app
app = create_app()
load_dotenv()
app = create_app_with_db(getenv("DB_HOST"))
app.run(debug=True)
11 changes: 9 additions & 2 deletions backend/project/endpoints/index.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
"""Index api point"""
from flask import Blueprint
from flask_restful import Resource

index_bp = Blueprint("index", __name__)


class Index(Resource):
"""Api endpoint for the / route"""

def get(self):
"""Example of an api endpoint function that will respond to get requests made to /
return a json data structure with key Message and value Hello World!"""
return {"Message": "Hello World!"}

index_bp.add_url_rule("/", view_func=Index.as_view("index"))


index_bp.add_url_rule("/", view_func=Index.as_view("index"))
Empty file.
31 changes: 31 additions & 0 deletions backend/project/models/course_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Models for relation between users and courses"""
# pylint: disable=too-few-public-methods

from sqlalchemy import Integer, Column, ForeignKey, PrimaryKeyConstraint, String
from project import db
from project.models.users import Users
from project.models.courses import Courses

class BaseCourseRelation(db.Model):
"""Base class for course relation models,
both course relation tables have a
course_id of the course to wich someone is related and
an uid of the related person"""

__abstract__ = True

course_id = Column(Integer, ForeignKey('courses.course_id'), nullable=False)
uid = Column(String(255), ForeignKey("users.uid"), nullable=False)
__table_args__ = (
PrimaryKeyConstraint("course_id", "uid"),
)

class CourseAdmins(BaseCourseRelation):
"""Admin to course relation model"""

__tablename__ = "course_admins"

class CourseStudents(BaseCourseRelation):
"""Student to course relation model"""

__tablename__ = "course_students"
15 changes: 15 additions & 0 deletions backend/project/models/courses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""The Courses model"""
# pylint: disable=too-few-public-methods
from sqlalchemy import Integer, Column, ForeignKey, String
from project import db
from project.models.users import Users

class Courses(db.Model):
"""This class described the courses table,
a course has an id, name, optional ufora id and the teacher that created it"""

__tablename__ = "courses"
course_id = Column(Integer, primary_key=True)
name = Column(String(50), nullable=False)
ufora_id = Column(String(50), nullable=True)
teacher = Column(String(255), ForeignKey("users.uid"), nullable=False)
28 changes: 28 additions & 0 deletions backend/project/models/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Model for projects"""
# pylint: disable=too-few-public-methods
from sqlalchemy import ARRAY, Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from project import db
from project.models.courses import Courses

class Projects(db.Model):
"""This class describes the projects table,
a projects has an id, a title, a description,
an optional assignment file that can contain more explanation of the projects,
an optional deadline,
the course id of the course to which the project belongs,
visible for students variable so a teacher can decide if the students can see it yet,
archieved var so we can implement the archiving functionality,
a test path,script name and regex experssions for automated testing"""

__tablename__ = "projects"
project_id = Column(Integer, primary_key=True)
title = Column(String(50), nullable=False, unique=False)
descriptions = Column(Text, nullable=False)
assignment_file = Column(String(50))
deadline = Column(DateTime(timezone=True))
course_id = Column(Integer, ForeignKey("courses.course_id"), nullable=False)
visible_for_students = Column(Boolean, nullable=False)
archieved = Column(Boolean, nullable=False)
test_path = Column(String(50))
script_name = Column(String(50))
regex_expressions = Column(ARRAY(String(50)))
25 changes: 25 additions & 0 deletions backend/project/models/submissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Model for submissions"""
# pylint: disable=too-few-public-methods
from sqlalchemy import Column,String,ForeignKey,Integer,CheckConstraint,DateTime,Boolean
from project import db
from project.models.users import Users

class Submissions(db.Model):
"""This class describes the submissions table,
submissions can be made to a project, a submission has
and id, a uid from the user that uploaded it,
the project id of the related project,
an optional grading,
the submission time,
submission path,
and finally the submission status
so we can easily present in a list which submission succeeded the automated checks"""

__tablename__ = "submissions"
submission_id = Column(Integer, nullable=False, primary_key=True)
uid = Column(String(255), ForeignKey("users.uid"), nullable=False)
project_id = Column(Integer, ForeignKey("projects.project_id"), nullable=False)
grading = Column(Integer, CheckConstraint("grading >= 0 AND grading <= 20"))
submission_time = Column(DateTime(timezone=True), nullable=False)
submission_path = Column(String(50), nullable=False)
submission_status = Column(Boolean, nullable=False)
16 changes: 16 additions & 0 deletions backend/project/models/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Model for users"""
# pylint: disable=too-few-public-methods
from sqlalchemy import Boolean, Column, String
from project import db


class Users(db.Model):
"""This class defines the users table,
a user has an uid,
is_teacher and is_admin booleans because a user
can be either a student,admin or teacher"""

__tablename__ = "users"
uid = Column(String(255), primary_key=True)
is_teacher = Column(Boolean)
is_admin = Column(Boolean)
3 changes: 3 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
flask
flask-restful
flask-sqlalchemy
python-dotenv
psycopg2-binary
20 changes: 20 additions & 0 deletions backend/run_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

# Run Docker Compose to build and start the services, and capture the exit code from the test runner service
docker-compose -f tests.yaml up --build --exit-code-from test-runner

# Store the exit code in a variable
exit_code=$?

# After the tests are finished, stop and remove the containers
docker-compose -f tests.yaml down

# Check the exit code to determine whether the tests passed or failed
if [ $exit_code -eq 0 ]; then
echo "Tests passed!"
else
echo "Tests failed!"
fi

# Exit with the same exit code as the test runner service
exit $exit_code
31 changes: 31 additions & 0 deletions backend/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: '3.8'

services:
postgres:
image: postgres:latest
environment:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_database
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test_user -d test_database"]
interval: 5s
timeout: 3s
retries: 3
start_period: 5s

test-runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres:
condition: service_healthy
environment:
POSTGRES_HOST: postgres # Use the service name defined in Docker Compose
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_database
volumes:
- .:/app
command: ["pytest"]
Loading

0 comments on commit 543d961

Please sign in to comment.