Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds new oncall service feedback table #3856

Merged
merged 12 commits into from
Oct 11, 2023
Merged
30 changes: 30 additions & 0 deletions src/dispatch/auth/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,33 @@ def has_required_permissions(
participant.individual.email for participant in current_case.participants
]
return current_user.email in participant_emails


class FeedbackDeletePermission(BasePermission):
def has_required_permissions(
self,
request: Request,
) -> bool:
permission = any_permission(
permissions=[
SensitiveProjectActionPermission,
],
request=request,
)
if not permission:
individual_contact_id = request.path_params.get("individual_contact_id", "0")
# "0" is passed if the feedback is anonymous
if individual_contact_id != "0":
pk = PrimaryKeyModel(id=individual_contact_id)
individual_contact = individual_contact_service.get(
db_session=request.state.db, individual_contact_id=pk.id
)

if not individual_contact:
return False

current_user = get_current_user(request=request)
if individual_contact.email == current_user.email:
return True

return permission
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Adds project to the service feedback table and allows hours to be fractional

Revision ID: 064c71206256
Revises: 3538650dc471
Create Date: 2023-10-06 09:53:50.119947

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "064c71206256"
down_revision = "3538650dc471"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("service_feedback", sa.Column("project_id", sa.Integer(), nullable=True))
op.create_foreign_key(None, "service_feedback", "project", ["project_id"], ["id"])
op.alter_column(
"service_feedback", "hours", existing_type=sa.Integer(), type_=sa.Numeric(), nullable=True
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "service_feedback", type_="foreignkey")
op.drop_column("service_feedback", "project_id")
op.alter_column(
"service_feedback", "hours", existing_type=sa.Numeric(), type_=sa.Integer(), nullable=True
)
# ### end Alembic commands ###
19 changes: 8 additions & 11 deletions src/dispatch/feedback/service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from pydantic import Field
from typing import Optional, List

from sqlalchemy import Column, Integer, ForeignKey, DateTime, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import Column, Integer, ForeignKey, DateTime, String, Numeric
from sqlalchemy_utils import TSVectorType
from sqlalchemy.orm import relationship

Expand All @@ -18,16 +17,16 @@
class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base):
# Columns
id = Column(Integer, primary_key=True)
feedback = Column(String)
rating = Column(String)
schedule = Column(String)
hours = Column(Integer)
hours = Column(Numeric(precision=10, scale=2))
shift_start_at = Column(DateTime)
shift_end_at = Column(DateTime)

# Relationships
individual_contact_id = Column(Integer, ForeignKey("individual_contact.id"))
whitdog47 marked this conversation as resolved.
Show resolved Hide resolved
individual = relationship("IndividualContact")

project_id = Column(Integer, ForeignKey("project.id"))
project = relationship("Project")

search_vector = Column(
TSVectorType(
Expand All @@ -37,20 +36,18 @@ class ServiceFeedback(TimeStampMixin, FeedbackMixin, Base):
)
)

@hybrid_property
def project(self):
return self.individual.project
whitdog47 marked this conversation as resolved.
Show resolved Hide resolved


# Pydantic models
class ServiceFeedbackBase(DispatchBase):
feedback: Optional[str] = Field(None, nullable=True)
hours: Optional[int]
hours: Optional[float]
individual: Optional[IndividualContactReadMinimal]
rating: ServiceFeedbackRating = ServiceFeedbackRating.little_effort
schedule: Optional[str]
shift_end_at: Optional[datetime]
shift_start_at: Optional[datetime]
project: Optional[ProjectRead]
created_at: Optional[datetime]


class ServiceFeedbackCreate(ServiceFeedbackBase):
Expand Down
5 changes: 4 additions & 1 deletion src/dispatch/feedback/service/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ def create(*, service_feedback_in: ServiceFeedbackCreate, db_session: Session) -
None if not service_feedback_in.individual else service_feedback_in.individual.id
)

project_id = None if not service_feedback_in.project else service_feedback_in.project.id

service_feedback = ServiceFeedback(
**service_feedback_in.dict(exclude={"individual"}),
**service_feedback_in.dict(exclude={"individual", "project"}),
individual_contact_id=individual_contact_id,
project_id=project_id,
)
db_session.add(service_feedback)
db_session.commit()
Expand Down
39 changes: 37 additions & 2 deletions src/dispatch/feedback/service/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status, Depends

from dispatch.auth.permissions import (
FeedbackDeletePermission,
PermissionsDependency,
)
from dispatch.database.core import DbSession
from dispatch.database.service import search_filter_sort_paginate, CommonParameters
from dispatch.models import PrimaryKey

from .models import ServiceFeedbackPagination
from .models import ServiceFeedbackRead, ServiceFeedbackPagination
from .service import get, delete


router = APIRouter()
Expand All @@ -12,3 +19,31 @@
def get_feedback_entries(commons: CommonParameters):
"""Get all feedback entries, or only those matching a given search term."""
return search_filter_sort_paginate(model="ServiceFeedback", **commons)


@router.get("/{service_feedback_id}", response_model=ServiceFeedbackRead)
def get_feedback(db_session: DbSession, service_feedback_id: PrimaryKey):
"""Get a feedback entry by its id."""
feedback = get(db_session=db_session, service_feedback_id=service_feedback_id)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[{"msg": "A feedback entry with this id does not exist."}],
)
return feedback


@router.delete(
"/{service_feedback_id}/{individual_contact_id}",
response_model=None,
dependencies=[Depends(PermissionsDependency([FeedbackDeletePermission]))],
)
def delete_feedback(db_session: DbSession, service_feedback_id: PrimaryKey):
"""Delete a feedback entry, returning only an HTTP 200 OK if successful."""
feedback = get(db_session=db_session, service_feedback_id=service_feedback_id)
if not feedback:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[{"msg": "A feedback entry with this id does not exist."}],
)
delete(db_session=db_session, service_feedback_id=service_feedback_id)
2 changes: 2 additions & 0 deletions src/dispatch/individual/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class IndividualContact(Base, ContactMixin, ProjectMixin):
external_id = Column(String)

events = relationship("Event", backref="individual")
service_feedback = relationship("ServiceFeedback", backref="individual")

filters = relationship(
"SearchFilter", secondary=assoc_individual_filters, backref="individuals"
)
Expand Down
12 changes: 8 additions & 4 deletions src/dispatch/plugins/dispatch_slack/feedback/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
from dispatch.feedback.incident.models import FeedbackCreate
from dispatch.feedback.service import service as feedback_service
from dispatch.individual import service as individual_service
from dispatch.feedback.service.models import ServiceFeedbackRating
from dispatch.feedback.service.models import ServiceFeedbackRating, ServiceFeedbackCreate
from dispatch.incident import service as incident_service
from dispatch.participant import service as participant_service
from dispatch.feedback.service.reminder import service as reminder_service
from dispatch.plugin import service as plugin_service
from dispatch.project import service as project_service
from dispatch.plugins.dispatch_slack.bolt import app
from dispatch.plugins.dispatch_slack.fields import static_select_block
from dispatch.plugins.dispatch_slack.middleware import (
Expand Down Expand Up @@ -375,8 +376,8 @@ def handle_oncall_shift_feedback_submission_event(
db_session: Session,
form_data: dict,
):
hours = form_data.get(ServiceFeedbackNotificationBlockIds.hours_input, {})
if not hours.isnumeric():
hours = form_data.get(ServiceFeedbackNotificationBlockIds.hours_input, "")
if not hours.replace(".", "", 1).isdigit():
ack_with_error(ack=ack)
return

Expand Down Expand Up @@ -409,14 +410,17 @@ def handle_oncall_shift_feedback_submission_event(
)
)

service_feedback = feedback_service.ServiceFeedbackCreate(
project = project_service.get(db_session=db_session, project_id=project_id)

service_feedback = ServiceFeedbackCreate(
feedback=feedback,
hours=hours,
individual=individual,
rating=ServiceFeedbackRating(rating),
schedule=schedule_id,
shift_end_at=shift_end_at,
shift_start_at=None,
project=project,
)

service_feedback = feedback_service.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export default {
return {}
},
computed: {
...mapFields("feedback", ["dialogs.showRemove"]),
...mapFields("incident_feedback", ["dialogs.showRemove"]),
},

methods: {
...mapActions("feedback", ["remove", "closeRemove"]),
...mapActions("incident_feedback", ["remove", "closeRemove"]),
},
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<delete-dialog />
<v-row no-gutters>
<v-col>
<div class="headline">Feedback</div>
<div class="headline">Incident feedback</div>
</v-col>
<v-col class="text-right">
<table-filter-dialog :projects="defaultUserProjects" />
Expand Down Expand Up @@ -74,10 +74,10 @@
import { mapFields } from "vuex-map-fields"
import { mapActions } from "vuex"

import DeleteDialog from "@/feedback/DeleteDialog.vue"
import DeleteDialog from "@/feedback/incident/DeleteDialog.vue"
import Participant from "@/incident/Participant.vue"
import RouterUtils from "@/router/utils"
import TableFilterDialog from "@/feedback/TableFilterDialog.vue"
import TableFilterDialog from "@/feedback/incident/TableFilterDialog.vue"

export default {
name: "FeedbackTable",
Expand All @@ -104,7 +104,7 @@ export default {
},

computed: {
...mapFields("feedback", [
...mapFields("incident_feedback", [
"table.options.q",
"table.options.page",
"table.options.itemsPerPage",
Expand Down Expand Up @@ -136,7 +136,7 @@ export default {
},

methods: {
...mapActions("feedback", ["getAll", "removeShow"]),
...mapActions("incident_feedback", ["getAll", "removeShow"]),
},

created() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export default {
},

computed: {
...mapFields("feedback", ["table.options.filters.incident", "table.options.filters.project"]),
...mapFields("incident_feedback", [
"table.options.filters.incident",
"table.options.filters.project",
]),

numFilters: function () {
return sum([this.incident.length, this.project.length])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getField, updateField } from "vuex-map-fields"
import { debounce } from "lodash"

import SearchUtils from "@/search/utils"
import FeedbackApi from "@/feedback/api"
import FeedbackApi from "@/feedback/incident/api"

const getDefaultSelectedState = () => {
return {
Expand Down
37 changes: 37 additions & 0 deletions src/dispatch/static/dispatch/src/feedback/service/DeleteDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<template>
<v-dialog v-model="showRemove" persistent max-width="800px">
<v-card>
<v-card-title>
<span class="headline">Delete Feedback?</span>
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap> Are you sure you would like to delete this feedback? </v-layout>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="blue en-1" text @click="closeRemove()"> Cancel </v-btn>
<v-btn color="red en-1" text @click="remove()"> Delete </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script>
import { mapActions } from "vuex"
import { mapFields } from "vuex-map-fields"
export default {
name: "ServiceFeedbackDeleteDialog",
data() {
return {}
},
computed: {
...mapFields("service_feedback", ["dialogs.showRemove"]),
},

methods: {
...mapActions("service_feedback", ["remove", "closeRemove"]),
},
}
</script>
Loading