---
title: Candidate Grouping
tags: Devex
---
# Candidate Groupings
## Intro
- Curated Candidate Lists (aka `CL`)
- Candidate Folders (aka `CF`)
Nowadays CL's and CF's are `Tags`/`Taggins`, the proposal is to split the logic on this two to help maintanability and makign it easier to add features in the future. At the begginig they seamed nearly the same, but we've concluded that it is better to separate them.
So to proceed, we'll need to create 2 new endpoints and deprecate the taggins one:
- `api/curated_lists` (and for the admin?)
- `api/folders` (and for the admin?)
These endpoints will point to the two newly created controllers.
This change comes also with changes in the database level, so new tables will be created for these two new models, and the existing data will be migrated.
## Models
### `app/models/tagging.rb`
```rb
# Table name: ng_taggings
id :integer not null, primary key
tag_type :string(255) default("Folder")
taggable_type :string(255)
created_at :datetime
updated_at :datetime
ng_tag_id :integer
taggable_id :integer
```
#### `app/models/tag.rb`
```rb
# Table name: ng_tags
id :integer not null, primary key
curated :boolean default(FALSE)
description :text
is_favorite :boolean default(FALSE)
name :string(255)
shared :boolean default(TRUE)
taggings_count :integer
created_at :datetime
updated_at :datetime
company_id :integer not null
consultant_id :integer
```
## Questions
- Differences and similarities between CL and CF
> :green_heart: same
> :red_circle: different
| | similarity | Folders | Curated Candidate Lists |
| ------------ | ------------- |------------- | ----------------------- |
| Visibility | :red_circle: | Private | Public |
| Managed by | :red_circle: | Themselves | Devex |
| Lists | :green_heart: | Candidates | Candidates |
| CA Card | :red_circle: | not visible | Visible |
| Association | :green_heart: | many-to-many | many-to-many |
- When searching candidates, RA will need a new filter for CCL's (FE)?
- Candidate API response will split taggins in json into Folders and CCL
- CRUD'S:
- `:create`
- `:update`
- `:delete`
- `:add_candidates_to(Groupable)`
- `:remove_candidates_from(Groupable)`
- Which are the current endpoints?
```rb
POST /public/current_user/taggings(.json)
DELETE /public/current_user/taggings(.json)
GET /public/current_user/taggings(.json)
POST /public/current_user/tags(.json)
GET /public/current_user/tags(.json)
DELETE /public/current_user/tags/:slug(.json)
PATCH /public/current_user/tags/:slug(.json)
GET /public/current_user/tags/:slug(.json)
GET /public/current_user/tags/curated(.json)
GET /public/current_user/tags/editable(.json)
```
- How/where are the CCL going to be managed? a New admin section?
## The BE release plan
1. create a **FeatureFlag** `split_candidate_taggins_into_folders_and_curated_lists`
2. Create **Controllers** that proxy the existing logic.
3. Create or modify new **endpoints**.
4. tackle **`CuratedList`**:
1. Create new **model** `CuratedList`
2. Create "shared logic" **helpers/concers** (`:add_to`, `:remove_from`)
3. Refactor **`CuratedListController`** to use its own logic
4. Rake task to migrate (copy) all `Tag.curated` data to `CuratedList`.
5. FE point to new CuratedList `endpoints`
5. tackle **`Folder`**:
1. Create new **model** `Folder`
3. Refactor **`FoldersController`** to use its own logic
4. Rake task to migrate (copy) all `Tag.folder` (new scope) data to `Folder`.
5. FE point to new Folder `endpoints`
6. Clean up legacy:
1. delete `Tag/Taggin` data
2. remove unused code: models, controllers ...
3. clean FF`split_candidate_taggins_into_folders_and_curated_lists`
## The 'NEW' Model associations
As said above, all model associations must be many-to-many.
Only the relevant association declarations are shown.
### The `CuratedList` Model Proposal
We set a [has_many :through association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association) to set up a many-to-many connection with `Consultant` model as *candidates*.
There's no need to associate with a `Company` or `Recruiter` since only Devexers can manage the lists, and can be done with the existing permission system.
This association indicates that the `CuratedList` model can be matched with zero or more instances of `Consultant` by proceeding through `ConsultantCuratedList` model.
```rb
class CuratedList < ActiveRecord::Base
has_many :consultant_curated_lists
has_many :candidates, through: :consultant_curated_lists, source: :consultant
end
# in between
class ConsultantCuratedList < ActiveRecord::Base
belongs_to :curated_list
belongs_to :consultant
end
class Consultant < ActiveRecord::Base
has_many :consultant_curated_lists
has_many :curated_lists, through: :consultant_curated_lists
end
```
### The `Folder` Model Proposal
Both the Recruiter and candidates come from the same model: `Consultant` but have different roles, to tackle this we can have different approaches:
- option 1: a single join table for both roles.
- option 2: each role has its own join table.
#### Option 1, one join table for Recruiters & Candidates
```rb
class Folder < ActiveRecord::Base
belongs_to :company
belongs_to :consultant
has_many :consultant_folders
has_many :consultants, through: :consultant_folders
has_many :consultant_folder_recruiters, -> { recruiters }, class_name: "ConsultantFolder"
has_many :consultant_folder_candidates, -> { candidates }, class_name: "ConsultantFolder"
has_many :recruiters, through: :consultant_folder_recruiters, source: :consultant
has_many :candidates, through: :consultant_folder_candidates, source: :consultant
def shared?
company.present?
end
def personal?
# favorite folder
consultant.present?
end
end
# in between
class ConsultantFolder < ActiveRecord::Base
# role :string(255)
# is_owner :boolean
belongs_to :folder
belongs_to :consultant
validates :role, inclusion: { in: ["recruiter", "candidate"] }
scope :recruiters, -> { where(role: "recruiter") }
scope :candidates, -> { where(role: "candidate") }
end
class Consultant < ActiveRecord::Base
has_many :consultant_folders
has_many :folders, through: :consultant_folders
end
```
#### Option 2, two join table for each role
```rb
class Folder < ActiveRecord::Base
belongs_to :company
validates :company_id, presence: true
# owner_id :consultant_id, optional: true
has_many :folder_candidates
has_many :candidates, through: :folder_candidates, source: :consultant
def personal?
# favorite folder
consultant.present?
end
end
# in between
class FolderCandidates < ActiveRecord::Base
belongs_to :folder
belongs_to :consultant
end
class Consultant < ActiveRecord::Base
has_many :folder_recruiters
has_many :managed_folders, through: :folder_recruiters
has_many :folder_candidates
has_many :member_folders, through: :folder_candidates
end
```
# Ccl features
- MUST have the option to have CCL as drafts
- other proposal like a pending approval to belong to a CCL?
- May have an owner, but since is managed by 2 persons is not a great deal by now.
# Sprint 5
- [Nuts Development - Agile Board - Jira](https://mydevex.atlassian.net/jira/software/c/projects/NUT/boards/111/backlog?view=detail&selectedIssue=NUT-1361&issueLimit=100)
- [Devex Curated Candidate Lists: How to add profiles to CCLs?](https://docs.google.com/document/d/1_wfhMP6S1TtxGOI8Zr9Yy8AieJVgtTYWygE5uwfJ9PY/edit)
-
# Resources
- [CCL Badges – Figma](https://www.figma.com/file/s7e2sbvV59u8sm13PT9kBw/CCL-Badges?node-id=31%3A52445&t=mgdRY1Uz0XC1Lb1r-0)
Documentation:
- [2.4 The has\_many :through Association - guides.rubyonrails](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
- [Creating a many to many relationship in Rails - Stack Overflow](https://stackoverflow.com/a/55612199)
- [4.3 has_many Association Reference methods - guides.rubyonrails](https://guides.rubyonrails.org/association_basics.html#has-many-association-reference)
- [ActiveRecord::Associations::ClassMethods](https://api.rubyonrails.org/v7.0.4.2/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many)
- https://coderwall.com/p/muvy9w/forget-counter-cache-for-many-to-many-associations-in-rails
- https://www.eyupatis.com/rails-has-many-through-association-with-counter-cache-argument/
- https://gist.github.com/O-I/3a9b57159089239e4003
- Add a privilege: https://github.com/Devex/grape-api/compare/master...agustibr:grape-api:NUT-1360-create-GET_curated_candidate_lists_endpoint?expand=1
- Grape-api AUTHENTICATION: https://github.com/Devex/grape-api/blob/master/AUTHENTICATION.md
# PR
- update workflow: https://github.com/Devex/grape-api/pull/11463
- add CCL model: https://github.com/Devex/grape-api/compare/master...agustibr:grape-api:NUT-1359-create-curated_candidate_list-model?expand=1
----
- https://github.com/Devex/grape-api/compare/master...agustibr:grape-api:NUT-1360-create-GET_curated_candidate_lists_endpoint?expand=1
- https://mydevex.atlassian.net/browse/NUT-1360?atlOrigin=eyJpIjoiMmM2NjExOWJmZWQ3NDE4MGIxNTQ0NjlhMmFhMDU1NTUiLCJwIjoiaiJ9