Beispiel #1
0
def create_estimation_session_task() -> None:
    """
    Create a new empty estimation session for every cell.

    An empty estimation session does not contain any issues or participants. The current user is added as the scrum
    master of the session, as without this permission it wouldn't be possible to make any future modifications.

    WARNING
    Creating a session without issues causes some chaos in Jira, as the `/session/async/{sessionId}/rounds/` endpoint
    returns HTTP 500 in such case. It does not break other API calls, so operations like updating, closing, and deleting
    the session (via the API) work correctly. It makes the session unusable via the browser by breaking two views:
        - estimation,
        - configuration.
    Therefore, the decision is to avoid adding the participants to the session until there are issues that can be added
    too. Assuming that the sessions are fully automated, and don't require any manual interventions in the beginning,
    this should not cause any troubles.
    """
    with connect_to_jira() as conn:
        session_name = get_next_poker_session_name(conn)
        for cell in get_cells(conn):
            if not settings.DEBUG:  # We really don't want to trigger this in the dev environment.
                conn.create_poker_session(
                    board_id=cell.board_id,
                    name=session_name,
                    issues=[],
                    participants=[],
                    scrum_masters=[conn.myself()['key']],
                    send_invitations=False,
                )
Beispiel #2
0
def get_overcommitted_users(conn: CustomJira) -> dict[str, list[User]]:
    """
    Retrieve users that have negative time left for the next sprint (i.e. are overcommitted).

    :param conn: Jira connection.
    :return: List of overcommitted users.
    """
    cells = get_cells(conn)
    result = dict[str, list[User]]()

    # Generate dashboards in parallel.
    with ThreadPool(processes=settings.MULTIPROCESSING_POOL_SIZE) as pool:
        results = [pool.apply_async(Dashboard, (cell.board_id, conn)) for cell in cells]
        dashboards = [result.get(settings.MULTIPROCESSING_TIMEOUT) for result in results]

    for dashboard in dashboards:
        overcommitted_users: list[User] = []
        for row in dashboard.rows:
            # Check if the user has `raw` value - this excludes artificial users, like "Unassigned".
            if row.remaining_time < 0 and row.user.raw:
                overcommitted_users.append(row.user)

        # Ignore cells without overcommitted users.
        if overcommitted_users:
            result[dashboard.cell.name] = overcommitted_users

    return result
Beispiel #3
0
def create_next_sprint_task(board_id: int) -> int:
    """A task for creating the next sprint for the specified cell."""
    with connect_to_jira() as conn:
        cells = get_cells(conn)
        cell = next(c for c in cells if c.board_id == board_id)
        sprints: List[Sprint] = get_sprints(conn, cell.board_id)

        next_sprint = create_next_sprint(conn, sprints, cell.key, board_id)
    return get_sprint_number(next_sprint)
Beispiel #4
0
def test_get_cells():
    # noinspection PyTypeChecker
    cells = get_cells(MockJiraConnection())
    assert len(cells) == 2
    assert cells[0].name == 'Test1'
    assert cells[0].board_id == 1
    assert cells[0].key == 'T1'
    assert cells[1].name == 'Test2'
    assert cells[1].board_id == 2
    assert cells[1].key == 'T2'
Beispiel #5
0
def move_estimates_to_tickets_task() -> None:
    """
    Applies the average vote results from the closed estimation session to the tickets for every cell.

    If there were no votes for a specific issue, its assignee (or another responsible person) is notified.
    """
    with connect_to_jira() as conn:
        session_name = get_next_poker_session_name(conn)

        for cell in get_cells(conn):
            vote_values = conn.poker_session_vote_values(cell.board_id)
            poker_sessions = conn.poker_sessions(cell.board_id,
                                                 state="CLOSED",
                                                 name=session_name)

            if not poker_sessions and not settings.DEBUG:
                # This can happen if a new cell has been added, then its session was created manually, and it either:
                # - does not have a correct name,
                # - does not have the Jira bot added as its scrum master.
                # noinspection PyUnresolvedReferences
                from sentry_sdk import capture_message

                capture_message(
                    f"Could not find a session called {session_name} in {cell.name}. If this is a new cell, please "
                    f"make sure that an estimation session with this name exists and {settings.JIRA_BOT_USERNAME} "
                    f"has been added as a scrum master there.")

            # Handle applying the results from multiple sessions with the same name (though it should not happen).
            for session in poker_sessions:
                for issue, results in conn.poker_session_results(
                        session.sessionId).items():
                    votes = []
                    for result in results.values():
                        vote = result.get("selectedVote")
                        try:
                            votes.append(float(vote))  # type: ignore
                        except ValueError:
                            # Ignore non-numeric answers.
                            pass

                    try:
                        final_vote = get_poker_session_final_vote(
                            votes, vote_values)
                    except AttributeError:  # No votes.
                        ping_users_on_ticket(
                            conn, conn.issue(issue),
                            settings.SPRINT_ASYNC_POKER_NO_ESTIMATES_MESSAGE)
                    else:
                        if not settings.DEBUG:  # We really don't want to trigger this in the dev environment.
                            conn.update_issue(
                                issue, conn.issue_fields[
                                    settings.JIRA_FIELDS_STORY_POINTS],
                                str(final_vote))
Beispiel #6
0
def close_estimation_session_task() -> None:
    """
    Close all "next-sprint" estimation sessions for every cell.

    The "next-sprint" estimation session needs to match the following criteria:
    - is open,
    - has the name matching the result of the `get_next_poker_session_name` function.
    """
    with connect_to_jira() as conn:
        session_name = get_next_poker_session_name(conn)

        for cell in get_cells(conn):
            poker_sessions = conn.poker_sessions(cell.board_id,
                                                 state="OPEN",
                                                 name=session_name)
            if not settings.DEBUG:  # We really don't want to trigger this in the dev environment.
                # Handle closing multiple sessions with the same name (though it should not happen).
                for session in poker_sessions:
                    conn.close_poker_session(session.sessionId,
                                             send_notifications=True)

    move_estimates_to_tickets_task.delay()
Beispiel #7
0
 def list(self, _request):
     """Lists all available cells."""
     with connect_to_jira() as conn:
         cells = get_cells(conn)
     serializer = CellSerializer(cells, many=True)
     return Response(serializer.data)
Beispiel #8
0
def update_estimation_session_task() -> None:
    """
    Update estimation session's issues and participants.

    If no issues exist for the session, then it will not be updated. The reasoning behind this has been described in the
    `create_estimation_session_task` function.

    This does not override the manual additions to the session - i.e. if an issue or user has been added manually to the
    session, then it will be retained, as it merges available issues and participants with the applied ones.
    However, any removed items (e.g. an issue scheduled for the next sprint, or a user who is a member of the cell)
    will be added back automatically.

    FIXME: This adds all cell members as scrum masters to the session, because at the moment we do not have a way to
           determine whether the member is a part of the core team. We can restrict this in the future, if needed.
    """
    with connect_to_jira() as conn:
        session_name = get_next_poker_session_name(conn)
        issues = get_unestimated_next_sprint_issues(conn)
        # FIXME: This will not work for more than 1000 users. To support it, add `startAt` to handle the pagination.
        all_users = conn.search_users(
            "''",
            maxResults=1000)  # Searching for the "quotes" returns all users.

        for cell in get_cells(conn):
            try:
                poker_session = conn.poker_sessions(cell.board_id,
                                                    state="OPEN",
                                                    name=session_name)[0]
            except IndexError:
                if not settings.DEBUG:
                    # It can happen:
                    # 1. When this runs before the `create_estimation_session_task`, e.g. when the sprint is created
                    #    manually a moment before the full hour.
                    # 2. If a new cell has been added, then its session was created manually, and it either:
                    #    - does not have a correct name,
                    #    - does not have the Jira bot added as its scrum master.
                    # noinspection PyUnresolvedReferences
                    from sentry_sdk import capture_message

                    capture_message(
                        f"Could not find a session called {session_name} in {cell.name}. If you haven't completed the "
                        f"sprint yet, then you should consider adjusting the start time of this task, so it's started "
                        f"only once the new sprint has been started. If this is a new cell, please make sure that an "
                        f"estimation session with this name exists and {settings.JIRA_BOT_USERNAME} has been added as "
                        f"a scrum master there.")
                continue

            # Get IDs of issues belonging only to a specific cell.
            cell_issue_ids = set(issue.id for issue in issues
                                 if issue.key.startswith(cell.key))
            current_issue_ids = set(
                conn.poker_session_results(poker_session.sessionId).keys())
            all_issue_ids = list(current_issue_ids | cell_issue_ids)
            if all_issue_ids:
                # User's `name` and `key` are not always the same.
                members = get_cell_member_names(
                    conn, get_cell_members(conn.quickfilters(cell.board_id)))
                member_keys = set(user.key for user in all_users
                                  if user.displayName in members)

                current_member_keys = set(
                    user.userKey for user in poker_session.participants)
                current_scrum_master_keys = set(
                    user.userKey for user in poker_session.scrumMasters)

                all_member_keys = list(current_member_keys | member_keys)
                all_scrum_master_keys = list(current_scrum_master_keys
                                             | member_keys)

                if not settings.DEBUG:  # We don't want to trigger this in the dev environment.
                    # TODO: Handle 403 response when the bot is not added as a participant.
                    poker_session.update({
                        'issuesIds': all_issue_ids,
                        'participants': all_member_keys,
                        'scrumMasters': all_scrum_master_keys,
                        'sendInvitations':
                        False,  # TODO: This should notify participants in case of any changes.
                    })
Beispiel #9
0
def complete_sprint_task(board_id: int) -> None:
    """
    1. Upload spillovers.
    2. Upload commitments.
    3. Move archived issues out of the active sprint.
    4. Close the active sprint.
    5. Move issues from the closed sprint to the next one.
    6. Open the next sprint.
    7. Create role tickets.
    8. Trigger the `new sprint` webhooks.
    9. Release the sprint completion lock and clear the cache related to end of sprint date.
    """
    with connect_to_jira() as conn:
        cells = get_cells(conn)
        cell = next(c for c in cells if c.board_id == board_id)
        spreadsheet_tasks = [
            upload_spillovers_task.s(cell.board_id, cell.name),
            upload_commitments_task.s(cell.board_id, cell.name),
        ]

        # Run the spreadsheet tasks asynchronously and wait for the results before proceeding with ending the sprint.
        with allow_join_result():
            # FIXME: Use `apply_async`. Currently blocked because of `https://github.com/celery/celery/issues/4925`.
            #   CAUTION: if you change it, ensure that all tasks have finished successfully.
            group(spreadsheet_tasks).apply().join()

        sprints: List[Sprint] = get_sprints(conn, cell.board_id)
        sprints = filter_sprints_by_cell(sprints, cell.key)

        for sprint in sprints:
            if sprint.state == 'active':
                active_sprint = sprint
                break

        next_sprint = get_next_sprint(sprints, active_sprint)

        archived_issues: List[Issue] = conn.search_issues(
            **prepare_jql_query_active_sprint_tickets(
                [
                    'None'
                ],  # We don't need any fields here. The `key` attribute will be sufficient.
                {settings.SPRINT_STATUS_ARCHIVED},
                project=cell.name,
            ),
            maxResults=0,
        )
        archived_issue_keys = [issue.key for issue in archived_issues]

        issues: List[Issue] = conn.search_issues(
            **prepare_jql_query_active_sprint_tickets(
                [
                    'None'
                ],  # We don't need any fields here. The `key` attribute will be sufficient.
                settings.SPRINT_STATUS_ACTIVE
                | {settings.SPRINT_STATUS_DEPLOYED_AND_DELIVERED},
                project=cell.name,
            ),
            maxResults=0,
        )
        issue_keys = [issue.key for issue in issues]

        if not settings.DEBUG:  # We really don't want to trigger this in the dev environment.
            if settings.FEATURE_CELL_ROLES:
                # Raise error if we can't read roles from the handbook
                get_cell_member_roles()

            # Remove archived tickets from the active sprint. Leaving them might interrupt closing the sprint.
            conn.move_to_backlog(archived_issue_keys)

            # Close the active sprint.
            conn.update_sprint(
                active_sprint.id,
                name=active_sprint.name,
                startDate=active_sprint.startDate,
                endDate=active_sprint.endDate,
                state='closed',
            )

            # Move issues to the next sprint from the closed one.
            conn.add_issues_to_sprint(next_sprint.id, issue_keys)

            # Open the next sprint.
            conn.update_sprint(
                next_sprint.id,
                name=next_sprint.name,
                startDate=next_sprint.startDate,
                endDate=next_sprint.endDate,
                state='active',
            )

            # Ensure that the next sprint exists. If it doesn't exist, create it.
            # Get next sprint number for creating role tasks there.
            if future_next_sprint := get_next_sprint(sprints, next_sprint):
                future_next_sprint_number = get_sprint_number(
                    future_next_sprint)
            else:
                future_next_sprint_number = create_next_sprint_task(board_id)
                future_next_sprint = get_next_sprint(sprints, next_sprint)

            cell_dict = {
                'key': cell.key,
                'name': cell.name,
                'board_id': cell.board_id,
            }

            create_role_issues_task.delay(cell_dict, future_next_sprint.id,
                                          future_next_sprint_number)

            trigger_new_sprint_webhooks_task.delay(
                cell.name, next_sprint.name, get_sprint_number(next_sprint),
                board_id)