diff --git a/backend/Dockerfile b/backend/Dockerfile index 5a21c42..69d11e4 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -5,7 +5,8 @@ ENV PYTHONUNBUFFERED 1 WORKDIR /app -RUN apt update && pip install --upgrade pip +RUN apt update && \ + pip install --upgrade pip COPY requirements.txt . diff --git a/backend/app/coordinator/routes/examination_schedule.py b/backend/app/coordinator/routes/examination_schedule.py index 7d48518..a0c0df9 100644 --- a/backend/app/coordinator/routes/examination_schedule.py +++ b/backend/app/coordinator/routes/examination_schedule.py @@ -1,11 +1,16 @@ -from apiflask import APIBlueprint -from flask import abort +import datetime +from apiflask import APIBlueprint +from flask import abort, Response, make_response + +from ...base.utils import paginate_models +from ...dependencies import db +from ...examination_schedule.models import ExaminationSchedule, Enrollment +from ...students.models import Group +from ...project_supervisor.models import ProjectSupervisor from ..schemas import ExaminationScheduleSchema, ExaminationScheduleUpdateSchema, MessageSchema, \ ExaminationSchedulesQuerySchema, ExaminationSchedulesPaginationSchema -from ...examination_schedule.models import ExaminationSchedule -from ...dependencies import db -from ...base.utils import paginate_models +from ..utils import generate_examination_schedule_pdf_file bp = APIBlueprint("examination_schedule", __name__, url_prefix="/examination_schedule") @@ -71,3 +76,33 @@ def set_date_of_examination_schedule(id: int, data: dict) -> dict: examination_schedule_query.update(data) db.session.commit() return {"message": "You set date of examination schedule!"} + + +@bp.post('//download/') +def download_examination_schedule(examination_schedule_id: int) -> Response: + examination_schedule = db.session.query(ExaminationSchedule). \ + filter(ExaminationSchedule.id == examination_schedule_id).first() + + if examination_schedule is None: + abort(404, "Examination schedule doesn't exist!") + + distinct_dates = db.session.query(db.func.Date(Enrollment.start_date)).distinct().all() + # print(distinct_dates) + nested_enrollments = [] + for d in distinct_dates: + date_tmp = datetime.datetime.strptime(d[0], "%Y-%m-%d").date() + enrollment = db.session.query(Enrollment).join(ExaminationSchedule, isouter=True).\ + join(Group, isouter=True).join(ProjectSupervisor, isouter=True). \ + filter(ExaminationSchedule.id == examination_schedule_id).filter( + db.func.Date(Enrollment.start_date) == date_tmp).all() + nested_enrollments.append(enrollment) + + # print(nested_enrollments) + pdf = generate_examination_schedule_pdf_file(examination_schedule.title, nested_enrollments) + title = examination_schedule.title.replace("-", "_").split() + filename = "_".join(title) + + response = make_response(pdf) + response.headers['Content-Type'] = 'application/pdf' + response.headers['Content-Disposition'] = 'attachment; filename=%s.pdf' % filename + return response diff --git a/backend/app/coordinator/utils.py b/backend/app/coordinator/utils.py index 86ced4a..cacd399 100644 --- a/backend/app/coordinator/utils.py +++ b/backend/app/coordinator/utils.py @@ -1,10 +1,19 @@ -from typing import Generator, Any, List +import datetime from collections import defaultdict +from io import BytesIO +from itertools import chain +from typing import Generator, Any, List import pandas as pd +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import mm, inch +from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Table from .exceptions import InvalidNameOrTypeHeaderException from ..students.models import Student +from ..examination_schedule.models import Enrollment def check_columns(df: pd.DataFrame) -> bool: @@ -55,3 +64,82 @@ def generate_csv(students: List[Student]) -> str: df = pd.DataFrame(dataframe) return df.to_csv(index=False) + + +def generate_examination_schedule_pdf_file(title: str, nested_enrollments: List[List[Enrollment]]) -> bytes: + pagesize = (297 * mm, 210 * mm) + headers = ["lp.", "Godzina", "Nazwa projektu", "Opiekun", "Zespol", "Komisja"] + pdf_buffer = BytesIO() + my_doc = SimpleDocTemplate( + pdf_buffer, + pagesize=pagesize, + topMargin=1 * inch, + leftMargin=1 * inch, + rightMargin=1 * inch, + bottomMargin=1 * inch, + title=title + ) + + style = getSampleStyleSheet() + bodyText = style['BodyText'] + bodyText.fontName = 'Helvetica' + normal = style["Heading1"] + normal.alignment = TA_CENTER + flowables = [] + + # print(nested_enrollments) + for enrollments in nested_enrollments: + if len(enrollments) == 0: + continue + date = datetime.datetime.strftime(enrollments[0].start_date, '%d/%m/%Y') + paragraph_1 = Paragraph(f"{title} ~ {date}", normal) + flowables.append(paragraph_1) + data = [headers] + + for idx, e in enumerate(enrollments, start=1): + new_date = e.start_date + datetime.timedelta(hours=2) + group_name = e.group.name if e.group is not None else "" + if group_name != '': + ps = e.group.project_supervisor + project_supervisor_fullname = f"{ps.first_name[0]}. {ps.last_name}" + students = e.group.students + # print(students) + team = ", ".join([f"{s.first_name} {s.last_name}" for s in students]) + else: + project_supervisor_fullname = "" + team = "" + + members = e.committee.members + # print(members) + if len(members) == 0: + committee = '' + else: + members_iter = (f"{m.first_name[0]} {m.last_name}" for m in members) + if project_supervisor_fullname != '': + members_iter = chain(members_iter, [project_supervisor_fullname]) + + committee = ", ".join(members_iter) + + data.append([str(idx), new_date.strftime("%H:%M"), + Paragraph(group_name, bodyText), + Paragraph(project_supervisor_fullname, bodyText), + Paragraph(team, bodyText), + Paragraph(committee, bodyText), + ]) + # print(data) + + table = Table(data=data, + style=[ + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#A6F1A6")), + ('BACKGROUND', (0, 0), (1, -1), colors.HexColor("#A6F1A6")) + ], + colWidths=[0.25 * inch, 0.7 * inch, 1.6 * inch, 1.5 * inch, 4 * inch, 3 * inch] + ) + flowables.append(table) + flowables.append(PageBreak()) + + my_doc.build(flowables) + pdf_value = pdf_buffer.getvalue() + pdf_buffer.close() + return pdf_value diff --git a/backend/app/examination_schedule/models.py b/backend/app/examination_schedule/models.py index d0bd5a9..3ce553b 100644 --- a/backend/app/examination_schedule/models.py +++ b/backend/app/examination_schedule/models.py @@ -20,6 +20,7 @@ class Enrollment(Base): examination_schedule = db.relationship('ExaminationSchedule', backref='enrollments') committee = db.relationship("Committee", uselist=False, backref=db.backref('enrollment', passive_deletes=True)) group_id = db.Column(db.Integer, db.ForeignKey('groups.id')) + group = db.relationship("Group", uselist=False, backref='enrollment') class Committee(Base): diff --git a/backend/app/students/models.py b/backend/app/students/models.py index 637b52a..d927e82 100644 --- a/backend/app/students/models.py +++ b/backend/app/students/models.py @@ -17,7 +17,7 @@ class Group(Base): project_supervisor = db.relationship('ProjectSupervisor', backref='groups', lazy=True) points_for_first_term = db.Column(db.Integer, default=0, nullable=False) points_for_second_term = db.Column(db.Integer, default=0, nullable=False) - enrollment = db.relationship('Enrollment', uselist=False, backref='group') + # enrollment = db.relationship('Enrollment', uselist=False, backref='group') @classmethod def search_by_name(cls, search_name: str = None) -> BaseQuery: diff --git a/backend/requirements.txt b/backend/requirements.txt index 7d9b951..fa1bea8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,15 +1,15 @@ -alembic==1.7.7 -click==8.1.3 -Flask==2.1.2 -Flask-Cors==3.0.10 -flask-marshmallow==0.14.0 -Flask-Migrate==3.1.0 -Flask-SQLAlchemy==2.5.1 -MarkupSafe==2.1.1 -marshmallow-sqlalchemy==0.28.0 -pandas==1.4.2 -pytest==7.1.2 -pytest-flask==1.2.0 +click>=8.1.3,<8.2.0 +Flask>=2.1.2,<2.2.0 +Flask-Cors>=3.0.10,<3.1.0 +flask-marshmallow>=0.14.0,<0.15.0 +Flask-Migrate>=3.1.0,<3.2.0 +Flask-SQLAlchemy>=2.5.1,<2.6.0 +MarkupSafe>=2.1.1,<2.2.0 +marshmallow-sqlalchemy>=0.28.0,<0.29.0 +pandas>=1.4.2,<1.5.0 +pytest>=7.1.2,<7.2.0 +pytest-flask>=1.2.0,<1.3.0 apiflask>=1.0.2,<1.1.0 python-dotenv==0.21.0 -factory_boy==3.2.1 \ No newline at end of file +factory_boy>=3.2.1,<3.3.0 +reportlab>=3.6.12,<3.7.0