Userprops are defined in bin/upgrading/proplists.dat. They're a quick way to store preferences, settings, and information about the user, such as the values of choices they've made in the past. If you're looking for a quick way to store any data about the user, without having to create a whole new database table, your first thought should be "userprop".
(There are other things in proplists.dat along the same lines -- talkprops, which apply to comment objects, and logprops, which apply to entries -- but for the purpose of this walkthrough, we'll assume that you're dealing with userprops.)
When to use userprops
- Are you trying to store a yes/no for the user, like whether or not they have a specific feature on or off? If so, you should make a new userprop.
- Are you trying to store a trinary or multiple-value option, like the option for what email address to show on the profile for paid users? (none, system email address, user-specified email address, local (@dreamwidth.org) email address, system + local, user + local) If so, you should make a new userprop.
- Are you trying to store a small amount of text as a preference, like the customized crosspost footer text or the username of the person who invited the user? If so, you should make a new userprop.
- Are you trying to store multiple things that all depend on each other, or things that have the possibility to get really long, or things that there can be a lot of (like tags or memories or vgifts received/sent)? If so, a userprop won't help you -- that's getting into "new database table" territory.
On your development environment, give your working user the priv canview:userprops or canview:* and go to /admin/propedit.bml. That lets you see all of the userprops defined on your dev environment, and manipulate them as necessary. It's a good idea to keep this page handy, because while you develop, you'll want to refer to it as you go.
Userprops are found in bin/upgrading/proplists.dat and look like this:
userproplist.icq: cldversion: 0 datatype: char des: ICQ Number indexed: 1 multihomed: 1 prettyname: ICQ
To create a new userprop, add it into bin/upgrading/proplists.dat. It's okay to copy an existing userprop definition and just change what you want to change.
Once you have your new userprop created, save the file and tell the database to reload the file:
$LJHOME/bin/upgrading/update-db.pl -r -p --innodb
Then go to /admin/propedit and verify that your new userprop exists.
For full descriptions of each section of the user prop definition see the sections below, this is a short summary:
The lines you probably want to change are 'des' and 'prettyname'. Prettyname should be a short, explanatory, human-understandable title for the userprop, and 'des' should be a description of what it does and what the possible values are (and what they mean). Documenting the possible values and what they do here will save you and others a hell of a lot of time code-diving later, so be sure you do it!
"Datatype" is something else you can change if you want to. The two you'll most usually use are 'char' (characters) and 'num' (for numbers), although there's also 'bool' for boolean (on/off). It's usually a better idea to go with 'char' as a type for greatest flexibility -- if you set something up as boolean and later on discover that you need another option, it's annoying to re-do them, while if you set it up as 'char' you can treat it as boolean in the code and still have the ability to expand later.
In most situations, you want cldversion = 4, indexed = 0, multihomed = 0, type = bool/char. That will do for almost all settings that a user might have.
For things that you want to be searched on the global database (things that are closer to a 1:1 relationship from values to users) then you can consider cldversion = 0, indexed = 1, multihomed = 1, type = bool/char.
Avoid type = blobchar, or talk to a senior developer if you need one.
CLustered Data VERSION. The dversion of a user is, in effect, which database tables they are using. We are currently on dversion 8. This field tells the userprop system "in which dversion was this property clustered".
To make this more explicit, this helps tell the system where to find the data for the property. A value of 0 means "this property is not clustered, find it on the global". A value that is non-zero but less than or equal to the current user's dversion means the data is on one of the user cluster databases. If the value is greater than the user's dversion, then this property will be on the user clusters, but is currently on the global for this user.
If that made no sense, that's OK. Keep reading.
Will we ever want to do a reverse lookup on this property? I.e., to find which users have a value of X for this property? If YES, then cldversion = 0.
For example, it might be useful to say "hey, who has ICQ number 2903909?" and then look that up and say "oh, Mark does!" In that case, you want the property all in one location (on the global database), so set cldversion = 0.
On the other hand, if you have a property that is a setting, you don't need to do a reverse lookup. "Who has opted out of the latest posts feed?" is a question we won't ask, because the answer is (probably) "lots of people". In that case, set cldversion = 4.
Why "4"? It's traditional at this point. You could set it to anything from 1 to 8 and it would have the same impact. Just set it to 4. :-)
You almost always want this value set to 4 unless you really know you need it to be 0.
There are four valid values: char, num, bool, blobchar.
These don't matter much right now. The only thing that matters is if you use blobchar, you get forced onto the user clusters (cldversion and indexed become irrelevant). You can, however, store up to ~16MB of data in the property. (Please don't.)
All other values are business as usual. One day they might be necessary or useful but they aren't for now ...
If you think you need a blobchar property, you should probably talk to some senior developers and see what they think.
A simple, short phrase describing what this property does. It doesn't have to be long or anything, it's just referenced when we look at the file to figure out what it this property is. I'm not even sure if we use this on the site anywhere.
This is a boolean value, either 1 or 0. The implication is similar to the cldversion questions: if you need to look up users from a value, then you want to have indexed set to 1.
This value only matters if cldversion is 0! Only properties that are stored on the global can be indexed.
Most of the time you want this to be 0.
This is a useful property that doesn't matter for us right now at this scale, but for huge installations like LJ, it was a definite bacon saver.
The gist of it is that setting this allows you to have a property stored on the global database and the user clusters. Some properties have cldversion = 0, indexed = 1 because you want to be able to look them up in reverse (who has this AIM name? etc), but that puts the property on the global. This causes you to have to hit the global whenever you do something as simple as render a profile page, because those properties are only on the global!
Setting multihomed lets the property live in two places. You pay the cost of having to write twice every time the user changes the property, but it lets you get fast reads (from the user clusters) as well as indexed reads (from the global).
Most of the time you want to set this to 0 unless you have a cldversion = 0, indexed = 1 property. In that case, set it to 1.
Title case proper name for a property. Similar to the description.
Working with userprops
To manipulate your userprops in-code, there are a bunch of functions you can call. Most of them are defined in cgi-bin/LJ/User/Permissions.pm, in section 8 (Userprops, Caps, and Displaying Content to Others).
The most useful functions and how they should be called:
$u->prop( userprop ); # returns the current value of the userprop 'userprop' for user object $u $u->set_prop( userprop, $value ); # sets the userprop 'userprop' to value '$value' for user object $u $u->clear_prop( userprop ); # clears the value of the userprop 'userprop' (sets it to undef) for user object $u
Best practice tips
- Remember that when you create a new userprop, everyone who already has created an account on the site won't have the userprop set. Your code should take that into account and assume smart defaults for what to do when the userprop isn't defined, which will also make the choice for what to do for new accounts. (See some of the various functions in section 8 of cgi-bin/LJ/User/Permissions.pm cgi-bin/LJ/User/Permissions.pm, which provides some examples of how to set defaults and assume the proper behavior for userprops.)
- If you're accepting user input to set the userprop -- for instance, if you're not setting it based on the user's choice of a drop-down box or a checkbox in a HTML form, but as part of a text input form -- be sure to run it through one of the user-input sanitizing and escaping functions in cgi-bin/ljtextutil.pl.
- You're probably going to want to set, clear, and manipulate your userprops through a Settings package, so they can appear on one of the tabs of manage/settings/.
- If you want your userprop to be settable through the admin console "set" command, you need to create a section for it in cgi-bin/LJ/Hooks/Setters.pm. Unless you add it here, it won't be settable or manipulatable via the admin console.
- When you're working with userprop values in-code, it's a good idea to make sure that you provide an alternative for what to do if the userprop has a value that shouldn't be possible, just in case.
- If you want newly-created users to have a specific setting for the userprop, instead of just assuming what to do in-code, you can add it to the %USERPROP_INIT section of etc/config.pl. You should only do this after talking with Denise and/or Mark about it, though, and be sure to note that you did this in the commit message accompanying your pull request for the issue, because Mark doesn't allow changes in the repository to overwrite the config files on Dreamwidth itself and will have to manually add the line after your changes go live.