The Rating Stone Reputation System Database

$Id: Database\040Design.md,v 1.95 2023/09/13 00:47:03 agmsmith Exp $

  1. The Rating Stone Reputation System Database
    1. What is Rating Stone?
    2. Basic Idea of Write Once Objects and Links
      1. Changing Things by Making New Objects
      2. Single Table Inheritance
        1. Reusing Generic Fields to Avoid Space Explosion
      3. Direct References, Link Records, or Full Blown Lists
        1. Design Choice - Faster
      4. Retrospect
    3. About Posts and Groups
    4. Relationships
      1. Symmetry Dimension
      2. Approval Dimension
    5. Permissions Role Model
      1. Wildcard Language
    6. LedgerBase Classes
      1. LedgerChangeMarking < LedgerBase
        1. LedgerDelete < LedgerChangeMarking
        2. LedgerApprove < LedgerChangeMarking
      2. LedgerUser < LedgerBase
        1. LedgerCompany < LedgerUser
      3. LedgerThing < LedgerBase
      4. LedgerPost < LedgerBase
      5. LedgerMedia < LedgerBase
      6. LedgerSubgroup < LedgerBase
        1. LedgerFullGroup < LedgerSubgroup
      7. LedgerAwardCeremony < LedgerBase
        1. Calculating Awards
    7. LinkBase Classes
      1. LinkOwner < LinkBase
      2. LinkReply < LinkBase
      3. LinkMedia < LinkBase
      4. LinkHomePost < LinkBase
      5. LinkHomeGroup < LinkBase
      6. LinkRole < LinkBase
      7. LinkGroupRoleDelegation < LinkBase
      8. LinkGroupContent < LinkBase
      9. LinkSubgroup < LinkBase
      10. LinkRelationship < LinkBase
        1. LinkSymmetricRelationship < LinkRelationship
        2. LinkAsymmetricRelationship < LinkRelationship
        3. LinkOneWayRelationship < LinkAsymmetricRelationship
          1. LinkFriend < LinkSymmetricRelationship
          2. LinkEnemy < LinkSymmetricRelationship
          3. LinkSpouse < LinkSymmetricRelationship
          4. LinkParentChild < LinkAsymmetricRelationship
          5. LinkEmployerEmployee < LinkAsymmetricRelationship
          6. LinkFollower < LinkOneWayRelationship
      11. LinkOpinion < LinkBase
        1. LinkMetaOpinion < LinkOpinion
      12. LinkBonus < LinkBase
        1. LinkBonusUnique < LinkBonus
    8. Auxiliary Classes
      1. AuxLink
      2. AuxLedger
    9. Object Permissions
      1. LinkBase Permissions
      2. LinkReply Permissions
    10. Generating the Database
    11. Test Plan - A few Use Cases
    12. Change Log

What is Rating Stone?

It’s a social media and reputation system prototype web site that lets users write posts and rate things. There’s a bit of an economy of rating points, with new ones being doled out weekly and old points fading away over time. Points are also used for actions on the system (such as replying to a post) to deter unthinking spamming and other abuses. Anyway, we need a database to keep track of the users, posts, rating points, groups of users and so on.

Because I’m thinking of on implementing the reputation system as a BlockChain ledger application, or as a federated collection of systems exchanging records, the database is set up as a write once collection of object records and link records. Objects are the things the user can view and which have ratings (posted messages, lists of things, other users). Links implement lists of things, connecting a pair of objects together, one being a parent or container object (such as a list of book reviews) and the other being a child object in that container (such as a book review object). Usually rating points are also transfered when you create a link (perhaps describing how much you like that book review). I’m using separate link records rather than parent references internal to the objects so that an object can have multiple parents (useful for filing something under multiple categories).

Changing Things by Making New Objects

For example, if you want to edit some posted text, the system will create a newer object (they all have datestamps) with the new text and a reference back to the original text object record. Then when someone wants to see the original text, it will search for newer versions and display that text instead. But new references (links) to the text (such as replies) will still reference the original text record, since that has a more constant identity then possibly yet more new text records. Advanced users can view a list of all those text records and see how the text changed over time.

Similarly when someone adds an item to a list, a link record will be added to connect the list and the item. To remove an item, the link record isn’t modified, instead a new deletion record is created, marking the original link record as deleted.

Theoretically, when displaying the current state of things, you run through all the records in chronological order, adding and removing objects and links from the current state. In practice, we cache things. So an object record would have a flag that marks it as deleted or amended, and a link record would similarly be marked, and there can be cached forward references to more quickly point out the record with the latest information.

Single Table Inheritance

Because I don’t want to have zillions of tables in the database, and want to inherit functionality for various related record types (generic object > text object > book review object), we’re using the Ruby on Rails ActiveRecord “single table inheritance” setup. There will be one actual database table for objects and another one for links between objects, both have the Rails magic “type” field in each row to specify the particular subclass used for that row.

Reusing Generic Fields to Avoid Space Explosion

To avoid the common complaint about single table inheritance adding too many fields for subclasses, we put some generic number and string fields into the base class and reuse them differently for each subclass. Subclasses give those generic fields subclass-specific names by using the magic “alias_attribute” ActiveRecord method. So where the base may have a generic string1 and string2 field, a User subclass could rename those as FullName and EMailAddress, with “alias_attribute :full_name, :string1” and “alias_attribute :email_address, :string2”. ActiveRecord will then automatically build queries using the actual database field (string1, string2) names whenever we use the subclass object.

There’s a design choice needed for making lists of things. Should you make link records between a main object and sub-objects that apply to it, or should you have a second object as kind of a list header, and link the sub-objects to it? For example, a Friends list. You could link friends (LedgerUser records) to the main user (another LedgerUser record) with specialised LinkFriend records. Or you could make a LedgerListFriends object with a link to the main user, then attach the friends to that with generic link records.

Design Choice - Faster

Direct links are faster for the database, but need a lot of custom link record subclasses, but those are fairly cheap. The list header method just needs to give the list header a name and uses generic records, but requires more database operations. There’s also a third option of putting the link reference in the record as an id field, like friend_id, but that only works if you have one friend (useful in other cases, like having only one creator). We’ll go with the faster but more work custom link subclasses method, except for user defined lists which will use the list header with a name technique, and plain references where that makes sense. If the number of subclasses gets too big, this decision will have to be reconsidered.

Retrospect

In retrospect it may be simpler to combine objects and links into a class structure based on a single multipurpose object. Currently we have records for linking things together (such as linking a user and a thing together with a rating to say that that user has an opinion about that thing) and object records representing things (posts, pictures, etc). The links transfer reputation points. The objects contain reputation points. A multipurpose object design would make it simpler to have an opinion about an opinion, which is needed for meta-moderation. And we need meta-moderation to keep the quality of opinions up. Though having simpler link records makes the database smaller and faster.

About Posts and Groups

When someone writes a post, they create both a LedgerPost object with the text and a link object (LinkGroupContent) to connect it to the group it is in. If they want it to be in multiple groups, they can just add more links. If it’s a reply to some other post, they add a link (LinkReply) to that post too. It’s a bit awkward and expensive in ratings points (each link needs points) having both a link to the reply and a link to the group, but both are needed. We can’t default to the group based on the group the previous message is in, since later on someone could add that previous post to some other group and your post would suddenly be visible to people you hadn’t expected. Also this discourages the evil of unnecessary cross-posting.

The links also award points to both the post and the group (the user decides how much to spend and how to split it), so that posts using the group contribute a bit to its upkeep. The group owners can spend the group points on the group’s behalf, perhaps reimbursing moderators for the points they use while doing their work (creating approval and deletion objects takes points). Of course, if the group’s points fall to zero, it can be garbage collected. Same for posts that aren’t referenced much over time.

I’d like to have small points awarded for just reading a post, but that won’t be practical since it would blow up the database size tremendously (each read would create a LinkBase record). Or maybe we could have a daily “tip” or donation operation that awards points to the list of posts the user read that day, and a variation of LinkBase that uses AuxLedger records to specify a bunch of posts to get a reward. Though that would make the backtracking from the post to donors awkward (not just looking for a connected LinkBase object) when doing awards ceremonies. So for now, you can “like” or maybe “cast a stone” for individual posts and award points manually.

So, what happens if you make a post that isn’t in a group? People can still find it by looking at the creator’s profile for all the posts they have written. There are no groups to allow people to read it, so it has a default permission role of Banned for everyone except the creator or owners (if the user attaches extra owners to the post). An alternative for ungrouped posts is to make them readable by everybody, including anonymous Internet users so search engines will pick them up. Which to choose? A default of Banned would force users to spend extra points to make their post readable in some group. A default of overly public would encourage more public discussion. I’m going to make them public, since we still require spending typically 1.0 reputation points to create an ungrouped reply. It costs twice as much (or you could make do with a shorter message life) to make it a reply in a private discussion, since you have the group link(s) to add as well as the reply link. People will still use groups since it’s worthwhile to reach a more relevant audience.

Relationships

We may want to identify people based on relationships. Or maybe not - relationships are currently unimplemented, and for “feed” purposes we can “follow” a user by simply becoming a member of their personal group. The obvious relationship used by many social media is Friend. Another is Follower. We can also do Parent, Child, Spouse and that should about cover it for family relationships (grandparents are Parent of Parent of person, siblings share a parent, etc). We might want to add variations of friendship like boyfriend/girlfriend, work related acquaintance, divorced and so on but preferably without the extra complication of gender. Possibly besides birthday records, we’ll need death date records so a spouse is a widower when the other spouse is dead, but they go back to being married when they are both dead (they’re always married, but we’re showing what is important to their audience).

After discarding some earlier ideas on relationship implementation, it seems there are a two orthogonal properties of relationships we have to worry about for searching and security purposes: Symmetry and approval. All relationships will be represented in our database by a LinkBase subclass record listing two people (parent and child fields, though that usually isn’t the actual meaning) and how they’re connected.

Symmetry Dimension

Bidirectional or equal or symmetric relationships are like Friend where both ends of the relationship are an indistinguishable “Friend”. Same for Spouse. But Parent/Child is a pair that has distinctly different ends. So is Follower/Leader, or Employer/Employee. Those we’ll call asymmetric.

The main complication is that we have to search differently depending on the symmetry. If we’re looking for friends of person X, we have to search for all Friend records that have X in the parent_id as well as all Friend records that have X as the child_id. If we’re looking for all parents of X, we have to search the Parent/Child records which have X as the child. If we’re looking for all children of X, we have to search the same kind of parent/child records, but look for ones where the parent field is X.

So, the search technique changes and we need to know that Parent/Child searches look for the same type of record but search different fields.

Approval Dimension

The other dimension is approval. Sometimes we need approval from both parties to create the link; each approves their end using a LedgerApprove record. Other times we have a one way relationship, such as a fan of a movie star, and only the fan end needs approval (and that’s usually automatic because they created the link).

One side effect of approval is that the reputation points assigned to the link only get counted when the link is fully approved. That gives incentive to do approvals, and opens the door to third party suggested relationships (be a fan of this brand and you get some points). Of course, instead of approving, you can delete the link, and the points spent on making the link are wasted.

Partially approved relationships are still useful. If only one end is approved, then for example instead of Friends we have admirer (the approved end) and admiree (the unapproved one). Or if both ends haven’t been approved, we have a suggested friendship (maybe a third party created the relationship record). One-way relationships still need approval of the originating end, the fan implicitly approves the relationship record when they create it for themselves. If someone else created it, then it still needs the originating end approved by the fan. The movie star end is automatically approved. Otherwise fans and haters would have their one way relationship rebuffed; a particular problem for awarding down points. No, the movie star or villain can’t delete one way relationships; like fleas they stick around for a while and annoy.

Permissions Role Model

The main worry is about posts. The system needs to decide who can read a post, who is allowed to reply, who can add a post to a private discussion group, who can delete it and so on. We’ll worry about other permissions later. After considering access control lists and even UNIX style permissions I decided that every post should be in a group or several groups. The group decides many of those things mainly by having a membership list. Even a person’s blog would be a group. Perhaps private direct communictions could be a group too, with just the two people in the conversation being group members.

The next big insight was to use permission roles. They can be put in a rough order where one more permissive role includes the things allowed in the lower less permissive roles, to save on having to grant each permission individually. In practice, each role is granted by linking the user to a group with a record that specifies the highest role they have in that group.

  1. Banned. Can’t access the group or posts.
  2. Reader. Can read posts and see the group description.
  3. Member. Can see more group information, like membership lists. Some groups also automatically approve posts by members, effectively giving the member write priviledges. Others require approval by a moderator. By the way, there’s no permission needed to request that a post be added to a group, so even a non-member could submit a post.
  4. Opinionator. Can make opinions about things, and associate them with the group.
  5. Meta Opinionator. Can make comments on opinions about messages in the group. There might be bonus points for accurate opinions and bans for creators of bad ones.
  6. Message Moderator. Can approve, unapprove (makes it pending approval again) and disconnect posts and opinions from the group. Can see deleted messages and opinions.
  7. Member Moderator. Approves and rejects membership requests.
  8. Owner. Owners can be added to a group (and other objects) by a LinkOwner record. They have all permissions (including approving or deleting owners and moderators), except they can’t change the creator.
  9. Creator. Has all permissions to modify the object, including changing the creator.

We’ll use ten times the list row numbers here as the role priority, in case we later want to add new roles in-between.

When finding out if a user has a permission for an action allowed by a certain role, check for a match with the creator first (everything is allowed). Then see if they are in the Banned role second: permission is denied (if you want to explicitly deny a user access to the group, give them the Banned role). Then start searching at the most permissive role after Creator and work down to the lowest level that matches the role query. If they were not found in the search then they don’t have permission for the role. In practice, we’ll first look for some LinkBase subclass record that specifies roles and sort by role priority. Then try the more computationally expensive wildcards for each role.

The group would have a few settings related to those roles (stored in a separate database table rather than directly in the LedgerFullGroup record, unless we want to use JSON in the group record):

Wildcard Language

To identify a group of people by wildcards, we can make up a little language. We want to have expressions that say:

Perhaps a functional language like:

So you could have “Friend(Members)+Members” for friends of all members and the members too. An empty string would mean nobody.

LedgerBase Classes

This is the base class for persistent objects that the user may interact with. There is one database table for all objects of this class and subclasses. The name is short for LedgerObjectBase class. I tried to find other words for a short form, but there’s already an “Object” class and “Thing” didn’t seem serious or distinct enough.

LedgerChangeMarking < LedgerBase

Used for marking possibly multiple objects and/or links as deleted, undeleted, approved or rejected. AuxLedger and AuxLink records are used to associate this LedgerChangeMarking with all the things (both LedgerBase object records and LinkBase link records) being changed. Subclasses specify which kind of marking (mark deleted, mark approved, etc) to do. A boolean in the record specifies whether to set or clear the marking.

Please don’t make loops of LedgerChangeMarking records, this will lock up the server. While you can do an undelete by deleting a LedgerDelete with another LedgerDelete record, the recommended way (easier for the user to understand) to undo a delete is to make a new LedgerDelete which clears the delete flag on the same set of files as the original LedgerDelete.

While you can have several LedgerChangeMarking records affecting the same object, only the undeleted ones (can be expired, that doesn’t matter) played back in chronological order decide the target object’s status. So yes, if enough time passes, the LedgerChangeMarking will fade away and the targets will no longer be deleted or approved etc. Though most often the targets will have faded away too by that time. Or we could have a policy of not garbage collecting LedgerChangeMarking records.

LedgerDelete < LedgerChangeMarking

Deletes the specified records, can be LedgerBase and/or LinkBase ones. If the new_marking_state flag is false, it undeletes.

You usually need to be the creator or owner of the link to change the deleted state. I was thinking that people who can approve the link ends could also delete it, but that then means that someone who has been banned from a group could delete their banning link. May need to revisit this, with exceptions depending on the link subclass.

LedgerApprove < LedgerChangeMarking

Both child and parent approvals of the specified LinkBase records are potentially changed when you make a LedgerApprove request. The actual changes are determined by whether you have permission to change those approvals (relative security role used). To more strongly reject permission, delete the link instead (if you are allowed to).

LedgerUser < LedgerBase

Idenfities a particular user. We don’t have placeholder users (for celebrities and other external people); a group should be created by fans instead.

Elsewhere in the User table, we keep track of non-ledger (no version history) user information:

LedgerCompany < LedgerUser

Are corporations people? Not really, but we have them as users too in case someone wants to run a company web page or otherwise have a presence on this reputation system. As well, employees can list the LedgerCompany as their employer. Alternatively, they are a regular User but linked to a category of Companies. I prefer the latter method.

LedgerThing < LedgerBase

Represents a real world thing like a book. Users can comment about it and award it rating points, but it never regenerates points (only Users do that). Attach content to it (like pictures or a post) to describe it better, using LinkHomePost. Links to a classification group can rate it. For example, a book reviews group would have a link to the book to award it points. Can be in multiple groups, such as “My Books” and “World’s Best Books”. Probably not in the first prototype, just posts is fine.

LedgerPost < LedgerBase

A post made by a user. This is the object the user interacts with the most. Contains a subject and some formatted text. They can also attach pictures and other media files with LinkMedia records (they’ll be displayed in a grid after the text, in order by link creation date).

There can also be replies to this thing, it can be put in a discussion group, added to a category list to identify what the topic of the content is, have rating points awarded to it, and so on. It is also usually visible to users and even external web sites, unless permissions (determined by the groups it is in) make it private.

LedgerMedia < LedgerBase

A file uploaded by the user. Usually some sort of media, like a picture or video clip. Can be attached to multiple posts to make them more interesting, and referenced by external web sites as well. Byte size only limited by the points the user wants to spend to upload it. All kinds can display a picture, which can be clicked on to get more detail or play it if it is a video or audio file or download it if binary.

LedgerSubgroup < LedgerBase

A way of collecting related things together. Useful as a subtopic of a discussion group or a category for photographs within a classification tree. All content that fit the subtopic (decided by users and moderators) are linked to this object by LinkGroupContent records (even replies to posts already in the subgroup).

LinkSubgroup records specify the hierarchy of subgroups (actually a possibly cyclic directed graph), by linking parent group to child group. Multiple parents are allowed, so a photo taken in Canada in July 2020 could be filed both in a subgroup for North America and a subgroup for Year 2020. You could even make subgroups for each family member and classify the photos by who is in them.

There’s an optional home post (see LinkHomePost) that describes the group in more detail. The home post usually will have a list of recent additions in the group displayed after it.

A subgroup object doesn’t have members attached or do permission role check operations; that’s delegated to the LedgerFullGroup(s) which control this subgroup by LinkGroupRoleDelegation records. If there are none then only creators or owners can use the subgroup.

LedgerFullGroup < LedgerSubgroup

Besides being a grouping object, this one also keeps track of LedgerUsers in various roles (plain member, moderator and so on), linked to it by LinkRole records. The plain LedgerSubgroup objects controlled by this group will delegate their permission determination to this LedgerFullGroup and its membership lists by a LinkGroupRoleDelegation record for each subgroup.

Currently, extra settings (post moderation options, etc) are stored in a related GroupSettings database table, though we may need to use ledger objects if a history of changes needs to be preserved, or JSON in the group record if we want to keep it all in one place.

LedgerAwardCeremony < LedgerBase

This record marks the time when (usually weekly) rating points are awarded. We perform the ceremony relative to planetary time, so the actual time between ceremonies won’t always be exactly 7 days and 0 seconds, due to the slowdown of the earth’s rotation and server downtime problems. Currently deleting this kind of record may cause problems (like duplicate ceremony numbers), so don’t do that.

Calculating Awards

To avoid rewriting the whole database, current point levels based on fading and awards over time for regular LedgerBase objects are calculated on demand and cached. If needed, we can recalculate everything for a particular object back to the beginning of time, which is useful when you import records from somewhere else or didn’t save cached values in a backup.

There’s a bit of extra complication when Link records have been (un)deleted or (un)approved where the points given to the child disappear and reappear (creators are always charged for the points spent). We generally calculate based on the final deleted state for the latest award ceremony. Though you may want to calculate the state of the system as of a particular date in the past.

The LedgerAwardCeremony class has functions for keeping track of the latest award ceremony and starting a new one (invoked by a weekly cron script on the web server or manually).

Bonuses are kept track of by linking the user with LinkBonus records to various things (category or a post explaining the bonus), possibly multiple times for some or singletons for others (like e-mail activation). There’s a category for +1 weekly entitlement point for a good year of posting, another one for -1 weekly point of annual inactivity, or no bonus for just doing a minor amount of posting during the year. Those bonus links are made during the award ceremony on the annual anniversary of a user’s account activation.

As mentioned elsewhere, the weekly allowance bonus points are added to the user’s meh score each week (theoretically just after the ceremony is done). The user can spend meh points and if their up is more than their down points, the extra up points can be spent too, though their meh will be used first. In the future if users are particularly obnoxious, we can also limit the spending of meh points to ones above the down points, but then we’d need another mechanism to make sure they can still spend their allowance points in all situations.

LinkBase Classes

This is the base class for links of all kinds between pairs of objects. There is one database table for all instances of this class and subclasses. The particular subclass identifies what the link’s purpose is (object A owns object B, object A is a list header and B is in the list, etc).

LinkOwner < LinkBase

The parent is a LedgerUser who is to be treated almost the same as the creator of the object specified as the child of the link. They can do everything the creator can do, except change the creator.

Only the creator or an existing owner can approve new owners of an object.

Adding an owner to a LedgerUser currently doesn’t do much, but in future it could be a way of letting multiple people control a person or company account.

LinkReply < LinkBase

The child is a reply to the parent. Usually both objects are LedgerPost. Besides the usual creator, the users who are allowed to approve either end of the link are also allowed to delete the link. So if someone attaches their post as a reply to your post, you can break that connection. Quotes are also done with LinkReply, but child and parent are reversed (you can tell by the backwards creation dates).

LinkMedia < LinkBase

The child is some sort of media which should be displayed when the parent is shown. Usually the parent is a LedgerPost and the child is a LedgerMedia of some type. We just treat them all as pictures and show them in a grid after the text of the post (grid order determined by the order of datestamps of these link records). The user can click on individual ones to play them.

LinkHomePost < LinkBase

Parent is a Ledger(Full/Sub)group or LedgerThing and child is a LedgerPost. That post will be used as the home post (home page, status page, profile page, description, etc) for the parent. If there are several, we show them all, kind of like a list of announcements or pinned posts. Delete the LinkHomePost to unpin them.

LinkHomeGroup < LinkBase

Parent is a LedgerUser and child is a LedgerFullGroup. That group will be used as the home group for the user’s personal posts. Normally the group will be created when the user is created, with the user listed as the group’s creator. That way they’ll be able to control who can see their personal posts. If someone wants to follow that user, they will simply subscribe to the group as a reader. If there are several records, only the latest one will be used.

LinkRole < LinkBase

Specifies the role a person has in a group. Parent is the LedgerFullGroup, child is the LedgerUser.

LinkGroupRoleDelegation < LinkBase

Specifies that the parent LedgerFullGroup is used for permission checks for users trying to access the child LedgerSubgroup. If there is more than one of these records, they’re all checked until one of them gives permission or all of them deny permission.

LinkGroupContent < LinkBase

Links a child LedgerPost to a parent LedgerFullGroup or LedgerSubgroup. Means that the child is considered part of the group’s collection of posts and will use the group’s permissions to decide who can see the child. Quite often the links will be pending until each end is approved by a moderator or higher role (usually moderator at the group end, owner/creator at the post end). Note that all replies to a post that are in the same group also need a LinkGroupContent record, since group inclusion can change from post to post as the users wish (user interface defaults to using the same groups as the original post when replying).

Unlike most Links where only the owner or creator of the linked to thing can change the approval, group message moderators can change the approval of the parent (group) end of this link (since it’s trying to connect to their group). As well, they can also delete this link (to get it out of their pending things to moderate list). Similarly the users who can change the approval of the child end of the link (usually the creator of the post) are allowed to delete the link.

LinkSubgroup < LinkBase

Specifies that the child LedgerSubgroup or LedgerFullGroup is subordinate to the parent LedgerFullGroup or LedgerSubgroup. Used for making a directed graph structure of groups in order to classify things (like photos or posts attached to individual group objects) or otherwise break them down into more managable smaller chunks. Multiple parents are allowed and you can make cycles if you wish (though that will reduce database query performance).

LinkRelationship < LinkBase

This is the abstract base class to show the parent and child are LedgerUser objects in some sort of relationship, such as parent, child, spouse, friend, enemy, employer, follower. Might be used in the future, but not in the initial minimal prototype system.

Only the creator and people who need to give approval can delete it.

Of course, the link will fade away over time unless new reputation points are spent to create a fresh copy. Thus you may have several copies of the same relationship in the database. Whichever one is undeleted and has full approvals will be used to find people with the relationship.

All ends of the link which are the same as the creator are automatically approved. Other ends will be in a pending state until the referred person uses a LedgerApprove to approve their end. Where you can approve, you can also unapprove your end of the relationship. Until both are approved, it’s a partial relationship (admirer rather than friends) and not usually shown to users or used for finding related people.

When finding posts for a person’s feed, items from related people will be given increased priority.

LinkSymmetricRelationship < LinkRelationship

Relationships can be symmetric (friend/friend) or asymetric (parent/child). This subclass handles the symmetric ones. For searching efficiency, we’ll just arbitrarily make the older user (lower ID number) the parent_id in symmetric links.

Subclasses of this will export a function that finds all people related to a specified bunch of people, such as Friends(X). Note the input X is a collection of people, so we can use the results of a previous relationship test as input. That way Enemies(Enemies(X)) works, if you want to give permissions to enemies of X’s enemies.

LinkAsymmetricRelationship < LinkRelationship

Asymmetric relationships have distinctly different ends of the relationship (such as real world parent and child, or Follower/Leader, Employer/Employee). The particular relationship class will define which one goes in the parent field and which is the child.

Subclasses will export two functions that find all people that are related to a specified person, one for each end, so you can ask for all employees of a boss via Employees(x) or ask for all bosses of an employee via Bosses(x).

LinkOneWayRelationship < LinkAsymmetricRelationship

Rather than two way relationships, some are one way such as follower. The person being followed (a Leader) doesn’t have a say in whether they’re being followed. Thus approval is always given for the child end and the relationship is set up with the originator as parent (follower in the parent field, followed or leader in the child field). If the parent is the creator, they’re automatically approved. Only the creator and parent can delete this link.

LinkFriend < LinkSymmetricRelationship

The parent is a friend of child. This is a bidirectional relationship so both ends are friends with each other. If one end hasn’t approved the friendship, then we’ll refer to it as an unacknowledged friendship and perhaps say X is admired by Y and Y has an admirer. If neither are approved, then it’s a suggested (by the creator) friendship.

LinkEnemy < LinkSymmetricRelationship

You can declare your enemies too, if they agree. Bidirectional like LinkFriend. Real enemies will give and accept negative points. Don’t expect that to happen often.

LinkSpouse < LinkSymmetricRelationship

Parent record (a LedgerUser) is a spouse of the child record (also a LedgerUser). Bidirectional. Both ends need to be approved.

LinkParentChild < LinkAsymmetricRelationship

Parent is parent in real life of child. Both ends need to be approved.

LinkEmployerEmployee < LinkAsymmetricRelationship

Parent is the employer or company employing the child. Both ends need to be approved. Otherwise you’ll see something like “claims to be employed by X”.

LinkFollower < LinkOneWayRelationship

Parent LedgerUser is a fan or at least interested in the child. If you like the child LedgerUser, send some positive points, negative if you hate them. They receive those points either way.

LinkOpinion < LinkBase

The link creator has an opinion about the child object (usually a LedgerPost or a LedgerUser or LedgerThing). Can be positive or negative or meh, decided by the points awarded to the child. Parent is usually the creator of the child, in case the creator wants to award them some spendable points too. Automatically approved for the child end and parent end, and can only be deleted or unapproved by the creator.

For example, if you like a post, you can make a LinkOpinion referencing the post (as the child object) and rate it upwards with your points. The parent field would identify the post’s author, and give them some additional points if you wish to essentially rate them too. The default for a quick “like” is to just use half the points on the child, half on the parent. A fancier user interface would let you specify parent points separately, with possibly a separate direction.

LinkMetaOpinion < LinkOpinion

You can have an opinion about someone else’s opinion or other link objects. If you disagree with the rating someone made on a post, you can make a LinkMetaOpinion that has the person or group responsible for the meta opinion as the child and the author of the original opinion as the parent, and the LinkOpinion record ID in the number1 field. The creator is the user making the meta-opinion (usually a moderator for the group if child is a group) and paying for it. It doesn’t affect the original opinion’s operation, which doesn’t have a reputation rating of its own.

Your meta opinion doesn’t have to always be about some other opinion. If you like the way someone connected a bit of useful information (an existing old post answering a frequently asked question) as a reply to a new post, you can reward them for making that link with a LinkMetaOpinion’s referencing the LinkReply record and with the creator of the LinkReply as the parent. I suppose you could have an opinion about a LinkMetaOpinion too.

LinkBonus < LinkBase

This kind of link adds or removes weekly bonus points to/from a child LedgerUser record. The parent object explains the bonus, usually in the form of LedgerPost of some sort that can be displayed to the user if they want to know why they got that bonus, or perhaps a Category object (have a tree of bonuses). Normal users can’t create this kind of link other than indirectly through their actions.

The links fade away as usual and the bonus can have a time limit as well. Your bonus for being a good poster in the previous year can be set to last for a year and then stop contribuing when it hits its expiry ceremony number. Though if the expiry date is too far in the future, it will also fade away as usual and eventually get garbage collected. Similarly you’ll have to reenter an e-mail address once the bonus for having a tested e-mail has expired. Some bonuses like the e-mail bonus will be limited to only one existing at a time.

The total of all bonus points at the award ceremony is pegged between 0 and 100 (in case they have too many negative bonuses, or have unfairly many positive bonuses) and becomes their week’s allowance. The user also gets to spend that many points even if their net reputation is negative, to avoid totally silencing them. The weekly allowance and week’s spending is stored in the User record, outside the ledger system, mostly for informational and user interface purposes.

LinkBonusUnique < LinkBonus

A subclass for bonuses which are unique. There can only be one of these active at the same time linked to the same parent object. For example, the activated e-mail bonus has to be unique, otherwise the user will just keep on activating more e-mail addresses to get more of a bonus.

Auxiliary Classes

This database table and class is used for connecting LedgerBase records (a LedgerChangeMarking usually) and LinkBase records. Used for deleting & undeleting, and only for LinkBase subclasses - giving approvals. The main difference with LinkBase is that no rating points are involved.

AuxLedger

Like AuxLink, but for connecting LedgerBase objects (usually a LedgerChangeMarking object) to child LedgerBase objects (such as things being deleted).

Object Permissions

There are several permissions used for objects and links to control who can do what, the last three are for links only:

  1. The user who is the creator/owner (can be multiple people for LedgerBase records) can delete, undelete and for LedgerBase records create a new version of the record. Implemented in object or link method creator_owner?(luser).
  2. The people who are allowed to view the record. Implemented in object or link method allowed_to_view?(luser).
  3. There are separate permissions for changing the approval flag for the parent in a link and
  4. the approval for a child in a link (the link doesn’t work unless both approve it).
  5. There’s also the initial approval state of a link. It is usually the same as the current parent and child approvals relative to the creator of the link.

LinkBase Permissions

This is the default for classes inheriting from LinkBase.

  1. creator_owner?(luser) - Only creator of the link is allowed to delete it.
  2. allowed_to_view?(luser) - Only creator of the link, the child’s creator or the parent’s creator can view the link record.
  3. permission_to_change_parent_approval(luser) - Only creator of the parent can approve it.
  4. permission_to_change_child_approval(luser) - Only creator of the child can approve it.
  5. initial_approval_state() - Default implementation uses values from permission_to_change_*_approval with the link creator as the test user.

LinkReply Permissions

  1. creator_owner?(luser) - Creator of the link, and also creator/owner of the child or parent is allowed to delete the link.
  2. allowed_to_view?(luser) - Inherited default. No need for most people to examine the reply connection.
  3. permission_to_change_parent_approval - Inherited default.
  4. permission_to_change_child_approval - Inherited default.
  5. initial_approval_state - Starts off fully approved. Otherwise it is just too much of a pain for the original post’s author to approve all the replies. Though maybe we can flag a post as requiring approvals to combat spam, perhaps allowing only group members to reply with pre-approval, later.

Generating the Database

Here are the rails command lines (out of date, new fields have been added over time) which can be used to generate the database and initial source code files:

rails generate model LedgerBase type:string
bool1:boolean date1:datetime number1:integer string1:string string2:string
text1:text creator:references original:references amended:references
deleted:boolean current_down_points:float current_meh_points:float
current_up_points:float

rails generate model LinkBase type:string
parent:references child:references creator:references deleted:boolean
pending:boolean rating_points_spent:float rating_points_boost_child:float
rating_points_boost_parent:float rating_direction:string original_ceremony:integer

rails generate model AuxLedger parent:references child:references

rails generate model AuxLink parent:references child:references

# And for subclasses, do something like this:

rails generate model LedgerList --parent=LedgerBase

Combine and edit the migration files to have default values and better field details like this:

class CreateLedgerAndLinkBases < ActiveRecord::Migration[7.0]
  def change
    create_table :ledger_bases, force: false, comment: "Ledger objects base class and record.  Don't force: cascade deletes since this is a write-once ledger where usually nothing gets deleted (except during points expired object garbage collection)." do |t|
      t.string :type, default: "LedgerBase", comment: "Names the ActiveRecord subclass used to load this row, turning on single table inheritance."
      t.references :original, null: true, foreign_key: {to_table: :ledger_bases, name: "fk_rails_ledgeroriginal"}, comment: "Points to the original version of this record, or equal to id if this is the original one.  NULL if not initialised (should be a copy of the id of this record)."
      t.references :amended, null: true, foreign_key: {to_table: :ledger_bases, name: "fk_rails_ledgeramended"}, comment: "Points to the latest version of this record, or NULL if this is not the original record."
      t.boolean :deleted, default: false, comment: "True if there is a LedgerDelete record that is currently deleting this record, otherwise false (record is alive)."
      t.boolean :expired_now, default: false, comment: "When true, the record will be permanently deleted (not just marked as deleted) at the next award ceremony."
      t.boolean :expired_soon, default: false, comment: "When true a warning message is displayed to users when viewing this object (suggesting that they spend some points to revive it), doesn’t affect how the record is treated."
      t.boolean :has_owners, default: false, comment: "True if there is one or more LinkOwner records (even deleted ones) that references this record.  False if there are none, which means we can skip searching for LinkOwner records every time we check permissions, which saves a lot of database queries!"
      t.boolean :is_latest_version, default: true, comment: "True if the record is the latest version of the object.  False otherwise.  Caches the result of looking up the original object and seeing which record is the latest, so we have less overhead when displaying only the latest versions in a list of posts.  Also lets us skip older versions directly in an SQL query."
      t.references :creator, null: true, foreign_key: {to_table: :ledger_bases, name: "fk_rails_ledgercreator"}, comment: "Identifies the user who created this record, using their original ID."
      t.float :rating_points_spent_creating, default: -1.0, comment: "The number of points spent on creating this Ledge object by the creator.  Includes transaction fees.  Since every object has a creator, the data about the creation spending is stored here in the object, rather than making a separate LinkBase record for every object.  It also means we avoid the worries about approvals and deletion that come with a LinkBase record.  If negative when creating a new object (but not in the database), use default spending amounts.  If the creator of a LedgerObject is changed, the points used for creating it will be charged to the new creator and uncharged from the original creator, backdated to the original ceremony."
      t.float :rating_points_boost_self, default: 0.0, comment: "The number of points used to boost the rating of this object, during creation.  Should not be negative."
      t.string :rating_direction_self, default: "M", comment: "Use U for up, D for down or M for meh.  Controls how points spent during creation modify this object's rating."
      t.boolean :bool1, default: false, comment: "Generic boolean, defined by subclasses."
      t.datetime :date1, null: true, comment: "Generic date and time from year 0 to year 9999, defined by subclasses."
      t.bigint :number1, default: 0, comment: "Generic number for counting things, or referencing other database tables, usage defined by subclasses."
      t.string :string1, default: "", comment: "Generic string (up to 255 bytes), defined by subclasses."
      t.string :string2, default: "", comment: "Generic string (up to 255 bytes), defined by subclasses."
      t.text :text1, default: "", comment: "Generic text (lots of characters), defined by subclasses."
      t.float :current_down_points, default: 0.0, comment: "Number of rating points in the down direction for this object. This is the current total, including fading over time (recalculated at the beginning of the week in the awards ceremony) plus new ratings applied this week."
      t.float :current_meh_points, default: 0.0, comment: "Number of rating points in the meh non-direction for this object. This is the current total, including fading over time (recalculated at the beginning of the week in the awards ceremony) plus new ratings applied this week."
      t.float :current_up_points, default: 0.0, comment: "Number of rating points in the up direction for this object. This is the current total, including fading over time (recalculated at the beginning of the week in the awards ceremony) plus new ratings applied this week."
      t.integer :current_ceremony, default: -1, comment: "The number of the awards ceremony that the current points were calculated for.  0 means before the first ceremony.  Set to -1 to force a recalculation of current points.  If it is less than the most recent ceremony's number, the points just need an update recalculation."
      t.integer :original_ceremony, default: -1, comment: "The number of the awards ceremony immediately prior to the creation of this object.  0 if before the first awards ceremony.  Negative is an initialisation bug.  Theoretically you could figure it out from the record creation date."
      t.timestamps
    end

    add_index :ledger_bases, :string1
    add_index :ledger_bases, :string2
    add_index :ledger_bases, :number1

    create_table :link_bases, force: false, comment: "LinkBase base class and record for linking LedgerObjects together." do |t|
      t.string :type, default: "LinkBase", comment: "Names the ActiveRecord subclass used to load this row, turning on single table inheritance."
      t.references :parent, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_linkparent"}, comment: "Points to the parent LedgerBase object (or subclass) which is usually the main one or older one in the association.  Uses the original ID of the parent."
      t.references :child, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_linkchild"}, comment: "Points to the child LedgerBase object (or subclass) which is the child in the association.  Uses the original ID of the child."
      t.references :creator, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_linkcreator"}, comment: "Identifies the User who created this link, using their original ID."
      t.float :float1, default: 0.0, comment: "Generic floating point number, usage defined by subclasses."
      t.bigint :number1, default: -1, comment: "Generic number for counting things, or referencing other database tables, usage defined by subclasses."
      t.string :string1, default: "", comment: "Generic string (up to 255 bytes), defined by subclasses."
      t.boolean :deleted, default: false, comment: "True if there is a LedgerDelete record that deletes this record, otherwise false (this record is alive)."
      t.boolean :approved_parent, default: false, comment: "True if the link to the parent object has been approved.  False means it's pending; the link record exists but it can't be traversed (sort of like being deleted) until someone gives permission via LedgerApprove."
      t.boolean :approved_child, default: false, comment: "True if the link to the child object has been approved.  False means it's pending; the link record exists but it can't be traversed (sort of like being deleted) until someone gives permission via LedgerApprove."
      t.float :rating_points_spent, default: -1.0, comment: "The number of points spent on making this link by the creator.  Includes transaction fees.  Negative when creating a link means use default spending amount and default allocation for the various boosts, though won't be negative in the database."
      t.float :rating_points_boost_parent, default: 0.0, comment: "The number of points used to boost the rating of the parent object.  Should not be negative."
      t.float :rating_points_boost_child, default: 0.0, comment: "The number of points used to boost the rating of the child object.  Should not be negative."
      t.string :rating_direction_parent, default: "M", comment: "Use U for up, D for down or M for meh.  Controls how points spent modify the parent's rating."
      t.string :rating_direction_child, default: "M", comment: "Use U for up, D for down or M for meh.  Controls how points spent modify the child's rating."
      t.integer :original_ceremony, default: -1, comment: "The week's award ceremony number when this record was created, 0 if before any ceremonies have been done.  -1 if it hasn't been set yet (in which case it will soon be set and the point boosts added to the parent and child objects)."
      t.timestamps
    end

    add_index :link_bases, :string1
    add_index :link_bases, :number1

    create_table :aux_links, force: false, comment: "AuxLink class and record for connecting LedgerObjects (usually LedgerDelete or LedgerApprove) to LinkBase records (usually links being deleted or approved)." do |t|
      t.references :parent, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_auxlinkparent"}, comment: "Points to the LedgerBase object (or subclass) which has the delete or undelete or give permission order."
      t.references :child, null: false, foreign_key: {to_table: :link_bases, name: "fk_rails_auxlinkchild"}, comment: "Points to the child LinkBase object (or subclass) which is being modified by the parent."
    end

    create_table :aux_ledgers, force: false, comment: "AuxLedger class and record for connecting LedgerBase records (usually LedgerDelete) to other LedgerBase records (usually objects being deleted)." do |t|
      t.references :parent, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_auxledgerparent"}, comment: "Points to the LedgerBase object (or subclass) which has the delete or undelete order."
      t.references :child, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_auxledgerchild"}, comment: "Points to the child LedgerBase object (or subclass) which is being modified by the parent."
    end

    create_table :group_settings, comment: "Extra information for each LedgerFullGroup object is stored in this table." do |t|
      t.references :ledger_full_group, null: false, foreign_key: {to_table: :ledger_bases, name: "fk_rails_groupsettingsgroup"}, comment: "Points to the LedgerFullGroup (a LedgerBase subclass) that this record contains the extra settings for."
      t.boolean :auto_approve_non_member_posts, default: false, comment: "True if posts attached to the group by a non-member are automatically approved (if they have enough points).  False means a moderator will need to approve them."
      t.boolean :auto_approve_member_posts, default: true, comment: "True if posts attached to the group by a member are automatically approved (if they have enough points).  False means a moderator will need to approve them.  Weird if you only auto-approve non-members."
      t.boolean :auto_approve_members, default: false, comment: "True if new member applications are automatically approved (if they have enough points).  False requires approval by a member moderator."
      t.float :min_points_non_member_post, default: -1.0, comment: "Minimum number of net rating points needed when posting as a non-member to get automatic approval.  If they don't have enough, their post will need to be approved by a moderator.  Ignored if automatic approval is turned off."
      t.float :max_points_non_member_post, default: 2.0, comment: "Maximum number of net rating points allowed when posting as a non-member to get automatic approval.  If they have too many, their post will need to be approved by a moderator.  Ignored if automatic approval is turned off."
      t.float :min_points_member_post, default: -10.0, comment: "Minimum number of net rating points needed when posting as a member to get automatic approval.  If they don't have enough, their post will need to be approved by a moderator.  Ignored if automatic approval is turned off."
      t.float :max_points_member_post, default: 10.0, comment: "Maximum number of net rating points allowed when posting as a member to get automatic approval.  If they have too many, their post will need to be approved by a moderator.  Ignored if automatic approval is turned off."
      t.float :min_points_membership, default: 1.0, comment: "Minimum number of net rating points needed when applying to be a member to get automatic approval.  Ignored if automatic approval is turned off.  If they don't get automatically approved, a moderator will need to approve their request."
      t.float :max_points_membership, default: 100.0, comment: "Maximum number of net rating points allowed when applying to be a member to get automatic approval.  Ignored if automatic approval is turned off.  If they don't get automatically approved, a moderator will need to approve their request."
      t.string :wildcard_role_banned, default: "", comment: "Wildcard expression to identify people who are banned, in addition to explicit LinkRole records.  So we can ban friends of banned people, etc."
      t.string :wildcard_role_reader, default: "", comment: "Relationship expression specifying additional people who are allowed to read the group."
      t.string :wildcard_role_member, default: "", comment: "Relationship expression specifying additional people who are considered members of the group, even if they didn't apply."
      t.string :wildcard_role_meta_opinionator, default: "", comment: "Relationship expression specifying additional people who are allowed to rate opinions about messages in this group."
      t.string :wildcard_role_message_moderator, default: "", comment: "Relationship expression specifying additional people who are allowed to moderate (approve/delete) messages."
      t.string :wildcard_role_member_moderator, default: "", comment: "Relationship expression specifying additional people who are allowed to moderate membership requests."
    end
  end
end

Flesh out ledger_base.rb and link_base.rb with a few associations too:

class LedgerBase < ApplicationRecord
  # Always have a creator, but "optional: false" makes it reload the creator
  # object every time we do something with an object.  So just require it to
  # be non-NULL in the database definition.
  belongs_to :creator, class_name: :LedgerBase, optional: true
  has_many :objects_created, class_name: :LedgerBase, foreign_key: :creator_id

  belongs_to :original, class_name: :LedgerBase, optional: true
  belongs_to :amended, class_name: :LedgerBase, optional: true

  has_many :link_downs, class_name: :LinkBase, foreign_key: :parent_id
  has_many :descendants, through: :link_downs, source: :child
  has_many :link_ups, class_name: :LinkBase, foreign_key: :child_id
  has_many :ancestors, through: :link_ups, source: :parent
  has_many :links_created, class_name: :LinkBase, foreign_key: :creator_id

  has_many :aux_ledger_downs, class_name: :AuxLedger, foreign_key: :parent_id
  has_many :aux_ledger_descendants, through: :aux_ledger_downs, source: :child
  has_many :aux_ledger_ups, class_name: :AuxLedger, foreign_key: :child_id
  has_many :aux_ledger_ancestors, through: :aux_ledger_ups, source: :parent
  has_many :aux_link_downs, class_name: :AuxLink, foreign_key: :parent_id
  has_many :aux_link_descendants, through: :aux_link_downs, source: :child

  has_many :link_opinion_targets, class_name: :LinkOpinion, foreign_key: :child_id
  has_many :link_opinion_authors, class_name: :LinkOpinion, foreign_key: :parent_id
  has_many :opinion_targets, through: :link_opinion_targets, source: :child
  has_many :opinion_authors, through: :link_opinion_authors, source: :parent
end

class LinkBase < ApplicationRecord
  belongs_to :parent, class_name: :LedgerBase, optional: false
  belongs_to :child, class_name: :LedgerBase, optional: false
  belongs_to :creator, class_name: :LedgerBase, optional: false

  has_many :aux_link_ups, class_name: :AuxLink, foreign_key: :child_id
  has_many :aux_link_ancestors, through: :aux_link_ups, source: :parent
end

Then do rails db:seed to set up the initial database contents. One key step is making the root LedgerBase object, with itself as its creator.

ActiveRecord::Base.connection.execute(
  "INSERT into ledger_bases
    (id, type, number1, string1, string2, text1, rating_points_spent_creating, rating_points_boost_self, current_meh_points, original_ceremony, current_ceremony, date1, created_at, updated_at)
  VALUES
    (0, 'LedgerUser', 0, 'Root LedgerBase Object', 'agmsmith@ncf.ca', 'The special root object/user which we need to manually create with a creator id of itself.  Then initial system objects can be created with it as their creator.  AGMS20200206', 0.0, 0.0, 1000.0, 0, 0, '0001-01-01 00:00:00', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);"
)

Test Plan - A few Use Cases

A new user writes a message. Another user replies to it. First user edits their message. What does second user see? First user replies to second user’s message. First user deletes their original message.

A user adds some photos and videos to a message. They are separate objects, and need the expendature of rating points to be attached (more points are required for a larger number of bytes). What happens when they run out of points? Do they get attached to the message and show up when it is displayed?

First user sets up a small category tree and adds a message to a couple of categories. Second user navigates tree to see messages about their favourite topic.

Weekly batch processing adds some reputation points to first and second user. First user likes second user, awarding some up points, declaring their appreciation. Second user awards some down points to first user, declaring their disgust. What are their total points? Another weekly batch run happens. What are their points?

Change Log

$Header: /var/www/ratingstone.agmsmith.ca/docs/RCS/Database\040Design.md,v 1.95 2023/09/13 00:47:03 agmsmith Exp $

$Log: Database\040Design.md,v $
Revision 1.95  2023/09/13 00:47:03  agmsmith
Clarifying edit, view permissions for objects and links.

Revision 1.94  2023/09/12 15:39:00  agmscoder
Updated to use current database field declarations.

Revision 1.93  2023/09/12 00:48:36  agmsmith
Proofread up to Auxiliary Classes, fixing wording and clarifying things.

Revision 1.92  2023/09/11 23:38:08  agmsmith
Reviewing the docs, WIP, up to creator_id.

Revision 1.91  2023/04/24 01:04:48  agmsmith
WIP - Adding a section on object permissions, next up do LedgerBase, LedgerPost, more Links.

Revision 1.90  2023/04/05 21:59:43  agmsmith
Add recent new User record fields.

Revision 1.89  2022/12/11 22:50:27  agmsmith
Fix indentation of headings, remove birthday objects.

Revision 1.88  2022/12/11 22:44:43  agmsmith
Clarified LinkOpinion and LinkMetaOpinion, LinkMetaMod redundant.
LedgerContent removed, was unneeded conplexification.
LinkOwner not needed now that all LedgerBase objects have creation fees internally.
Added Quotes functionality to LinkReply.

Revision 1.87  2022/12/11 20:14:54  agmsmith
Change meta_moderator to meta_opinionator for the Role.

Revision 1.86  2022/09/13 19:51:39  agmsmith
Decide that deleted flag in LedgerBase objects applies to all
versions of that object at once.

Revision 1.85  2022/09/04 21:32:26  agmsmith
Don't charge creation to creator for LedgerUser objects, since they
are their own creators and that would leave them with no points
after creation.

Revision 1.84  2022/08/18 18:05:38  agmsmith
Oops, forgot one rating_points_spent_creating negative case.

Revision 1.83  2022/08/18 17:59:12  agmsmith
Set default points for parent/child/self boost to zero.  Can have negative
for amount spent to show that standard amounts will be spent to creat the
object or link, but only at time of creation, not in the database.

Revision 1.82  2022/04/11 23:17:09  agmsmith
Rename field to expiry_ceremony.

Revision 1.81  2022/04/11 21:46:40  agmsmith
Add an end ceremony to LinkBonus.  Avoids a problem with missing points.

Revision 1.80  2022/03/01 21:47:47  agmsmith
Add a virtual Link for creating objects, but represented by fields
within the object rather than a separate Link record.

Revision 1.79  2022/01/31 21:58:19  agmsmith
More on awarding Meh points.  User record described, vs LedgerUser.

Revision 1.78  2022/01/29 21:13:04  agmsmith
Switch to using meh points to receive the weekly allowance of
reputation bonuses.  Solves several problems to do it that way.

Revision 1.77  2022/01/23 01:11:37  agmsmith
Correct award_number to new original_ceremony naming.

Revision 1.76  2021/09/12 21:49:00  agmsmith
Added LinkBonusUnique class rather than adding a boolean to LinkBonus.

Revision 1.75  2021/09/12 21:32:16  agmsmith
More LinkBonus details.

Revision 1.74  2021/09/09 19:59:43  agmsmith
LinkReply and LinkGroupContent can be deleted by users with just approval permission.

Revision 1.73  2021/09/09 01:56:42  agmsmith
Deleting links needs to be more strict, people who can do approval
shouldn't necessarily be able to delete.

Revision 1.72  2021/09/06 19:38:51  agmsmith
Renamed rating_ceremony as original_ceremony to be consistent
with the way the LedgerBase is named.

Revision 1.71  2021/09/05 19:37:35  agmsmith
Reorder so parent field documented before child field in LinkBase.

Revision 1.70  2021/09/02 13:54:18  agmsmith
First draft of LinkBonus.

Revision 1.69  2021/08/25 16:50:38  agmsmith
Added a comment to the award ceremony record.

Revision 1.68  2021/08/24 22:01:21  agmsmith
Remove checkpoint from example of database migration.

Revision 1.67  2021/08/24 21:52:02  agmsmith
Remove the checkpoint idea, it's actually useless.

Revision 1.66  2021/08/24 16:53:00  agmsmith
Work in progress - thinking about checkpointing points.

Revision 1.65  2021/08/22 21:41:41  agmsmith
Add checkpoint for current points at start of week.

Revision 1.64  2021/07/26 14:41:18  agmsmith
Update database migration example for new expiry fields.

Revision 1.63  2021/07/26 14:27:35  agmsmith
Add expired_soon, and delete links when object gets permanently deleted.

Revision 1.62  2021/07/24 22:34:58  agmsmith
Renamed LedgerChangeMarkings to singular, wording updates.

Revision 1.61  2021/07/22 00:01:15  agmsmith
Add expired field to the database migration example.

Revision 1.60  2021/07/21 23:33:36  agmsmith
Change database migration comments back to LedgerDelete or LedgerApprove.

Revision 1.59  2021/07/21 21:57:31  agmsmith
Switch to subclasses for LedgerDelete and LedgerApprove.

Revision 1.58  2021/07/15 01:32:12  agmsmith
After another proofreading run.

Revision 1.57  2021/07/13 23:59:23  agmsmith
Added opinionator role, message moderation and meta opinions updated.
LedgerDelete and LedgerApprove et al replaced by LedgerChangeMarkings.
Ceremony rework in progress.

Revision 1.56  2021/01/16 17:25:05  agmsmith
Typo.

Revision 1.55  2021/01/10 22:42:02  agmsmith
Clarify current_ceremony negative numbers.

Revision 1.54  2021/01/10 20:33:51  agmsmith
More award ceremony clarification.

Revision 1.53  2021/01/08 23:00:03  agmsmith
Explain award ceremony.

Revision 1.52  2021/01/08 18:41:24  agmsmith
Added more award ceremony fields; starting to implement them.

Revision 1.51  2021/01/04 17:22:42  agmsmith
Added LedgerThing and some LinkOpinion clarification.

Revision 1.50  2020/09/20 23:45:52  agmsmith
Need to have a subject for posts, add a string for generic content classes.

Revision 1.49  2020/09/13 20:48:36  agmsmith
Add note to is_latest_version as being useful in SQL queries too.

Revision 1.48  2020/09/11 01:26:12  agmsmith
Make the default of is_latest_version true, since new records are usually
latest.

Revision 1.47  2020/09/10 21:58:31  agmsmith
Add change log to index.

Revision 1.46  2020/09/10 21:57:02  agmsmith
Add is_latest_version field, to optimise version checking.

Revision 1.45  2020/08/06 22:53:07  agmsmith
Update database migration example with current version.

Revision 1.44  2020/08/05 20:42:14  agmsmith
Added has_owners field to LedgerBase.

Revision 1.43  2020/08/04 21:18:02  agmsmith
Add LinkHomeGroup, for user's home page.

Revision 1.42  2020/08/02 20:46:02  agmsmith
Move class structure retrospective regrets to later in the introduction.

Revision 1.41  2020/07/15 20:46:09  agmsmith
amended_id now used in later versions to detect race conditions.

Revision 1.40  2020/07/15 01:21:50  agmsmith
Add a note about multipurpose class structure maybe better than separate object
and links.

Revision 1.39  2020/07/14 00:22:05  agmsmith
Better wording.

Revision 1.38  2020/07/14 00:17:38  agmsmith
Group message moderators have special powers over LinkGroupContent that links
something to their group.

Revision 1.37  2020/07/12 01:00:19  agmsmith
Both ends of the link need to be approved before points awarded.

Revision 1.36  2020/07/10 19:35:35  agmsmith
Mention that the original version's id number is stored in various records.

Revision 1.35  2020/07/07 21:35:52  agmsmith
Put some of the exploratory explanations near the beginning, leaving the class
definitions at the end.

Revision 1.34  2020/07/07 20:51:57  agmsmith
Added meta-moderation idea from a discussion on Slashdot.

Revision 1.33  2020/07/04 16:58:42  agmsmith
No GroupSettings_id field needed, separate table indexed by id of the group
instead.

Revision 1.32  2020/06/30 20:51:44  agmsmith
Added database setup for GroupSettings table.

Revision 1.31  2020/06/30 00:24:46  agmsmith
Proof-reading pass.  Fix conceptual problem - no owners of links.  Also clarify
who can delete links (if you need approval, you can delete it too).

Revision 1.30  2020/06/29 20:27:42  agmsmith
Nope, not owners, else you could get someone to preapprove a relationship.

Revision 1.29  2020/06/29 15:43:14  agmsmith
Work out Relationship class structure, write up a bit more about relationships
and the dimensions of symmetry and approval.

Revision 1.28  2020/06/29 01:01:00  agmsmith
Added relationships section.

Revision 1.27  2020/06/27 21:30:08  agmsmith
Proof reading.  Added Subgroups.  Wildcard language.

Revision 1.26  2020/06/26 00:37:21  agmsmith
Added LinkRelationship, removed LinkPermission.

Revision 1.25  2020/06/25 00:06:50  agmsmith
Decide on making ungrouped posts public.  Add LinkOwner record.

Revision 1.24  2020/06/24 11:18:49  agmsmith
More work in progress on permission roles and discussion groups.

Revision 1.23  2020/06/21 00:20:58  agmsmith
Working on the Permissions Model.  Idea of Groups for all posts, roles.

Revision 1.22  2020/06/20 17:14:23  agmsmith
Rework approval vs deletion of records.  Now have separate child and parent end
of the link approval flags.  Delete is orthogonal to approval.

Revision 1.21  2020/06/15 15:56:25  agmsmith
Rewording, also combine pending and deleted functionality into the same thing
(granting permission undeletes a record).

Revision 1.20  2020/04/10 15:58:33  agmsmith
Have original_id be NULL on creation, but soon changing to copy of id.

Revision 1.19  2020/04/10 15:40:04  agmsmith
Update migration comment for original_id.

Revision 1.18  2020/04/10 15:37:19  agmsmith
Change original_id to be equal to id for the original record, not NULL.

Revision 1.17  2020/03/28 16:19:43  agmsmith
Split out AuxLink and AuxLedger records, making things simpler again.

Revision 1.16  2020/03/28 13:18:21  agmsmith
Typos.

Revision 1.15  2020/03/27 19:03:58  agmsmith
Minor typos fixed.

Revision 1.14  2020/03/26 20:25:45  agmsmith
Rethought deleting and permissions.

Revision 1.13  2020/02/20 21:45:08  agmsmith
Fix typos after proofreading.

Revision 1.12  2020/02/07 15:28:32  agmsmith
Mention initialising the root object in the database.

Revision 1.11  2020/02/04 22:20:21  agmsmith
Proof reading pass done.  Also added list design choices.

Revision 1.10  2020/02/03 22:33:40  agmsmith
Fix up formatting in kramdown for preformatted code.

Revision 1.9  2020/02/03 22:26:20  agmsmith
Added command lines needed to generate the database.

Revision 1.8  2020/02/02 22:15:24  agmsmith
Added permissions records.

Revision 1.7  2020/02/02 21:44:52  agmsmith
After some experiments, decided to use floating point rating points.

Revision 1.6  2020/01/31 21:54:25  agmsmith
Work in progress adding fields and descriptions, worrying about deleting.

Revision 1.5  2020/01/30 22:16:28  agmsmith
Starting to add fields to the base object record.

Revision 1.4  2020/01/30 15:45:39  agmsmith
Explain the ledger write-once system and single table inheritance.

Revision 1.3  2020/01/30 14:40:09  agmsmith
Writing continues, work in progress.

Revision 1.2  2020/01/29 22:52:13  agmsmith
Fixed up formatting of RCS log and version number.

Revision 1.1  2020/01/29 22:43:07  agmsmith
Initial revision