1 package SL::Helper::UserPreferences;
 
   4 use parent qw(Rose::Object);
 
   7 use SL::DBUtils qw(selectall_hashref_query selectfirst_hashref_query do_query selectall_ids);
 
  10 use Rose::Object::MakeMethods::Generic (
 
  11  'scalar --get_set_init' => [ qw(login namespace upgrade_callbacks current_version auto_store_back) ],
 
  15   my ($self, $key, $value) = @_;
 
  17   SL::DB->client->with_transaction(sub {
 
  18     my $tuple = $self->get_tuple($key);
 
  20     if ($tuple && $tuple->{id}) {
 
  21       $tuple->{value}  = $value;
 
  22       $self->_update($tuple);
 
  24       my $query = 'INSERT INTO user_preferences (login, namespace, version, key, value) VALUES (?, ?, ?, ?, ?)';
 
  25       do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace, $self->current_version, $key, $value);
 
  28   }) or do { die SL::DB->client->error };
 
  32   my ($self, $key) = @_;
 
  34   my $tuple = $self->get_tuple($key);
 
  36   $tuple ? $tuple->{value} : undef;
 
  40   my ($self, $key) = @_;
 
  44   SL::DB->client->with_transaction(sub {
 
  45     $tuple = selectfirst_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace, $key);
 
  46       SELECT * FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?
 
  48     if ($tuple && $tuple->{version} < $self->current_version) {
 
  49       $self->_upgrade($tuple);
 
  52     if ($tuple && $tuple->{version} > $self->current_version) {
 
  53       die "Future version $tuple->{version} for user preference @{ $self->namespace }/$key. Expected @{ $self->current_version } or less.";
 
  56   }) or do { die SL::DB->client->error };
 
  66   SL::DB->client->with_transaction(sub {
 
  67     $data = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace);
 
  68       SELECT * FROM user_preferences WHERE login = ? AND namespace = ?
 
  70     for my $tuple (@$data) {
 
  71       if ($tuple->{version} < $self->current_version) {
 
  72         $self->_upgrade($tuple);
 
  75       if ($tuple->{version} > $self->current_version) {
 
  76         die "Future version $tuple->{version} for user preference @{ $self->namespace }/$tuple->{key}. Expected @{ $self->current_version } or less.";
 
  80   }) or do { die SL::DB->client->error };
 
  88   my @keys = selectall_ids($::form, $::form->get_standard_dbh, <<"", 0, $self->login, $self->namespace);
 
  89     SELECT key FROM user_preferences WHERE login = ? AND namespace = ?
 
  95   my ($self, $key) = @_;
 
  97   die 'delete without  key is not allowed, use delete_all instead' unless $key;
 
  99   SL::DB->client->with_transaction(sub {
 
 100     my $query =  'DELETE FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?';
 
 101     do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace, $key);
 
 103   }) or do { die SL::DB->client->error };
 
 107   my ($self, $key) = @_;
 
 111   SL::DB->client->with_transaction(sub {
 
 112     my $query = 'DELETE FROM user_preferences WHERE login = ? AND namespace = ?';
 
 113     do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace);
 
 115   }) or do { die SL::DB->client->error };
 
 121   my ($self, $tuple) = @_;
 
 123   for my $to_version (sort { $a <=> $b } grep { $_ > $tuple->{version} } keys %{ $self->upgrade_callbacks }) {
 
 124     $tuple->{value}   = $self->upgrade_callbacks->{$to_version}->($tuple->{value});
 
 125     $tuple->{version} = $to_version;
 
 128   if ($self->auto_store_back) {
 
 129     $self->_update($tuple);
 
 134   my ($self, $tuple) = @_;
 
 136   my $query = 'UPDATE user_preferences SET version = ?, value = ? WHERE id = ?';
 
 137   do_query($::form, $::form->get_standard_dbh, $query, $tuple->{version}, $tuple->{value}, $tuple->{id});
 
 142 sub init_login             { SL::DB::Manager::Employee->current->login    }
 
 143 sub init_namespace         { ref $_[0]                                    }
 
 144 sub init_upgrade_callbacks { +{}                                          }
 
 145 sub init_current_version   { version->parse((ref $_[0])->VERSION)->numify }
 
 146 sub init_auto_store_back   { 1                                            }
 
 156 SL::Helper::UserPreferences - user based preferences store
 
 160   use SL::Helper::UserPreferences;
 
 161   my $user_pref = SL::Helper::UserPreferences->new(
 
 162     login             => $login,        # defaults to current user
 
 163     namespace         => $namespace,    # defaults to current package
 
 164     upgrade_callbacks => $upgrade_callbacks,
 
 165     current_version   => $version,      # defaults to __PACKAGE__->VERSION->numify
 
 166     auto_store_back   => 0,             # default 1
 
 169   $user_pref->store($key, $value);
 
 170   my $val    = $user_pref->get($key);
 
 171   my $tuple  = $user_pref->get_tuple($key);
 
 172   my $tuples = $user_pref->get_all;
 
 173   my $keys   = $user_pref->get_keys;
 
 174   $user_pref->delete($key);
 
 175   $user_pref->delete_all;
 
 179 This module provides a generic storage for information that needs to be stored
 
 180 between sessions per user and per client and between versions of the program.
 
 182 The storage can be accessed as a generic key/value dictionary, but also
 
 183 requires a namespace to avoid clashes and a version of the information.
 
 184 Additionally you must provide means to upgrade or invalidate stored information
 
 185 that is out of date, i.e. after a program upgrade.
 
 193 Creates a new instance. Available C<PARAMS>:
 
 199 The user for this storage. Defaults to current user login.
 
 203 A unique namespace. Defaults to the calling package.
 
 205 =item C<upgrade_callbacks>
 
 207 A hashref with version numbers as keys and subs as values. These subs are
 
 208 expected to take a value and return an upgraded value for the version of their
 
 211 No default. Mandatory.
 
 213 =item C<current_version>
 
 215 The version object that is considered current for stored information. Defaults
 
 216 to the version of the calling package. MUST be a number, and not a version
 
 217 object, so that versions can be used as hash keys in the ugrade_callbacks.
 
 219 =item C<auto_store_back>
 
 221 An otional flag indicating whether values from the database that were upgraded to a
 
 222 newer version should be stored back automatically. Defaults to
 
 223 C<$::lx_office_conf{debug}{auto_store_back_upgraded_user_preferences}> which in
 
 224 turn defaults to true.
 
 228 =item C<store KEY VALUE>
 
 230 Stores a key-value tuple. If there exists already a value for this key, it will
 
 237 Returns the value. If no such value exists returns undef instead.
 
 239 This is for easy of use, and does no distinction between non-existing values
 
 240 and valid undefined values. Use C<get_tuple> if you need this.
 
 242 =item C<get_tuple KEY>
 
 244 Retrieves a key-value tuple.
 
 246 Returns a hashref with C<key> and C<value> entries. If no such value
 
 247 exists returns undef instead.
 
 251 Retrieve all key-value tuples in this namespace and user.
 
 253 Returns an arrayref of hashrefs.
 
 257 Retrieve all keys for this namespace. Note: Unless you store vast amount of
 
 258 data, it's most likely easier to just C<get_all>.
 
 260 Returns an arrayref of keys.
 
 268 Delete all tuples for this namespace and user.
 
 274 Every entry in the user prefs must have a version to be compatible in case of
 
 277 Code reading user prefs must check if the version is the expected one, and must
 
 278 have upgrade code to upgrade out of date preferences to the current version.
 
 280 Code SHOULD write the upgraded version back to the store at the earliest time
 
 281 to keep preferences up to date. This should be able to be disabled to have
 
 282 developer versions not overwrite preferences with unsupported versions.
 
 286 Initial code dealing with prefs:
 
 290   $user_prefs->store("selected tab", $::form->{selected_tab});
 
 292 And the someone edits the code and removes the tab "Webdav". To ensure
 
 293 favorites with webdav selected are upgraded:
 
 297   my $upgrade_callbacks = {
 
 298     2 => sub { $_[0] eq 'WebDav' ? 'MasterData' : $_[0]; },
 
 301   my $val = $user_prefs->get("selected tab");
 
 303 =head1 LACK OF TYPING
 
 305 This controller will not attempt to preserve types. All data will be
 
 306 stringified. If your code needs to preserve numbers, you MUST encode the data
 
 307 to JSON or YAML before storing.
 
 309 =head1 PLANNED BEST PRACTICE
 
 311 To be able to decouple controllers and the schema upgrading required for this,
 
 312 there should be exactly one module responsible for managing user preferences for
 
 313 each namespace. You should find the corresponding preferences owners in the
 
 314 class namespace C<SL::Helper::UserPreferences>.
 
 316 For example the namespace C<PartsSearchFavorites> should only be managed by
 
 317 C<SL::Helper::UserPreferences::PartsSearchFavorites>. This way, it's possible
 
 318 to keep the upgrades in one place, and to migrate upgrades out of there into
 
 319 database upgrades during major releases. They also don't clutter up
 
 322 It is planned to strip all modules located there of their upgrade for a release
 
 323 and do automatic database upgrades.
 
 325 To avoid version clashes when developing customer branches, please only use
 
 326 stable version bumps in the unstable branch, and use dev versions in customer
 
 335 If a (namepace, key) tuple exists, a store will overwrite the last version
 
 339 If the value retrieved from the database is newer than the code version, an
 
 340 error must be thrown.
 
 344 get will check the version against the current version and apply all upgrade
 
 349 If the final step is not the current version, behaviour is undefined
 
 353 get_all will always return scalar context.
 
 357 =head1 TODO AND SPECIAL CASES
 
 359 * not defined whether it should be possible to retrieve the version of a tuple
 
 361 * it's not specified how to return invalidation from upgrade, nor how to handle
 
 364 * it's not specified whether admin is a user. for now it dies.
 
 366 * We're missing user agnostic methods for database upgrades
 
 374 Sven Schöling <s.schoeling@linet-services.de>