Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/bundler/rails-html-sanitizer-1.6.1
Browse files Browse the repository at this point in the history
  • Loading branch information
fermion authored Dec 14, 2024
2 parents fd9ce3c + 48f9984 commit 1240e4b
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 4 deletions.
33 changes: 33 additions & 0 deletions app/graphql/mutations/delete_project.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Mutations
class DeleteProject < BaseMutation
description "Delete a project."

# arguments passed to the `resolve` method
argument :project_id, ID, required: true,
description: "The ID of the project to delete. The project must meet the delete-ability requirements: no assignments, or all assignments having no actual hours recorded."

# return type from the mutation
type Types::StaffPlan::ProjectType

def resolve(project_id:)
project = context[:current_company].projects.find(project_id)

project.destroy

if project.errors.any?
project.errors.each do |error|
context.add_error(
GraphQL::ExecutionError.new(
error.full_message,
extensions: {
attribute: error.attribute.to_s,
}
)
)
end
end

project
end
end
end
16 changes: 16 additions & 0 deletions app/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,17 @@ type Mutation {
"""
assignmentId: ID!
): Assignment!

"""
Delete a project.
"""
deleteProject(
"""
The ID of the project to delete. The project must meet the delete-ability
requirements: no assignments, or all assignments having no actual hours recorded.
"""
projectId: ID!
): Project!
setCurrentCompany(
"""
The ID of the company to set as the current user's current_company_id. User must be an active member of the company.
Expand Down Expand Up @@ -344,6 +355,11 @@ type Mutation {

type Project {
assignments: [Assignment!]!

"""
Whether the assignment can be deleted
"""
canBeDeleted: Boolean!
client: Client!
cost: Float!
createdAt: ISO8601DateTime!
Expand Down
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ class MutationType < Types::BaseObject
field :upsert_project, mutation: Mutations::UpsertProject
field :upsert_client, mutation: Mutations::UpsertClient
field :delete_assignment, mutation: Mutations::DeleteAssignment
field :delete_project, mutation: Mutations::DeleteProject
end
end
6 changes: 5 additions & 1 deletion app/graphql/types/staff_plan/project_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class ProjectType < Types::BaseObject
field :ends_on, GraphQL::Types::ISO8601Date, null: true
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false

field :can_be_deleted, Boolean, null: false, description: 'Whether the assignment can be deleted'
field :assignments, [Types::StaffPlan::AssignmentType], null: false

def assigmnents
Expand All @@ -35,6 +35,10 @@ def users
def work_weeks
object.work_weeks
end

def can_be_deleted
object.can_be_deleted?
end
end
end
end
17 changes: 17 additions & 0 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Project < ApplicationRecord
validates :rate_type, inclusion: { in: VALID_RATE_TYPES }, allow_blank: true
validates :hours, numericality: { greater_than_or_equal_to: 0 }, allow_blank: true

before_destroy :ensure_project_is_destroyable, prepend: true

def confirmed?
status == CONFIRMED
end
Expand All @@ -54,4 +56,19 @@ def cancelled?
def completed?
status == COMPLETED
end

def can_be_deleted?
assignments.empty? ||
WorkWeek.where(assignment: assignments).where.not(actual_hours: 0).empty?
end

private

# a project is destroyable if it has no assignments OR if its assignments have no actual hours recorded
def ensure_project_is_destroyable
return if can_be_deleted?

errors.add(:base, "Cannot delete a project that has assignments with hours recorded. Try archiving the project instead.")
false
end
end
2 changes: 1 addition & 1 deletion app/models/work_week.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ def actual_hours_allowed?
return false if year_zero? || cweek_zero?

today = Date.today
today.year < year || (today.year == year && today.cweek >= cweek)
today.year > year || (today.year == year && today.cweek >= cweek)
end
end
10 changes: 8 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ services:
- 5432:5432
volumes:
- data:/var/lib/postgresql/data
env_file: .env
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_USERNAME=${DB_USERNAME}
container_name: staffplan-postgres

redis:
Expand All @@ -14,7 +17,10 @@ services:
- 6379:6379
volumes:
- redis:/var/lib/redis/data
env_file: .env
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=${DB_HOST}
- DB_USERNAME=${DB_USERNAME}
container_name: staffplan-redis

# localstack:
Expand Down
74 changes: 74 additions & 0 deletions spec/graphql/mutations/delete_project_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe Mutations::DeleteProject do

context "resolve" do
it 'resolves to an error when a project cannot be destroyed' do
query_string = <<-GRAPHQL
mutation($projectId: ID!) {
deleteProject(projectId: $projectId) {
id
status
}
}
GRAPHQL

project = create(:project)
company = project.client.company
user = create(:membership, company:).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 5)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: company
},
variables: {
projectId: project.id
}
)

expect(result["errors"].first["message"]).to eq("Cannot delete a project that has assignments with hours recorded. Try archiving the project instead.")
expect(project.reload.persisted?).to eq(true)
post_result = result["data"]["deleteProject"]
expect(post_result["id"]).to eq(project.id.to_s)
expect(post_result["status"]).to eq(Project::CONFIRMED)
end

it "destroys a project that can be destroyed" do
query_string = <<-GRAPHQL
mutation($projectId: ID!) {
deleteProject(projectId: $projectId) {
id
}
}
GRAPHQL

project = create(:project)
company = project.client.company
user = create(:membership, company:).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 0)

result = StaffplanReduxSchema.execute(
query_string,
context: {
current_user: user,
current_company: company
},
variables: {
projectId: project.id
}
)

post_result = result["data"]["deleteProject"]
expect(result["errors"]).to be_nil
expect(post_result["id"]).to eq(project.id.to_s)
expect { project.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
51 changes: 51 additions & 0 deletions spec/models/project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,57 @@
it { should have_many(:users).through(:assignments) }
end

describe 'callbacks' do
describe 'before_destroy: ensure_project_is_destroyable' do
it 'allows project deletion if there are no assignments' do
project = create(:project)

expect { project.destroy }.to change { Project.count }.by(-1)
expect { project.reload }.to raise_error(ActiveRecord::RecordNotFound)
end

it 'allows project deletion if there are assignments with assignees, but no actual hours' do
project = create(:project)
user = create(:membership, company: project.client.company).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 0)

expect { project.destroy }.to change { Project.count }.by(-1)
expect { project.reload }.to raise_error(ActiveRecord::RecordNotFound)
end

it 'blocks project deletion if there are assignments with actual hours' do
project = create(:project)
user = create(:membership, company: project.client.company).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 8)

expect { project.destroy }.to_not change { Project.count }
expect(project.errors.full_messages).to include("Cannot delete a project that has assignments with hours recorded. Try archiving the project instead.")
end
end
end

describe 'can_be_deleted?' do
it 'allows deletion if there are no assignments with actual hours recorded' do
project = create(:project)
user = create(:membership, company: project.client.company).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 0)

expect(project.can_be_deleted?).to be_truthy
end

it 'blocks deletion if there are assignments with actual hours recorded' do
project = create(:project)
user = create(:membership, company: project.client.company).user
assignment = create(:assignment, project: project, user: user)
create(:work_week, assignment: assignment, actual_hours: 8)

expect(project.can_be_deleted?).to be_falsey
end
end

describe "#confirmed?" do
it "returns true if the status is confirmed" do
project = build(:project, status: "confirmed")
Expand Down

0 comments on commit 1240e4b

Please sign in to comment.