Skip to content

API Reference

REST endpoints

Full endpoint documentation: Reviews on ReDoc ⧉

Review endpoints

Method Path Description
GET /reviews List reviews (filter by reviewee_id, reviewer_id, type, is_active)
GET /reviews/<id> Get a single review
PATCH /reviews/<id> Extend review dates (start_date, end_date)
GET /reviews/<id>/review-submissions List submissions for a review
POST /reviews/<id>/review-submissions Create a submission (add a reviewer)
GET /reviews/<id>/review-submissions/<id> Get a single submission
PATCH /reviews/<id>/review-submissions/<id> Update submission answers
GET /reviews/<id>/review-submissions/<id>/answers Get submission with answers (ABAC-gated)
PUT /reviews/<id>/review-submissions/<id>/publication Publish a submission
PUT /reviews/<id>/review-submissions/<id>/validation Approve a submission
DELETE /reviews/<id>/review-submissions/<id>/validation Reject a submission
GET /reviews/<id>/review-cycle Get the associated review cycle
GET /reviews/<id>/pdf Export review as PDF
PUT /reviews/<id>/release Release review (PDF to Google Drive)

Alaner-scoped endpoints

Method Path Description
GET /alaners/<id>/reviews List alaner's reviews
GET /alaners/<id>/review-submissions List approved submissions for reviewer
GET /alaners/<id>/reviews/<id>/self-review Get self-review submission
GET /alaners/<id>/received-feedback List feedback received (completed, ordered by end_date desc)
GET /alaners/<id>/given-feedback List feedback given (completed, ordered by end_date desc)
POST /alaners/<id>/feedback-summary Generate AI feedback summary via Dust (query: months 6-12)

Code reference

apps.eu_tools.alan_home.models.review_template.OptionValue dataclass

OptionValue(value, label)

Bases: DataClassJsonMixin

label instance-attribute

label

value instance-attribute

value

apps.eu_tools.alan_home.models.review_template.ReviewQuestion dataclass

ReviewQuestion(
    id, type, title=None, description=None, options=None
)

Bases: DataClassJsonMixin

description class-attribute instance-attribute

description = None

id instance-attribute

id

options class-attribute instance-attribute

options = None

title class-attribute instance-attribute

title = None

type instance-attribute

type

apps.eu_tools.alan_home.models.review_template.ReviewQuestionType

Bases: AlanBaseEnum

callout class-attribute instance-attribute

callout = 'callout'

checkbox class-attribute instance-attribute

checkbox = 'checkbox'

select class-attribute instance-attribute

select = 'select'

text class-attribute instance-attribute

text = 'text'

wrap_up class-attribute instance-attribute

wrap_up = 'wrap_up'

apps.eu_tools.alan_home.models.review_template.ReviewTemplate

Bases: BaseModel

__tablename__ class-attribute instance-attribute

__tablename__ = 'review_template'

community_id class-attribute instance-attribute

community_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=True,
    index=True,
)

description class-attribute instance-attribute

description = mapped_column(Text)

job_title class-attribute instance-attribute

job_title = mapped_column(Text, nullable=True)

job_titles class-attribute instance-attribute

job_titles = mapped_column(ARRAY(Text), nullable=True)

name class-attribute instance-attribute

name = mapped_column(Text, nullable=False)

questions class-attribute instance-attribute

questions = mapped_column(
    DataclassJSONBArray(
        ReviewQuestion,
        none_as_null=True,
        reject_unknown=True,
    ),
    nullable=False,
    default=list,
)

review_cycles class-attribute instance-attribute

review_cycles = relationship(
    "ReviewCycle",
    foreign_keys="ReviewCycle.template_id",
    back_populates="template",
    order_by="ReviewCycle.created_at",
)

self_review_cycles class-attribute instance-attribute

self_review_cycles = relationship(
    "ReviewCycle",
    foreign_keys="ReviewCycle.self_review_template_id",
    back_populates="self_review_template",
    order_by="ReviewCycle.created_at",
)

apps.eu_tools.alan_home.models.review_cycle.ReviewCycle

Bases: BaseModel

__tablename__ class-attribute instance-attribute

__tablename__ = 'review_cycle'

description class-attribute instance-attribute

description = mapped_column(Text, nullable=False)

duration class-attribute instance-attribute

duration = mapped_column(Integer, nullable=False)

is_active

is_active()
Source code in apps/eu_tools/alan_home/models/review_cycle.py
@hybrid_property
def is_active(self) -> bool:
    return self.is_cancelled is False and (
        self.start_date is None
        or self.start_date
        <= date.today()
        < self.start_date + timedelta(weeks=self.duration)
    )

is_cancelled class-attribute instance-attribute

is_cancelled = mapped_column(
    Boolean,
    nullable=False,
    default=False,
    server_default=false(),
)

max_reviewers class-attribute instance-attribute

max_reviewers = mapped_column(Integer, nullable=False)

min_reviewers class-attribute instance-attribute

min_reviewers = mapped_column(Integer, nullable=False)

name class-attribute instance-attribute

name = mapped_column(Text, nullable=False)

opt_in_eligible_alaners class-attribute instance-attribute

opt_in_eligible_alaners = relationship(
    "Alaner",
    secondary="smart_group_membership",
    primaryjoin="and_(ReviewCycle.opt_in_eligible_smart_group_id == SmartGroupMembership.smart_group_id, SmartGroupMembership.is_active_on(func.coalesce(ReviewCycle.start_date, func.current_date())))",
    secondaryjoin="SmartGroupMembership.alaner_id == Alaner.id",
    uselist=True,
    order_by="Alaner.slack_handle",
    viewonly=True,
)

opt_in_eligible_smart_group class-attribute instance-attribute

opt_in_eligible_smart_group = relationship(
    SmartGroup,
    foreign_keys=[opt_in_eligible_smart_group_id],
    uselist=False,
)

opt_in_eligible_smart_group_id class-attribute instance-attribute

opt_in_eligible_smart_group_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    index=True,
    nullable=True,
)

opted_in_alaners class-attribute instance-attribute

opted_in_alaners = relationship(
    "Alaner",
    secondary="review_cycle_opt_in",
    primaryjoin="ReviewCycle.id == ReviewCycleOptIn.review_cycle_id",
    secondaryjoin="ReviewCycleOptIn.alaner_id == Alaner.id",
    uselist=True,
    viewonly=True,
    order_by="Alaner.created_at.asc()",
)

peer_review_duration class-attribute instance-attribute

peer_review_duration = mapped_column(
    Integer, nullable=False
)

reviewees_smart_group class-attribute instance-attribute

reviewees_smart_group = relationship(
    SmartGroup,
    foreign_keys=[reviewees_smart_group_id],
    uselist=False,
)

reviewees_smart_group_id class-attribute instance-attribute

reviewees_smart_group_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    index=True,
    nullable=True,
)

reviewers_selection_guidelines class-attribute instance-attribute

reviewers_selection_guidelines = mapped_column(
    Text, nullable=True
)

reviews class-attribute instance-attribute

reviews = relationship(
    "Review",
    back_populates="review_cycle",
    uselist=True,
    order_by="Review.created_at",
)

self_review_duration class-attribute instance-attribute

self_review_duration = mapped_column(
    Integer, nullable=False
)

self_review_template class-attribute instance-attribute

self_review_template = relationship(
    ReviewTemplate,
    foreign_keys=[self_review_template_id],
    back_populates="self_review_cycles",
    uselist=False,
)

self_review_template_id class-attribute instance-attribute

self_review_template_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    index=True,
    nullable=True,
)

self_review_template_ids class-attribute instance-attribute

self_review_template_ids = mapped_column(
    ARRAY(UUID(as_uuid=True)),
    nullable=False,
    default=list,
    server_default="{}",
)

smart_reviewees class-attribute instance-attribute

smart_reviewees = relationship(
    "Alaner",
    secondary="smart_group_membership",
    primaryjoin="and_(ReviewCycle.reviewees_smart_group_id == SmartGroupMembership.smart_group_id, SmartGroupMembership.is_active_on(func.coalesce(ReviewCycle.start_date, func.current_date())))",
    secondaryjoin="SmartGroupMembership.alaner_id == Alaner.id",
    uselist=True,
    order_by="Alaner.slack_handle",
    back_populates="review_cycles",
    viewonly=True,
)

start_date class-attribute instance-attribute

start_date = mapped_column(Date)

template class-attribute instance-attribute

template = relationship(
    ReviewTemplate,
    foreign_keys=[template_id],
    back_populates="review_cycles",
    uselist=False,
)

template_id class-attribute instance-attribute

template_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    index=True,
    nullable=False,
)

template_ids class-attribute instance-attribute

template_ids = mapped_column(
    ARRAY(UUID(as_uuid=True)),
    nullable=False,
    default=list,
    server_default="{}",
)

type class-attribute instance-attribute

type = mapped_column(
    AlanBaseEnumTypeDecorator(ReviewCycleType),
    nullable=False,
)

validates_type class-attribute instance-attribute

validates_type = create_validator('type')

apps.eu_tools.alan_home.models.review_cycle.ReviewCycleType

Bases: AlanBaseEnum

fixed_date class-attribute instance-attribute

fixed_date = 'fixed_date'

lifecycle class-attribute instance-attribute

lifecycle = 'lifecycle'

apps.eu_tools.alan_home.models.review.Review

Bases: BaseModel, Historizable

__tablename__ class-attribute instance-attribute

__tablename__ = 'review'

alaner class-attribute instance-attribute

alaner = relationship(
    "Alaner",
    primaryjoin="foreign(Review.alaner_id) == Alaner.id",
    back_populates="reviews",
    uselist=False,
)

alaner_id class-attribute instance-attribute

alaner_id = mapped_column(
    Integer, nullable=False, index=True
)

feedback_summary class-attribute instance-attribute

feedback_summary = relationship(
    "ReviewFeedbackSummary",
    back_populates="review",
    uselist=False,
)

filename property

filename

max_reviewers class-attribute instance-attribute

max_reviewers = mapped_column(Integer, nullable=False)

min_reviewers class-attribute instance-attribute

min_reviewers = mapped_column(Integer, nullable=False)

name class-attribute instance-attribute

name = mapped_column(Text, nullable=False)

questions class-attribute instance-attribute

questions = mapped_column(
    DataclassJSONBArray(
        ReviewQuestion,
        none_as_null=True,
        reject_unknown=True,
    ),
    nullable=False,
    default=list,
)

released_on class-attribute instance-attribute

released_on = mapped_column(Date, nullable=True)

review_cycle class-attribute instance-attribute

review_cycle = relationship(
    ReviewCycle,
    foreign_keys=[review_cycle_id],
    back_populates="reviews",
    uselist=False,
)

review_cycle_id class-attribute instance-attribute

review_cycle_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=True,
    index=True,
)

review_submissions class-attribute instance-attribute

review_submissions = relationship(
    "ReviewSubmission",
    back_populates="review",
    uselist=True,
    order_by="ReviewSubmission.created_at",
)

self_review_questions class-attribute instance-attribute

self_review_questions = mapped_column(
    DataclassJSONBArray(
        ReviewQuestion,
        none_as_null=True,
        reject_unknown=True,
    ),
    nullable=False,
    default=list,
)

status class-attribute instance-attribute

status = mapped_column(
    AlanBaseEnumTypeDecorator(ReviewStatus),
    nullable=False,
    index=True,
)

type class-attribute instance-attribute

type = mapped_column(
    AlanBaseEnumTypeDecorator(ReviewType),
    nullable=False,
    index=True,
)

validates_status class-attribute instance-attribute

validates_status = create_validator('status')

validates_type class-attribute instance-attribute

validates_type = create_validator('type')

apps.eu_tools.alan_home.models.review.ReviewStatus

Bases: AlanBaseEnum

completed class-attribute instance-attribute

completed = 'completed'

ready_for_reviewers_approval class-attribute instance-attribute

ready_for_reviewers_approval = (
    "ready_for_reviewers_approval"
)

ready_for_reviewers_selection class-attribute instance-attribute

ready_for_reviewers_selection = (
    "ready_for_reviewers_selection"
)

ready_for_self_review class-attribute instance-attribute

ready_for_self_review = 'ready_for_self_review'

ready_for_submissions class-attribute instance-attribute

ready_for_submissions = 'ready_for_submissions'

apps.eu_tools.alan_home.models.review.ReviewType

Bases: AlanBaseEnum

ad_hoc class-attribute instance-attribute

ad_hoc = 'ad_hoc'

feedback class-attribute instance-attribute

feedback = 'feedback'

flash_feedback class-attribute instance-attribute

flash_feedback = 'flash_feedback'

review_cycle class-attribute instance-attribute

review_cycle = 'review_cycle'

trajectory_feedback class-attribute instance-attribute

trajectory_feedback = 'trajectory_feedback'

apps.eu_tools.alan_home.models.review_submission.ReviewSubmission

Bases: BaseModel, Historizable

__tablename__ class-attribute instance-attribute

__tablename__ = 'review_submission'

answers class-attribute instance-attribute

answers = mapped_column_with_privacy(
    PrimitiveJSONBMap(none_as_null=True),
    privacy_properties=PrivacyProperties(
        category=other,
        kay_strategy=NoneIfOtherColumnIsNone("released_on"),
        turing_strategy=NoneIfOtherColumnIsNone(
            "released_on"
        ),
    ),
)

approved_at class-attribute instance-attribute

approved_at = mapped_column(DateTime, nullable=True)

approver class-attribute instance-attribute

approver = relationship(
    "Alaner",
    primaryjoin="foreign(ReviewSubmission.approver_id) == Alaner.id",
    uselist=False,
)

approver_id class-attribute instance-attribute

approver_id = mapped_column(
    Integer, nullable=True, index=True
)

google_calendar_event_id class-attribute instance-attribute

google_calendar_event_id = mapped_column(
    Text, nullable=True
)
permalink = mapped_column(Text)

published_at class-attribute instance-attribute

published_at = mapped_column(DateTime)

rejected_at class-attribute instance-attribute

rejected_at = mapped_column(DateTime)

rejecter class-attribute instance-attribute

rejecter = relationship(
    "Alaner",
    primaryjoin="foreign(ReviewSubmission.rejecter_id) == Alaner.id",
    uselist=False,
)

rejecter_id class-attribute instance-attribute

rejecter_id = mapped_column(
    Integer, nullable=True, index=True
)

rejection_reason class-attribute instance-attribute

rejection_reason = mapped_column(Text)

released_on class-attribute instance-attribute

released_on = mapped_column(Date, nullable=True)

review class-attribute instance-attribute

review = relationship(
    Review,
    foreign_keys=[review_id],
    back_populates="review_submissions",
    uselist=False,
)

review_id class-attribute instance-attribute

review_id = mapped_column(
    UUID(as_uuid=True),
    ForeignKey(id),
    nullable=False,
    index=True,
)

reviewer class-attribute instance-attribute

reviewer = relationship(
    "Alaner",
    primaryjoin="foreign(ReviewSubmission.reviewer_id) == Alaner.id",
    uselist=False,
)

reviewer_id class-attribute instance-attribute

reviewer_id = mapped_column(
    Integer, nullable=False, index=True
)

type class-attribute instance-attribute

type = mapped_column(
    AlanBaseEnumTypeDecorator(ReviewSubmissionType),
    nullable=False,
)

validates_type class-attribute instance-attribute

validates_type = create_validator()

apps.eu_tools.alan_home.models.review_submission.ReviewSubmissionType

Bases: AlanBaseEnum

coach class-attribute instance-attribute

coach = 'coach'

coachee class-attribute instance-attribute

coachee = 'coachee'

peer class-attribute instance-attribute

peer = 'peer'

self class-attribute instance-attribute

self = 'self'

apps.eu_tools.alan_home.business_logic.reviews.InvalidReviewDateExtension

Bases: Exception

Raised when review date extension inputs are invalid.

apps.eu_tools.alan_home.business_logic.reviews.compute_review_status

compute_review_status(review)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def compute_review_status(review: Review) -> ReviewStatus:
    num_submissions = len(
        [rs for rs in review.review_submissions if rs.type != ReviewSubmissionType.self]
    )
    if num_submissions < review.min_reviewers:
        return ReviewStatus.ready_for_reviewers_selection

    # Ignore rejected submissions for approval/publication flow decisions
    non_rejected_submissions = [
        rs
        for rs in review.review_submissions
        if rs.rejected_at is None and rs.type != ReviewSubmissionType.self
    ]

    approved_non_rejected = [
        rs
        for rs in non_rejected_submissions
        if rs.approved_at is not None and rs.type != ReviewSubmissionType.self
    ]

    # If we don't have enough approved (among non-rejected), still waiting for reviewers' approval
    if len(approved_non_rejected) < review.min_reviewers:
        return ReviewStatus.ready_for_reviewers_approval

    if review.type in (ReviewType.review_cycle, ReviewType.ad_hoc) and not any(
        review_submission.published_at is not None
        and review_submission.type == ReviewSubmissionType.self
        for review_submission in review.review_submissions
    ):
        return ReviewStatus.ready_for_self_review

    # Completion should consider only non-rejected submissions
    if non_rejected_submissions and all(
        review_submission.published_at is not None
        for review_submission in non_rejected_submissions
    ):
        return ReviewStatus.completed

    return ReviewStatus.ready_for_submissions

apps.eu_tools.alan_home.business_logic.reviews.create_cycle_review_for_alaner

create_cycle_review_for_alaner(alaner, review_cycle)

Build a cycle review (+ self submission) for an alaner and add it to the session.

Mirrors the review construction done by ReviewsProvider.provision_user — timing config, intern handling, naming, peer/self questions — but performs no gating (active window / smart-group membership), publishes no event, and does not commit. The caller owns the transaction.

Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def create_cycle_review_for_alaner(
    alaner: "Alaner", review_cycle: ReviewCycle
) -> Review:
    """Build a cycle review (+ self submission) for an alaner and add it to the session.

    Mirrors the review construction done by ``ReviewsProvider.provision_user`` —
    timing config, intern handling, naming, peer/self questions — but performs no
    gating (active window / smart-group membership), publishes no event, and does
    not commit. The caller owns the transaction.
    """
    from apps.eu_tools.alan_home.commands.smart_groups.reviews import (
        ReviewTimingConfig,
        next_monday,
    )
    from apps.eu_tools.alan_home.commands.smart_groups.smart_group_definition import (
        get_all_smart_group_definitions,
    )
    from shared.helpers.time.utc import utcnow, utctoday

    reviewees_smart_group = review_cycle.reviewees_smart_group
    sg_def = (
        get_all_smart_group_definitions().get(
            reviewees_smart_group.smart_group_definition_id
        )
        if reviewees_smart_group is not None
        else None
    )
    timing: ReviewTimingConfig | None = getattr(sg_def, "review_timing", None)

    contract = (alaner.contract_type or "").lower()
    is_intern = contract.endswith(("intern", "apprentice"))

    if timing and timing.weeks_after_start_intern is not None and is_intern:
        base_date = alaner.start_date + timedelta(weeks=timing.weeks_after_start_intern)
    elif review_cycle.start_date is not None:
        base_date = review_cycle.start_date
    else:
        base_date = utctoday()

    if timing and timing.snap_start_to_monday:
        start = next_monday(base_date)
    else:
        start = base_date

    self_review_days = (
        timing.self_review_days
        if timing and timing.self_review_days is not None
        else review_cycle.self_review_duration
    )
    self_review_end = start + timedelta(days=self_review_days)

    if timing and timing.total_review_days is not None:
        end = start + timedelta(days=timing.total_review_days)
    else:
        end = start + timedelta(
            days=review_cycle.peer_review_duration + review_cycle.self_review_duration
        )

    smart_group_name = reviewees_smart_group.name if reviewees_smart_group else None
    if smart_group_name == "Newcomers 5m review" and is_intern:
        review_name = "Newcomer's review - 4 months"
    elif smart_group_name == "Newcomers 2.5m review" and is_intern:
        review_name = "Newcomer's review - 2 months"
    else:
        review_name = review_cycle.name

    alaner_community_id = alaner.community.id if alaner.community else None
    review = Review(
        alaner_id=alaner.id,
        name=review_name,
        review_cycle_id=review_cycle.id,
        type=ReviewType.review_cycle,
        questions=get_peer_questions_for_cycle(
            review_cycle.id, alaner.role, alaner_community_id
        ),
        self_review_questions=get_self_review_questions_for_cycle(
            review_cycle.id, alaner.role, alaner_community_id
        ),
        start_date=start,
        end_date=end,
        min_reviewers=review_cycle.min_reviewers,
        max_reviewers=review_cycle.max_reviewers,
        status=ReviewStatus.ready_for_reviewers_selection,
    )
    current_session.add(review)
    current_session.flush()
    current_session.add(
        ReviewSubmission(
            reviewer_id=review.alaner_id,
            review_id=review.id,
            type=ReviewSubmissionType.self,
            start_date=review.start_date,
            end_date=self_review_end,
            approver_id=review.alaner_id,
            approved_at=utcnow(),
        )
    )
    return review

apps.eu_tools.alan_home.business_logic.reviews.export_to_pdf

export_to_pdf(review_id)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def export_to_pdf(review_id: UUID) -> BytesIO:
    review = current_session.get_one(
        Review,
        review_id,
        options=(
            joinedload(Review.review_cycle),
            joinedload(Review.review_submissions, ReviewSubmission.reviewer),
        ),
    )

    consolidated_submissions: list[str] = []
    submissions_order = {
        ReviewSubmissionType.self: 0,
        ReviewSubmissionType.coach: 1,
        ReviewSubmissionType.coachee: 2,
        ReviewSubmissionType.peer: 3,
    }

    for review_submission in sorted(
        review.review_submissions,
        key=lambda submission: (
            submissions_order.get(submission.type, 999),
            submission.reviewer.last_name,
        ),
    ):
        if (
            review_submission.rejected_at is not None
            or review_submission.published_at is None
        ):
            continue

        reviewer = review_submission.reviewer
        consolidated_submission = markdown.markdown(
            f"## {reviewer.full_name} ({reviewer.role} - level {reviewer.level}) - {review_submission.type.value} review\n\n"
        )

        if review_submission.answers is None:
            consolidated_submission += markdown.markdown(
                "*Review was not submitted*\n\n"
            )

        else:
            questions = (
                review.self_review_questions
                if review_submission.type == ReviewSubmissionType.self
                else review.questions
            )
            for question in questions:
                if question.type == ReviewQuestionType.callout:
                    continue

                if review.type != ReviewType.feedback:
                    consolidated_submission += markdown.markdown(
                        f"### {question.title}\n\n"
                    )

                answer_value = mandatory(review_submission.answers).get(
                    question.id, "N/A"
                )
                if question.type == ReviewQuestionType.select:
                    values_to_labels = {
                        v.value: v.label
                        for v in cast(
                            "list[OptionValue]",
                            mandatory(question.options).get("values", []),
                        )
                    }
                    answer_label = values_to_labels.get(
                        cast("str", answer_value), cast("str", answer_value)
                    )
                    consolidated_submission += markdown.markdown(f"{answer_label}\n\n")

                else:
                    consolidated_submission += _shift_headings(
                        markdown.markdown(f"{answer_value}\n\n"), shift=3
                    )

        consolidated_submissions.append(consolidated_submission)

    title = markdown.markdown(
        f"# {mandatory(review.end_date).isoformat()} - {review.review_cycle.name if review.review_cycle else 'Continuous feedback'}"
    )
    html_content = "<hr>".join(consolidated_submissions)

    html = f"""
            <!DOCTYPE html>
            <html>
            <head>
                <meta charset="utf-8">
                <style>
                    @page {{
                        size: A4;
                        margin: 2.5cm 2cm 2.5cm 2cm;  /* top, right, bottom, left */
                    }}
                </style>
            </head>
            <body>
                {title}
                {html_content}
            </body>
            </html>
            """

    pdf_bytes = BytesIO()
    PDFWriter.html2pdf(html=html, path_or_bytes=pdf_bytes, css_path="css/reviews.css")
    pdf_bytes.seek(0)
    return pdf_bytes

apps.eu_tools.alan_home.business_logic.reviews.extend_review_dates

extend_review_dates(
    review_id, peer_review_end_date, self_review_end_date
)

Extend review deadlines with phase-specific reopen behavior.

Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def extend_review_dates(
    review_id: UUID,
    peer_review_end_date: date,
    self_review_end_date: date,
) -> Review:
    """Extend review deadlines with phase-specific reopen behavior."""
    from apps.eu_tools.alan_home.business_logic.review_calendar import (
        sync_review_submission_calendar_event,
    )
    from apps.eu_tools.alan_home.events.reviews import (
        ReviewCycleMessageSchema,
        ReviewExtended,
        ReviewMessageSchema,
    )
    from shared.messaging.broker import get_message_broker

    if peer_review_end_date < self_review_end_date:
        raise InvalidReviewDateExtension(
            "Peer review end date must be before self review end date"
        )

    review = get_or_raise_missing_resource(
        Review,
        review_id,
        options=[joinedload(Review.review_submissions)],
    )

    previous_start_date = review.start_date
    previous_end_date = review.end_date
    was_released = review.released_on is not None
    self_submission = _get_self_submission(review)
    previous_self_end_date = self_submission.end_date if self_submission else None

    self_phase_extended = _is_date_extended(
        previous_self_end_date, self_review_end_date
    )
    peer_phase_extended = _is_date_extended(review.end_date, peer_review_end_date)

    dates_changed = self_phase_extended or peer_phase_extended

    if self_phase_extended and self_submission is not None:
        self_submission.end_date = self_review_end_date
        if self_submission.published_at is not None:
            self_submission.published_at = None
            self_submission.released_on = None

    if peer_phase_extended:
        review.end_date = peer_review_end_date
        for submission in review.review_submissions:
            if submission.type != ReviewSubmissionType.self:
                submission.end_date = peer_review_end_date

    review.status = compute_review_status(review)
    _clear_release_if_no_longer_completed(
        review, force=peer_phase_extended and was_released
    )

    if not dates_changed:
        return review

    current_session.commit()
    for review_submission in review.review_submissions:
        sync_review_submission_calendar_event(review_submission)
    current_session.commit()

    get_message_broker().publish(
        ReviewExtended(
            review=ReviewMessageSchema.from_model(review),
            review_cycle=ReviewCycleMessageSchema.from_model(review.review_cycle),
            previous_start_date=mandatory(previous_start_date),
            previous_end_date=mandatory(previous_end_date),
            start_date=mandatory(review.start_date),
            end_date=mandatory(review.end_date),
        )
    )
    return review

apps.eu_tools.alan_home.business_logic.reviews.get_peer_questions_for_cycle

get_peer_questions_for_cycle(
    cycle_id, alaner_role=None, alaner_community_id=None
)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def get_peer_questions_for_cycle(
    cycle_id: UUID,
    alaner_role: str | None = None,
    alaner_community_id: UUID | None = None,
) -> list[ReviewQuestion]:
    return [
        question
        for template in get_templates_for_cycle(cycle_id)
        if _template_matches_alaner(template, alaner_role, alaner_community_id)
        for question in template.questions
    ]

apps.eu_tools.alan_home.business_logic.reviews.get_self_review_questions_for_cycle

get_self_review_questions_for_cycle(
    cycle_id, alaner_role=None, alaner_community_id=None
)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def get_self_review_questions_for_cycle(
    cycle_id: UUID,
    alaner_role: str | None = None,
    alaner_community_id: UUID | None = None,
) -> list[ReviewQuestion]:
    return [
        question
        for template in get_self_review_templates_for_cycle(cycle_id)
        if _template_matches_alaner(template, alaner_role, alaner_community_id)
        for question in template.questions
    ]

apps.eu_tools.alan_home.business_logic.reviews.get_self_review_templates_for_cycle

get_self_review_templates_for_cycle(cycle_id)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def get_self_review_templates_for_cycle(cycle_id: UUID) -> list[ReviewTemplate]:
    cycle = get_or_raise_missing_resource(ReviewCycle, cycle_id)
    return _ordered_templates(cycle.self_review_template_ids)

apps.eu_tools.alan_home.business_logic.reviews.get_templates_for_cycle

get_templates_for_cycle(cycle_id)
Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def get_templates_for_cycle(cycle_id: UUID) -> list[ReviewTemplate]:
    cycle = get_or_raise_missing_resource(ReviewCycle, cycle_id)
    return _ordered_templates(cycle.template_ids)

apps.eu_tools.alan_home.business_logic.reviews.patch_review_dates

patch_review_dates(review_id, start_date, end_date)

Update review start/end dates, delegating end-date extension to extend_review_dates.

Source code in apps/eu_tools/alan_home/business_logic/reviews.py
def patch_review_dates(
    review_id: UUID,
    start_date: date | None,
    end_date: date | None,
) -> Review:
    """Update review start/end dates, delegating end-date extension to extend_review_dates."""
    from apps.eu_tools.alan_home.business_logic.review_calendar import (
        sync_review_submission_calendar_event,
    )
    from apps.eu_tools.alan_home.events.reviews import (
        ReviewCycleMessageSchema,
        ReviewExtended,
        ReviewMessageSchema,
    )
    from shared.messaging.broker import get_message_broker

    review = get_or_raise_missing_resource(
        Review,
        review_id,
        options=[joinedload(Review.review_submissions)],
    )

    if end_date is not None and review.end_date != end_date:
        self_submission = _get_self_submission(review)
        self_review_end_date = (
            self_submission.end_date
            if self_submission is not None and self_submission.end_date is not None
            else end_date
        )
        if start_date is not None and review.start_date != start_date:
            review.start_date = start_date
            for review_submission in review.review_submissions:
                if review_submission.published_at is None:
                    review_submission.start_date = start_date
            current_session.flush()
        return extend_review_dates(
            review_id,
            peer_review_end_date=end_date,
            self_review_end_date=self_review_end_date,
        )

    if (start_date is not None and review.start_date != start_date) or (
        end_date is not None and review.end_date != end_date
    ):
        previous_start_date = review.start_date
        previous_end_date = review.end_date

        if start_date is not None:
            review.start_date = start_date
        if end_date is not None:
            review.end_date = end_date
        for review_submission in review.review_submissions:
            if review_submission.published_at is None:
                if start_date is not None:
                    review_submission.start_date = start_date
                if end_date is not None:
                    review_submission.end_date = end_date

        review.status = compute_review_status(review)
        _clear_release_if_no_longer_completed(review)

        current_session.commit()
        for review_submission in review.review_submissions:
            sync_review_submission_calendar_event(review_submission)
        current_session.commit()
        get_message_broker().publish(
            ReviewExtended(
                review=ReviewMessageSchema.from_model(review),
                review_cycle=ReviewCycleMessageSchema.from_model(review.review_cycle),
                previous_start_date=mandatory(previous_start_date),
                previous_end_date=mandatory(previous_end_date),
                start_date=mandatory(review.start_date),
                end_date=mandatory(review.end_date),
            )
        )

    return review

apps.eu_tools.alan_home.events.reviews.ReviewCreated dataclass

ReviewCreated(review, review_cycle)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

apps.eu_tools.alan_home.events.reviews.ReviewCycleMessageSchema dataclass

ReviewCycleMessageSchema(id, name, description, type)

description instance-attribute

description

from_model classmethod

from_model(review_cycle)
Source code in apps/eu_tools/alan_home/events/reviews.py
@classmethod
def from_model(
    cls, review_cycle: ReviewCycle | None
) -> "ReviewCycleMessageSchema | None":
    if review_cycle is None:
        return None
    return cls(
        id=review_cycle.id,
        name=review_cycle.name,
        description=review_cycle.description,
        type=review_cycle.type,
    )

id instance-attribute

id

name instance-attribute

name

type instance-attribute

type

apps.eu_tools.alan_home.events.reviews.ReviewExtended dataclass

ReviewExtended(
    review,
    review_cycle,
    previous_start_date,
    previous_end_date,
    start_date,
    end_date,
)

Bases: WebhookMessage

end_date instance-attribute

end_date

previous_end_date instance-attribute

previous_end_date

previous_start_date instance-attribute

previous_start_date

review instance-attribute

review

review_cycle instance-attribute

review_cycle

start_date instance-attribute

start_date

apps.eu_tools.alan_home.events.reviews.ReviewMessageSchema dataclass

ReviewMessageSchema(
    id, alaner, type, status, start_date, end_date
)

alaner instance-attribute

alaner

end_date instance-attribute

end_date

from_model classmethod

from_model(review)
Source code in apps/eu_tools/alan_home/events/reviews.py
@classmethod
def from_model(cls, review: Review) -> "ReviewMessageSchema":
    return cls(
        id=review.id,
        alaner=AlanerMessageSchema.from_model(review.alaner),
        type=review.type,
        status=review.status,
        start_date=review.start_date,
        end_date=review.end_date,
    )

id instance-attribute

id

start_date instance-attribute

start_date

status instance-attribute

status

type instance-attribute

type

apps.eu_tools.alan_home.events.reviews.ReviewStatusUpdated dataclass

ReviewStatusUpdated(review, review_cycle)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

apps.eu_tools.alan_home.events.reviews.ReviewSubmissionApproved dataclass

ReviewSubmissionApproved(
    review, review_submission, review_cycle
)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

review_submission instance-attribute

review_submission

apps.eu_tools.alan_home.events.reviews.ReviewSubmissionCreated dataclass

ReviewSubmissionCreated(
    review, review_submission, review_cycle
)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

review_submission instance-attribute

review_submission

apps.eu_tools.alan_home.events.reviews.ReviewSubmissionMessageSchema dataclass

ReviewSubmissionMessageSchema(
    id,
    reviewer,
    type,
    approver=None,
    approved_at=None,
    rejecter=None,
    rejected_at=None,
    published_at=None,
)

approved_at class-attribute instance-attribute

approved_at = None

approver class-attribute instance-attribute

approver = None

from_model classmethod

from_model(review_submission)
Source code in apps/eu_tools/alan_home/events/reviews.py
@classmethod
def from_model(
    cls, review_submission: ReviewSubmission
) -> "ReviewSubmissionMessageSchema":
    return cls(
        id=review_submission.id,
        reviewer=AlanerMessageSchema.from_model(review_submission.reviewer),
        type=review_submission.type,
        approver=AlanerMessageSchema.from_model(review_submission.approver),
        approved_at=review_submission.approved_at,
        rejecter=AlanerMessageSchema.from_model(review_submission.rejecter),
        rejected_at=review_submission.rejected_at,
        published_at=review_submission.published_at,
    )

id instance-attribute

id

published_at class-attribute instance-attribute

published_at = None

rejected_at class-attribute instance-attribute

rejected_at = None

rejecter class-attribute instance-attribute

rejecter = None

reviewer instance-attribute

reviewer

type instance-attribute

type

apps.eu_tools.alan_home.events.reviews.ReviewSubmissionPublished dataclass

ReviewSubmissionPublished(
    review, review_submission, review_cycle
)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

review_submission instance-attribute

review_submission

apps.eu_tools.alan_home.events.reviews.ReviewSubmissionRejected dataclass

ReviewSubmissionRejected(
    review, review_submission, review_cycle
)

Bases: WebhookMessage

review instance-attribute

review

review_cycle instance-attribute

review_cycle

review_submission instance-attribute

review_submission