Class: PlanMyStuff::BaseProject

Inherits:
ApplicationRecord show all
Defined in:
lib/plan_my_stuff/base_project.rb

Overview

Shared base for GitHub Projects V2 wrappers. Holds attribute definitions, generic find/list/update machinery, hydration, and instance helpers. Concrete subclasses (Project, TestingProject) add their own create! behavior and optional filtering on find/list.

Not instantiated directly. find and list dispatch via build_detail/ build_summary to the correct concrete subclass based on the metadata kind field in the project readme.

Direct Known Subclasses

Project, TestingProject

Constant Summary collapse

MAX_AUTO_PAGINATE_ITEMS =
500
ITEMS_PER_PAGE =
100

Instance Attribute Summary

Attributes inherited from ApplicationRecord

#github_response

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

#destroyed?, #initialize, #new_record?, #persisted?, read_field

Constructor Details

This class inherits a constructor from PlanMyStuff::ApplicationRecord

Class Method Details

.clone!(source_number:, title:) ⇒ PlanMyStuff::BaseProject

Clones an existing GitHub Project into the configured organization. The copy inherits all custom fields and board layout from the source. Returns the newly created project via find, dispatched to the correct concrete subclass (Project or TestingProject) based on the cloned readme.

Parameters:

  • source_number (Integer)

    project number of the project to copy from

  • title (String)

    title for the new project

Returns:



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/plan_my_stuff/base_project.rb', line 148

def clone!(source_number:, title:)
  org = PlanMyStuff.configuration.organization
  org_id = resolve_org_id(org)
  source_project_id = resolve_project_id(org, source_number)

  data = PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::COPY_PROJECT_V2,
    variables: {
      input: {
        ownerId: org_id,
        projectId: source_project_id,
        title: title,
        includeDraftIssues: false,
      },
    },
  )

  new_number = data.dig(:copyProjectV2, :projectV2, :number)
  PlanMyStuff::BaseProject.find(new_number)
end

.find(number, paginate: :auto, cursor: nil) ⇒ PlanMyStuff::BaseProject

Generic find - returns whichever concrete project type is at the given number, dispatching on metadata kind. Subclasses may override to apply filtering (e.g. Project raises for testing projects by default).

Parameters:

  • number (Integer)
  • paginate (Symbol) (defaults to: :auto)

    :auto (default) or :cursor

  • cursor (String, nil) (defaults to: nil)

    pagination cursor for :cursor mode

Returns:



58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/plan_my_stuff/base_project.rb', line 58

def find(number, paginate: :auto, cursor: nil)
  org = PlanMyStuff.configuration.organization

  case paginate
  when :auto
    find_auto_paginated(org, number)
  when :cursor
    find_with_cursor(org, number, cursor: cursor)
  else
    raise(ArgumentError, "Unknown paginate mode: #{paginate.inspect}. Use :auto or :cursor")
  end
end

.listArray<PlanMyStuff::BaseProject>

Generic list - returns all projects in the configured organization, each dispatched to its concrete type (Project or TestingProject). Subclasses may override to apply filtering.

Returns:



77
78
79
80
81
82
83
84
85
86
# File 'lib/plan_my_stuff/base_project.rb', line 77

def list
  org = PlanMyStuff.configuration.organization
  data = PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::LIST_PROJECTS,
    variables: { org: org },
  )

  nodes = data.dig(:organization, :projectsV2, :nodes) || []
  nodes.map { |node| build_summary(node) }
end

.resolve_default_project_number(project_number) ⇒ Integer

Resolves a project number, falling back to config.default_project_number.

Parameters:

  • project_number (Integer, nil)

Returns:

  • (Integer)


175
176
177
178
179
180
# File 'lib/plan_my_stuff/base_project.rb', line 175

def resolve_default_project_number(project_number)
  return project_number if project_number.present?

  PlanMyStuff.configuration.default_project_number ||
    raise(ArgumentError, 'project_number is required when config.default_project_number is not set')
end

.update!(project_number:, title: nil, readme: nil, description: nil, metadata: nil) ⇒ PlanMyStuff::BaseProject

Updates an existing project.

Parameters:

  • project_number (Integer)
  • title (String, nil) (defaults to: nil)
  • readme (String, nil) (defaults to: nil)

    user-visible readme content (metadata preserved)

  • description (String, nil) (defaults to: nil)

    project short description

  • metadata (Hash, nil) (defaults to: nil)

    custom fields to merge into existing metadata

Returns:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/plan_my_stuff/base_project.rb', line 98

def update!(project_number:, title: nil, readme: nil, description: nil, metadata: nil)
  org = PlanMyStuff.configuration.organization
  project_id = resolve_project_id(org, project_number)

  update_input = { projectId: project_id }
  update_input[:title] = title unless title.nil?
  update_input[:shortDescription] = description unless description.nil?

  if .present? || !readme.nil?
    current = find(project_number)
    parsed = PlanMyStuff::MetadataParser.parse(current.raw_readme)
     = parsed[:metadata]

    if .present?
      # Seed with fresh metadata when project has no existing PMS metadata
      if [:schema_version].blank?
         = PlanMyStuff::ProjectMetadata.build(user: nil).to_h
      end

      merged_custom_fields = ([:custom_fields] || {}).merge([:custom_fields] || {})
       = .merge()
      [:custom_fields] = merged_custom_fields
      PlanMyStuff::CustomFields.new(
        PlanMyStuff.configuration.custom_fields_for(:project),
        merged_custom_fields,
      ).validate!
    end

    body = readme.nil? ? parsed[:body] : readme
    update_input[:readme] = PlanMyStuff::MetadataParser.serialize(, body)
  end

  PlanMyStuff.client.graphql(
    PlanMyStuff::GraphQL::Queries::UPDATE_PROJECT,
    variables: { input: update_input },
  )

  find(project_number)
end

Instance Method Details

#closedBoolean?

Returns whether the project is closed.

Returns:

  • (Boolean, nil)

    whether the project is closed



21
# File 'lib/plan_my_stuff/base_project.rb', line 21

attribute :closed

#descriptionString?

Returns project short description (from shortDescription).

Returns:

  • (String, nil)

    project short description (from shortDescription)



27
# File 'lib/plan_my_stuff/base_project.rb', line 27

attribute :description, :string

#fieldsArray<Hash>

Returns all field definitions.

Returns:

  • (Array<Hash>)

    all field definitions



39
# File 'lib/plan_my_stuff/base_project.rb', line 39

attribute :fields, default: -> { [] }

#has_next_pageBoolean?

Returns whether more pages exist (only in cursor mode).

Returns:

  • (Boolean, nil)

    whether more pages exist (only in cursor mode)



45
# File 'lib/plan_my_stuff/base_project.rb', line 45

attribute :has_next_page

#idString?

Returns GitHub node ID.

Returns:

  • (String, nil)

    GitHub node ID



17
# File 'lib/plan_my_stuff/base_project.rb', line 17

attribute :id, :string

#itemsArray<PlanMyStuff::ProjectItem>

Returns project items.

Returns:



41
# File 'lib/plan_my_stuff/base_project.rb', line 41

attribute :items, default: -> { [] }

#metadataPlanMyStuff::BaseProjectMetadata

Returns parsed metadata (empty when no PMS metadata present).

Returns:



23
# File 'lib/plan_my_stuff/base_project.rb', line 23

attribute :metadata, default: -> { PlanMyStuff::ProjectMetadata.new }

#next_cursorString?

Returns cursor for next page (only in cursor mode).

Returns:

  • (String, nil)

    cursor for next page (only in cursor mode)



43
# File 'lib/plan_my_stuff/base_project.rb', line 43

attribute :next_cursor, :string

#numberInteger?

Returns project number.

Returns:

  • (Integer, nil)

    project number



19
# File 'lib/plan_my_stuff/base_project.rb', line 19

attribute :number, :integer

#pms_project?Boolean

Returns:

  • (Boolean)


418
419
420
# File 'lib/plan_my_stuff/base_project.rb', line 418

def pms_project?
  .schema_version.present?
end

#raw_readmeString?

Returns full readme as stored on GitHub.

Returns:

  • (String, nil)

    full readme as stored on GitHub



25
# File 'lib/plan_my_stuff/base_project.rb', line 25

attribute :raw_readme, :string

#readmeString?

Returns user-visible readme content (without metadata comment).

Returns:

  • (String, nil)

    user-visible readme content (without metadata comment)



35
# File 'lib/plan_my_stuff/base_project.rb', line 35

attribute :readme, :string

#reloadself

Re-fetches this project from GitHub and updates all local attributes.

Returns:

  • (self)


473
474
475
476
477
# File 'lib/plan_my_stuff/base_project.rb', line 473

def reload
  fresh = self.class.find(number)
  hydrate_from_project(fresh)
  self
end

#save!self

Persists the project. Creates if new, updates if persisted.

Returns:

  • (self)

Raises:



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
# File 'lib/plan_my_stuff/base_project.rb', line 428

def save!
  if new_record?
    created = self.class.create!(
      title: title,
      readme: readme || '',
      description: description,
      **,
    )
    hydrate_from_project(created)
  else
    update!(
      title: title,
      readme: readme,
      description: description,
      metadata: ,
    )
  end

  self
end

#status_fieldHash

Returns the Status single-select field definition.

Returns:

  • (Hash)

    with :id and :options keys

Raises:



409
410
411
412
413
414
415
# File 'lib/plan_my_stuff/base_project.rb', line 409

def status_field
  field = fields.find { |f| f[:name] == 'Status' && f[:options] }

  raise(APIError, "No 'Status' field found on project ##{number}") unless field

  field
end

#statusesArray<Hash>

Returns status options (name:).

Returns:

  • (Array<Hash>)

    status options (name:)



37
# File 'lib/plan_my_stuff/base_project.rb', line 37

attribute :statuses, default: -> { [] }

#titleString?

Returns project title.

Returns:

  • (String, nil)

    project title



31
# File 'lib/plan_my_stuff/base_project.rb', line 31

attribute :title, :string

#update!(**attrs) ⇒ self

Updates this project on GitHub. Raises StaleObjectError if the remote has been modified since this instance was loaded.

Parameters:

  • attrs (Hash)

    attributes to update (title:, readme:, description:, metadata:)

Returns:

  • (self)

Raises:



458
459
460
461
462
463
464
465
466
467
# File 'lib/plan_my_stuff/base_project.rb', line 458

def update!(**attrs)
  raise_if_stale!

  self.class.update!(
    project_number: number,
    **attrs,
  )

  reload
end

#updated_atTime?

Returns GitHub’s updatedAt timestamp.

Returns:

  • (Time, nil)

    GitHub’s updatedAt timestamp



29
# File 'lib/plan_my_stuff/base_project.rb', line 29

attribute :updated_at

#urlString?

Returns project URL.

Returns:

  • (String, nil)

    project URL



33
# File 'lib/plan_my_stuff/base_project.rb', line 33

attribute :url, :string