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

feat: vaa-data module #595

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

feat: vaa-data module #595

wants to merge 2 commits into from

Conversation

kaljarv
Copy link
Contributor

@kaljarv kaljarv commented Aug 2, 2024

WHY:

VAA-Data: the Voting Advice Application data model

The VAA-Data module defines an abstract data model for Voting Advice Applications encompassing (simultaneous) elections; candidates, parties and other entities; the constituencies in which these may be nominated and such nominations; questions or statements posed for the entities and voters to answer; the entities’ answers to these questions.

The module can be used by itself to build an easily traversable, hierarchical model of these data, but it is also designed to work seamlessly with the vaa-matching and vaa-filters modules as well as the OpenVAA voting advice application front- and backends.

TODO

We have to be able to use a key to point to a choice in the question options. The value for the option may be different from the key but should optimally default to the key.

interface Answer {
  key: string | number;
  info?: string;
}

interface Choice {
  key: Answer['key'];
  value: number;
  label: string;
}

Split the diagram into:

  1. Class inheritance
  2. Root, children and link hierarchy
  3. Combined

Add tests that check that Entity implements HasMatchableAnswers from vaa-data.

Figure out a way to define typing for customData.

Move question algorithms to separate functions

Main use cases in the front end

// vaa-data
Question implements FilterableQuestion
MatchableQuestion implements MatchableQuestion
// vaa-filters
Entity implements FilterableEntity, HasAnswers
Nomination implements WrappedFilterableEntity
// Frontend calls
DataRoot.elections
DataRoot.getQuestions({e: Election, c: Constituency}): Array<{category: QuestionCategory; questions: Array<Question>}>
Election.constituencies
Constituency.nominations
Constituency.questionCategories
Constituency.questions // Or should there be smth like EffectiveQuestionCategory
Nomination.nominee<Candidate>
Nomination.nominator<Party>
Entity.answers: AnswerDict // implement HasMatchableAnswers
Candidate.party // member of
Candidate.name
Candidate.getAnswer<TValue>(q: Question<TValue>, emptyAsUndef?: boolean): TValue | undefined
Candidate.formatAnswer(q: Question): string
QuestionCategory.questions
QuestionCategory.getQuestions(c: Constituency)
MatchableQuestion.normalizedDimensions?: number // implement MatchableQuestion
MatchableQuestion.normalizeValue(value: unknown): MatchingSpaceCoordinate | MatchingSpaceCoordinate[] // implement MatchableQuestion

File organisation

  • index.ts: provides public exports
  • src/
    • internal.ts: provides all exports used internally, ordered such that the module can be built without errors
    • core/: contains the abstract base classes Updatable, DataObject, NamedObject and atomic data types and some interfaces
    • objects/:
      • constituency/: Constituency and ConstituencyGroup
      • election/: Election
      • entities/: the abstract Entity base class and its descendants: Candidate, Party, Faction, Alliance
      • nomination/: the Nomination class which connects Entities to Constituencies
      • questions/: Question and QuestionCategory as well as subclasses of the former
    • root/: DataRoot
    • utils/: utilities used internally
  • tests/: unit tests

See boxes in the class diagram to see in which folder the classes are located.

The DataObjectData subtype for each class using such is defined in the accompanying type.ts file, e.g. ElectionData is defined in election.type.ts.

General structure

Data root

The data forms a hierarchy with the DataRoot object as its root. Each object in the hierarchy is contained as a child of its parent.

Object data

Each DataObject is associated with a matching DataObjectData type, which is used to provide the object with JSON-serializable data.

The data passed to the objects is accessed via getters mandated by the DataAccessor<TData> interface. The getters always provide a sensible default for the values of the same data type. Thus, for example, getters for optional string data properties always return an empty string ('').

The data is expected to be immutable and no setters are provided for the properties.

Providing data

All parent objects expose methods like provideFooData(data: Array<FooData>), which are used to build their child collections. These collections are stored internally as Collections or MappedCollections but are always returned as Collections, i.e. Arrays, by the public getters.

Candidates, for example, are stored as a Map<Id, Candidate> in DataRoot’s children but publicly accessed by DataRoot.candidates: Array<Candidate> or individually by DataRoot.getCandidate(id: Id): Candidate | undefined.

Children and linked objects

For maximal integrity, all objects have a single parent. If an object may have multiple references to it, it is stored in the DataRoot and non-parents must access it using Id-based getters. For example, in case of a nominated Candidate in a Nomination:

  • The Id of the Candidate is stored in NominationData.nomineeId
  • Nomination.nominee is a getter which internally calls this.root.getCandidate(this.data.nomineeId)

Updatable and subscribictions

All objects, including DataRoot, inherit from the abstract Updatable class, which allows others to subscribe to an onUpdate event triggered whenever the object’s data or children change.

Classes

This diagram is preliminary and does not contain all class properties.

Data tree

---
title: VAAData class inheritance and data tree
---
classDiagram
  direction TD

  %% CLASS DEFINITIONS

  %% namespace core {
%% 
  %%   class CanUpdate{
  %%     <<Interface>>
  %%     Mandates methods for updates and subscribing to changes in this object
  %%     %% CanUpdate parent
  %%     %% onUpdate(boolean propagate) void
  %%   }
%% 
  %%   class Updatable {
  %%     <<Abstract>>
  %%     Provides methods for updates and subscribing to changes in this object
  %%     Defines parent and children property templates
  %%     %% #Record~string, MaybeCollection~ children
  %%     %% #int numTransactions
  %%     %% #Array~UpdateHandler~ subscriptions
  %%     %% CanUpdate|null parent
  %%     %% update(Function transaction, boolean propagate = false) void
  %%     %% subscribe(UpdateHandler handler) Unsubscriber
  %%     %% unsubscribe(UpdateHandler handler) int
  %%     %% onUpdate(boolean propagate) void
  %%     %% reset() void
  %%   }
%% 
  %%   class DataAccessor~DataObjectData~ {
  %%     <<Interface>>
  %%     Mandates getters for all properties of DataObjectData
  %%     All subclasses implement this for their own data type
  %%   }
%% 
%% 
  %%   class DataObject {
  %%     <<Abstract>>
  %%     Implements the DataAccessor interface
  %%     Provides access to DataRoot
  %%     %% #DataObjectData data
  %%     %% DataRoot root
  %%     %% findAncestor(Function test) CanUpdate?
  %%   }
%% 
  %%   class NamedObject {
  %%     <<Abstract>>
  %%     Mandates an id and provides getter for name.
  %%   }
%% 
  %% }

  namespace root {
    class DataRoot {
      Collects all data objects
      Provides id-based getters for global objects
      %% getConstituencyDefinition(Id id): ConstituencyDefinition?
      %% getCandidate(Id id): Candidate?
      %% getParty(Id id): Candidate?
      %% getQuestions(MaybeArray~Election~ election, MaybeArray~Constituency~ constituency)
    }
  }


  namespace objects_election {
    class Election {
      Defines the name of the Election and possible dates
      Owns Nominations via ConstituencyGroups and Constituencies
    }
  }

  namespace objects_constituency {
    class ConstituencyGroup {
      Collects the groups of Constituencies used in an Election.
    }

    class ConstituencyDefinition {
      Defines the area or other criteria for a constituency.
      May be shared by multiple Constituencies.
      %% Array~Constituency~? get subConstituencies
    }

    class Constituency {
      Owns Nominations.
      Connects a ConstituencyDefinition to an Election via ConstituencyGroup.
      Provides getters for all properties of ConstituencyDefinition.
      %% ConstituencyDefinition constituencyDefinition
      %% #Array~Question~ get questions
    }
  }

  namespace objects_entities {
    class Entity {
      <<Abstract>>
      Any Entity standing in an Election.
      May have Answers.
    }

    class Candidate {
      A person that can be nominated.
    }

    class Party {
      An association of Persons, usually a party but may also be a constituency association or other electoral list.
      %% Array~Candidate~ get members
      %% Array~Faction~ get factions
    }

    class Faction {
      A subgroup of a Party, used in ’ley de lemas’ electoral systems.
      %% Array~Candidate~ get members
    }

    class Alliance {
      An electoral alliance of Parties.
      %% Array~Party~ get members
    }

    class Answer~Value~ {
      <<Type>>
      An answer to a Question
      Value value
      string? info
    }
  }

  namespace objects_nomination {
    class Nomination~Entity1, Entity2~ {
      Defines an Entity nominated for an Election in a Constituency by a possible nominator Entity.
      Defines the election symbol and possible election round
      %% Entity1 nominee
      %% Entity2 nominator
      %% string? electionSymbol
      %% number? electionRound
    }
  }

  namespace objects_questions {
    class QuestionCategory {
      A category of Questions
      May be restricted by Election or Constituency
      %% #Array~Id~? electionIds
      %% #Array~Id~? constituencyIds
      %% string type
    }

    class QuestionTemplate {
      May not be needed
    }

    class Question~BooleanMatchable~ {
      A Question object which may be matchable
      If matchable, must implement MatchableQuestion from vaa-matching
      May be restricted by Election or Constituency
      %% #Array~Id~? electionIds
      %% #Array~Id~? constituencyIds
      %% string? type
      %% BooleanMatchable matchable maybe unnecessary
      %% number get normalizedDimensions if BooleanMatchable
      %% normalizeValue(Id | any value) : MaybeArray~number~ if BooleanMatchable
    }

    class ChoiceQuestion {
      A subtype of Question that has enumerated options for answering
      %% #Array~Choice~ choices
    }

    class Choice~TValue~ {
      An enumerated answering option
      <<Type>>
      Id id
      any? value
      string? label
    }
  }

  %% INHERITANCE

  %% Updatable <|.. CanUpdate
  %% Updatable <|-- DataRoot
  %% Updatable <|-- DataObject
  %% DataObject <|.. DataAccessor
  %% DataObject <|-- Constituency
  %% DataObject <|-- Nomination
  %% DataObject <|-- NamedObject
  %% NamedObject <|-- Election
  %% NamedObject <|-- ConstituencyGroup
  %% NamedObject <|-- Constituency
  %% NamedObject <|-- QuestionCategory
  %% NamedObject <|-- Question
  %% NamedObject <|-- Entity
  %% Entity <|-- Candidate
  %% Entity <|-- Party
  %% Entity <|-- Faction
  %% Entity <|-- Alliance
  %% Question <|-- ChoiceQuestion
 
  %% CHILDREN AND LINKAGES

  DataRoot ..> ConstituencyDefinition : contains
  DataRoot ..> Election : contains
  DataRoot ..> Entity : contains subtypes of
  DataRoot ..> QuestionCategory : contains
  ConstituencyDefinition .. ConstituencyDefinition : may link to
  Election ..> ConstituencyGroup : contains
  ConstituencyGroup ..> Constituency : contains
  Constituency .. ConstituencyDefinition : links to via DataRoot
  Constituency ..> Nomination : contains
  Nomination .. Entity : links to subtypes via DataRoot
  Entity ..> Answer : has
  Alliance ..> Party : links to
  Party ..> Faction : may link to
  Party ..> Candidate : may link to
  Faction ..> Candidate : may link to
  QuestionCategory ..> Question : contains subtypes of
  Question ..> QuestionTemplate : may link to
  ChoiceQuestion ..> Choice : contains

Loading

Class inheritance

---
title: VAAData class inheritance and data tree
---
classDiagram
  direction LR

  %% CLASS DEFINITIONS

  namespace core {

    class CanUpdate{
      <<Interface>>
      Mandates methods for updates and subscribing to changes in this object
      %% CanUpdate parent
      %% onUpdate(boolean propagate) void
    }

    class Updatable {
      <<Abstract>>
      Provides methods for updates and subscribing to changes in this object
      Defines parent and children property templates
      %% #Record~string, MaybeCollection~ children
      %% #int numTransactions
      %% #Array~UpdateHandler~ subscriptions
      %% CanUpdate|null parent
      %% update(Function transaction, boolean propagate = false) void
      %% subscribe(UpdateHandler handler) Unsubscriber
      %% unsubscribe(UpdateHandler handler) int
      %% onUpdate(boolean propagate) void
      %% reset() void
    }

    class DataAccessor~DataObjectData~ {
      <<Interface>>
      Mandates getters for all properties of DataObjectData
      All subclasses implement this for their own data type
    }


    class DataObject {
      <<Abstract>>
      Implements the DataAccessor interface
      Provides access to DataRoot
      %% #DataObjectData data
      %% DataRoot root
      %% findAncestor(Function test) CanUpdate?
    }

    class NamedObject {
      <<Abstract>>
      Mandates an id and provides getter for name.
    }

  }

  namespace root {
    class DataRoot {
      Collects all data objects
      Provides id-based getters for global objects
      %% getConstituencyDefinition(Id id): ConstituencyDefinition?
      %% getCandidate(Id id): Candidate?
      %% getParty(Id id): Candidate?
      %% getQuestions(MaybeArray~Election~ election, MaybeArray~Constituency~ constituency)
    }
  }


  namespace objects_election {
    class Election {
      Defines the name of the Election and possible dates
      Owns Nominations via ConstituencyGroups and Constituencies
    }
  }

  namespace objects_constituency {
    class ConstituencyGroup {
      Collects the groups of Constituencies used in an Election.
    }

    class ConstituencyDefinition {
      Defines the area or other criteria for a constituency.
      May be shared by multiple Constituencies.
      %% Array~Constituency~? get subConstituencies
    }

    class Constituency {
      Owns Nominations.
      Connects a ConstituencyDefinition to an Election via ConstituencyGroup.
      Provides getters for all properties of ConstituencyDefinition.
      %% ConstituencyDefinition constituencyDefinition
      %% #Array~Question~ get questions
    }
  }

  namespace objects_entities {
    class Entity {
      <<Abstract>>
      Any Entity standing in an Election.
      May have Answers.
    }

    class Candidate {
      A person that can be nominated.
    }

    class Party {
      An association of Persons, usually a party but may also be a constituency association or other electoral list.
      %% Array~Candidate~ get members
      %% Array~Faction~ get factions
    }

    class Faction {
      A subgroup of a Party, used in ’ley de lemas’ electoral systems.
      %% Array~Candidate~ get members
    }

    class Alliance {
      An electoral alliance of Parties.
      %% Array~Party~ get members
    }

    class Answer~Value~ {
      <<Type>>
      An answer to a Question
      Value value
      string? info
    }
  }

  namespace objects_nomination {
    class Nomination~Entity1, Entity2~ {
      Defines an Entity nominated for an Election in a Constituency by a possible nominator Entity.
      Defines the election symbol and possible election round
      %% Entity1 nominee
      %% Entity2 nominator
      %% string? electionSymbol
      %% number? electionRound
    }
  }

  namespace objects_questions {
    class QuestionCategory {
      A category of Questions
      May be restricted by Election or Constituency
      %% #Array~Id~? electionIds
      %% #Array~Id~? constituencyIds
      %% string type
    }

    class QuestionTemplate {
      May not be needed
    }

    class Question~BooleanMatchable~ {
      A Question object which may be matchable
      If matchable, must implement MatchableQuestion from vaa-matching
      May be restricted by Election or Constituency
      %% #Array~Id~? electionIds
      %% #Array~Id~? constituencyIds
      %% string? type
      %% BooleanMatchable matchable maybe unnecessary
      %% number get normalizedDimensions if BooleanMatchable
      %% normalizeValue(Id | any value) : MaybeArray~number~ if BooleanMatchable
    }

    class ChoiceQuestion {
      A subtype of Question that has enumerated options for answering
      %% #Array~Choice~ choices
    }

    class Choice~TValue~ {
      An enumerated answering option
      <<Type>>
      Id id
      any? value
      string? label
    }
  }

  %% INHERITANCE

  Updatable <|.. CanUpdate
  Updatable <|-- DataRoot
  Updatable <|-- DataObject
  DataObject <|.. DataAccessor
  DataObject <|-- Constituency
  DataObject <|-- Nomination
  DataObject <|-- NamedObject
  NamedObject <|-- Election
  NamedObject <|-- ConstituencyGroup
  NamedObject <|-- Constituency
  NamedObject <|-- QuestionCategory
  NamedObject <|-- Question
  NamedObject <|-- Entity
  Entity <|-- Candidate
  Entity <|-- Party
  Entity <|-- Faction
  Entity <|-- Alliance
  Question <|-- ChoiceQuestion
 
  %% CHILDREN AND LINKAGES

  %% DataRoot ..> ConstituencyDefinition : contains
  %% DataRoot ..> Election : contains
  %% DataRoot ..> Entity : contains subtypes of
  %% DataRoot ..> QuestionCategory : contains
  %% ConstituencyDefinition .. ConstituencyDefinition : may link to
  %% Election ..> ConstituencyGroup : contains
  %% ConstituencyGroup ..> Constituency : contains
  %% Constituency .. ConstituencyDefinition : links to via DataRoot
  %% Constituency ..> Nomination : contains
  %% Nomination .. Entity : links to subtypes via DataRoot
  %% Entity ..> Answer : has
  %% Alliance ..> Party : links to
  %% Party ..> Faction : may link to
  %% Party ..> Candidate : may link to
  %% Faction ..> Candidate : may link to
  %% QuestionCategory ..> Question : contains subtypes of
  %% Question ..> QuestionTemplate : may link to
  %% ChoiceQuestion ..> Choice : contains

Loading

Check off each of the following tasks as they are completed

  • I have reviewed the changes myself in this PR. Please check the self-review document
  • I have added or edited unit tests.
  • I have run the unit tests successfully.
  • I have run the e2e tests successfully.
  • I have tested this change on my own device.
  • I have tested this change on other devices (Using Browserstack is recommended).
  • I have tested my changes using the WAVE extension
  • I have added documentation where necessary.
  • Is there an existing issue linked to this PR?

Clean up your git commit history before submitting the pull request!

@kaljarv kaljarv self-assigned this Aug 2, 2024
@kaljarv kaljarv added type: feature New feature or request category: architecture Architectural changes or architectural design scope: whole app All activities that concer the whole application labels Aug 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
category: architecture Architectural changes or architectural design scope: whole app All activities that concer the whole application type: feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: vaa-data module
1 participant