-
Notifications
You must be signed in to change notification settings - Fork 7
Card Config: Migrations
The Aperta project consists of many custom Task
models that are
responsible for managing the collection of specific subsets of
information from the user. There is an ongoing effort to convert these
legacy Tasks
into CustomCardTasks
which can manage the generic
collection of data for any subset of questions within the system. Having
a data-driven CustomCardTask
allows Aperta administrators to easily
create their own set of questions and modify them on the fly.
This document concerns itself with migrating a specific custom Task
model into a generic CustomCardTask
. For matter of efficiency, it is
written in a sequential manner, expecting the developer to complete each
section before moving to the next.
The following items will be part of every legacy task migration:
- Generation of XML to represent a new
CustomCardTask
- Migration and re-parenting existing answer data to the
Answer
table - Converting hardcoded handlebars template questions to
CardContent
- Retirement of existing legacy task code
The first and most important step in creating a migration is to conduct proper analysis of the existing legacy task. Each task migration is a unique snowflake and needs proper analysis to determine the level of complexity.
The legacy tasks in Aperta have been developed over a multi-year history with varying degrees of business rule complexity which can result in wide inconsistencies in how each legacy task works and where it stores answer data.
During the analysis phase, the following questions should be answered to determine the complexity of the existing Task and how much prior work can be leveraged to ease the burden of conversion:
- Where are answers currently stored?
- on the legacy
Task
model? - in the
Answer
table? - as attributes of a related model?
- on the legacy
- What types of questions are being asked?
- do questions show / hide based on other answers?
- do blocks of questions "repeat" (like they do for Funders or Authors)?
- are any questions not currently supported by the existing types of card-content?
- Does the card interact with models other than
Answer
? - Are there any highly custom, non-standard display elements on the
card?
- Examples: dynamic footer that changes based on current answers, select boxes that populate with external data such as liquid templates
- Does the card show different states of the same data (example: when a block of author questions are marked complete, it shows a summary of this content instead of disabled form fields)?
- Are the question answers being exported to external systems?
(billing log / salesforce / APEX / etc.)
- If so, be sure to look closely to see if there is custom logic around how that data is being used.
- Are there unique permissions being applied to the questions being
asked?
- Examples:
UploadManuscriptTask
andPaperReviewerTask
- Typically, you will find this in the corresponding controller
- Examples:
- Does the card require custom version snapshot views?
The more times that yes is answered to the above questions, the higher the complexity. If you encounter a high level of complexity, it is important to raise this as a concern to a product owner. This might be an opportunity to simplify how the card works or streamline the business process.
Questions 2 and 4 in the analysis phase will determine if a new custom card component will need to be created. Creation of a new component is out of scope for this document, but an example can be found in the commit to add financial-disclosure-summary card content.
Future steps in this document assume that all custom card content types already exist to build the new card.
Once the analysis phase is complete and the developer has a solid understanding as to how the legacy card works, the new XML can be created using the administrative interface.
- The preview can be manually compared to the legacy task by using the admin card preview tab.
- Existing question text and
idents
can be obtained from the legacy task's handlebar templates. - The existing data migrator (covered later in this document) expects
that the new XML
idents
match the existing ones. In other words, the developer should not add or subtractidents
as part of the initial migration. This should be a feature to feature migration. Any changes are expected to be introduced as a newCardVersion
.
Once the XML has been created, it should be saved in this specific
folder for loading by the card migrator:
lib/custom_card/configurations/xml_content
A CustomCard::Configurations
class is responsible for determining the
name of the custom card being created, whether it should be given a
state of published
(immediate available for use) or whether it should
be loaded in just a development environment. It also determines the role
name permissions that should be granted to the card.
The configuration class looks for an associated XML file in
lib/custom_card/configurations/xml_content
.
An example configuration file looks like this:
lib/custom_card/configurations/early_version.rb
A service class exists to generate the permission role names. To use it, load a recent production data export into your development environment and then from the Rails console, Listing permissions shows the commands to use.
> CustomCard::PermissionInquiry.new(legacy_class_name: "MyLegacyTask").legacy_permissions
> { view: ["Academic Editor", "Billing Staff", "Collaborator", "Cover Editor"], edit: ["Collaborator", "Cover Editor", "Creator"]}
The output can be copied directly into the configuration class.
There may be times where it's impractical to convert the legacy Task
subclass into a simple CustomCardTask
. The Task may be tied to
specific code elsewhere in the system, or the existing Roles and
Permissions scheme may depend on that Task in a way that makes removing
it prohibitive. A few extra steps are necessary to integrate the
existing Task subclass.
- The
task_class
method on aCustomCard::Configurations
class must be defined as something other than "CustomCardTask" as it is inCustomCard::Configurations::Base
. This example from the PR for migrating the Upload Manuscript Task shows the full changes.
- A corresponding
CardTaskType
record will need to be added to the database if you're not going to use theCustomCard::Migrator
service (see Section *Simple Data Migration for details). There's an example of this forTahiStandardTasks::UploadManuscriptTask
in the production database.
- The existing handlebars template for the task subclass will have to
be changed to match the contents of the
custom-card-task.hbs
file. For example, theupload-manuscript-task.hbs
file has the same contents of thecustom-card-task.hbs
file. This is necessary based on how the system renders templates for a givenTask
type.
The data migration is the process of creating the correct tree of card
content and moving existing answers to appropriate Answer
records. It
executes within the context of an ActiveRecord::Migration
so that it
is wrapped within a transaction and can be rolled back if something
fails.
There are three steps to the data migration:
- Loading the newly created
Card
using theCardConfiguration
class created earlier. - Calling the migrator to migrate existing data to the new
Card
. - Updating the existing
data.yml
for seed data.
Based on the analysis conducted earlier, the work to complete the migration will be either simple or custom.
A simple data migration can be used if all current answers are already
stored in the Answer
table. In this case, re-parent the existing
Answers
to the newly created CardContent
.
A CustomCard::Migrator
service class already exists for this purpose,
so the full data migration would look like Listing
migrator .
class MigratePublishingRelatedQuestionsTaskToCustomCard < ActiveRecord::Migration
def up
# load custom card into the system
CustomCard::Loader.all
# migrate legacy task to custom card
migrator = CustomCard::Migrator
.new(legacy_task_klass_name: "TahiStandardTasks::PublishingRelatedQuestionsTask",
configuration_class: CustomCard::Configurations::PublishingRelatedQuestions)
migrator.migrate
end
end
A custom migration is necessary if there are repeated blocks of content (Funders, Authors, etc.) or if answers are stored in multiple places within the application.
In this case, a custom migrator will need to be created.
The following tasks must be performed by the custom migrator:
- Convert Legacy Task to a
CustomCardTask
- Re-parent all answers to the appropriate newly created
CardContent
- Update the existing
TaskTemplate
to point to the newCard
- Destroy the legacy
JournalTaskType
record - Create=Repetitions= for any repeated content blocks
An example of a custom migrator meeting all these requirements can be found with the FinancialDisclosureMigrator.
Figure cardmigrationerd shows the models involved in custom cards. Here is a brief overview of these models:
-
CardContent
- each record represents a distinct piece of content that is displayed on aCard
. Often, this manifests itself as a question, but there are other types ofCardContent
that are not answerable.CardContent
is created by converting XML nodes found on the card admin screen. -
CardVersion
- when any changes are made to a card, a newCardVersion
will be created. This allows versioning of a tree ofCardContent
. -
Card
- manages a tree ofCardContent
. -
Answer
- holds the answer to a specific piece ofCardContent
. In the case of a repeated block of questions (such asFunder
), theAnswer
record will also be tied to aRepetition
. -
Repetition
- represents a tree ofCardContent
that can be repeated on aCard
.
Conceptionally, a Repetition
is a block of repeated content with its
answers. For instance, three Funders
would correspond to three
separate Repetition
records.
Concretely, a Repetition
is a tree model where the root of the tree
corresponds to a CardContent
of type repeat. This repeat
CardContent
is a tree that contains all the content that might be
repeated. For instance, the tree might have a child content that asks
for the funder's name. It is useful to think of this CardContent
tree
as the canonical representation of content that might be repeated on a
card. If a user had added three different funders on a paper, there
would still only be one repeat CardContent
.
Although the root of the Repetition
content corresponds to a single
CardContent
of type repeat, it itself is also a tree and will
contain children that each correspond to the canonical piece of
CardContent
. For example, if a user has added three funders, the
root of the Repetition
will have a relationship to the repeat
CardContent
. It will also have a child that corresponds a different
CardContent
that is responsible for asking for a funder name. In other
words, each node of the Repetition
tree corresponds to the a node in
the canonical CardContent
repeat tree. They mirror each other.
The children of a Repetition
each correspond to a piece of
CardContent
so that it can obtain question text, an Answer
record so
that it knows the user's answer, and an owner
(typically the
CustomCardTask
). This is how the answers for two different funders
are kept separately from each other.
Finally, a Repetition
uses acts_as_list
to maintain positional
order. This is necessary for being able to reorder a list of content in
a sensible, performant manner.
In Listing carderd , the rails-erd gem is used to generate the diagram in Figure cardmigrationerd
rake erd only="Card,CardVersion,CardContent,CardTaskType,Answer,Task,Repetition,EntityAttribute" filename="cardmigrationerd" filetype="pdf" title=false inheritancee=true indirect=true
quadrant-grid.png (image/png)
cardmigrationerd.pdf (application/pdf)