Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 8d24868f

Von Sven Schöling vor fast 8 Jahren hinzugefügt

  • ID 8d24868f33a1f969b873713b5474d9e4599cda6b
  • Vorgänger a8c9873e
  • Nachfolger f40363c5

UserPreferences: erste version

Unterschiede anzeigen:

SL/Helper/UserPreferences.pm
1
package SL::Helper::UserPreferences;
2

  
3
use strict;
4
use parent qw(Rose::Object);
5
use version;
6

  
7
use SL::DBUtils qw(selectall_hashref_query selectfirst_hashref_query do_query selectall_ids);
8

  
9
use Rose::Object::MakeMethods::Generic (
10
 'scalar --get_set_init' => [ qw(login namespace upgrade_callbacks current_version auto_store_back) ],
11
);
12

  
13
sub store {
14
  my ($self, $key, $value) = @_;
15

  
16
  my $tuple = $self->get_tuple($key);
17

  
18
  if ($tuple) {
19
    $tuple->{value}  = $value;
20
    $self->_update($tuple);
21
  } else {
22
    my $query = 'INSERT INTO user_preferences (login, namespace, version, key, value) VALUES (?, ?, ?, ?, ?)';
23
    do_query($::form, $::form->get_standard_dbh, $query, $self->login, $self->namespace, $self->current_version, $key, $value);
24
  }
25
}
26

  
27
sub get {
28
  my ($self, $key) = @_;
29

  
30
  my $tuple = $self->get_tuple($key);
31

  
32
  $tuple ? $tuple->{value} : undef;
33
}
34

  
35
sub get_tuple {
36
  my ($self, $key) = @_;
37

  
38
  my $tuple = selectfirst_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace, $key);
39
    SELECT * FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?
40

  
41
  if ($tuple && $tuple->{version} < $self->current_version) {
42
    $self->_upgrade($tuple);
43
  }
44

  
45
  if ($tuple && $tuple->{version} > $self->current_version) {
46
    die "Future version $tuple->{version} for user preference @{ $self->namespace }/$key. Expected @{ $self->current_version } or less.";
47
  }
48

  
49
  return $tuple;
50
}
51

  
52
sub get_all {
53
  my ($self) = @_;
54

  
55
  my $data = selectall_hashref_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace);
56
    SELECT * FROM user_preferences WHERE login = ? AND namespace = ?
57

  
58
  for my $tuple (@$data) {
59
    if ($tuple->{version} < $self->current_version) {
60
      $self->_upgrade($tuple);
61
    }
62

  
63
    if ($tuple->{version} > $self->current_version) {
64
      die "Future version $tuple->{version} for user preference @{ $self->namespace }/$tuple->{key}. Expected @{ $self->current_version } or less.";
65
    }
66
  }
67

  
68
  return $data;
69
}
70

  
71
sub get_keys {
72
  my ($self) = @_;
73

  
74
  my @keys = selectall_ids($::form, $::form->get_standard_dbh, <<"", 0, $self->login, $self->namespace);
75
    SELECT key FROM user_preferences WHERE login = ? AND namespace = ?
76

  
77
  return @keys;
78
}
79

  
80
sub delete {
81
  my ($self, $key) = @_;
82

  
83
  die 'delete without  key is not allowed, use delete_all instead' unless $key;
84

  
85
  my @keys = do_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace, $key);
86
    DELETE FROM user_preferences WHERE login = ? AND namespace = ? AND key = ?
87

  
88
}
89

  
90
sub delete_all {
91
  my ($self, $key) = @_;
92

  
93
  my @keys = do_query($::form, $::form->get_standard_dbh, <<"", $self->login, $self->namespace);
94
    DELETE FROM user_preferences WHERE login = ? AND namespace = ?
95

  
96
}
97

  
98
### internal stuff
99

  
100
sub _upgrade {
101
  my ($self, $tuple) = @_;
102

  
103
  for my $to_version (sort { $a <=> $b } grep { $_ > $tuple->{version} } keys %{ $self->upgrade_callbacks }) {
104
    $tuple->{value}   = $self->upgrade_callbacks->{$to_version}->($tuple->{value});
105
    $tuple->{version} = $to_version;
106
  }
107

  
108
  if ($self->auto_store_back) {
109
    $self->_update($tuple);
110
  }
111
}
112

  
113
sub _update {
114
  my ($self, $tuple) = @_;
115

  
116
  my $query = 'UPDATE user_preferences SET version = ?, value = ? WHERE id = ?';
117
  do_query($::form, $::form->get_standard_dbh, $query, $tuple->{version}, $tuple->{value}, $tuple->{id});
118
}
119

  
120
### defaults stuff
121

  
122
sub init_login             { SL::DB::Manager::Employee->current->login    }
123
sub init_namespace         { ref $_[0]                                    }
124
sub init_upgrade_callbacks { +{}                                          }
125
sub init_current_version   { version->parse((ref $_[0])->VERSION)->numify }
126
sub init_auto_store_back   { 1                                            }
127

  
128
1;
129

  
130
__END__
131

  
132
=encoding utf-8
133

  
134
=head1 NAME
135

  
136
SL::Helper::UserPreferences - user based preferences store
137

  
138
=head1 SYNOPSIS
139

  
140
  use SL::Helper::UserPreferences;
141
  my $user_pref = SL::Helper::UserPreferences->new(
142
    login             => $login,        # defaults to current user
143
    namespace         => $namespace,    # defaults to current package
144
    upgrade_callbacks => $upgrade_callbacks,
145
    current_version   => $version,      # defaults to __PACKAGE__->VERSION->numify
146
    auto_store_back   => 0,             # default 1
147
  );
148

  
149
  $user_pref->store($key, $value);
150
  my $val    = $user_pref->get($key);
151
  my $tuple  = $user_pref->get_tuple($key);
152
  my $tuples = $user_pref->get_all;
153
  my $keys   = $user_pref->get_keys;
154
  $user_pref->delete($key);
155
  $user_pref->delete_all;
156

  
157
=head1 DESCRIPTION
158

  
159
This module provides a generic storage for information that needs to be stored
160
between sessions per user and per client and between versions of the program.
161

  
162
The storage can be accessed as a generic key/value dictionary, but also
163
requires a namespace to avoid clashes and a version of the information.
164
Additionally you must provide means to upgrade or invalidate stored information
165
that is out of date, i.e. after a program upgrade.
166

  
167
=head1 FUNCTIONS
168

  
169
=over 4
170

  
171
=item C<new PARAMS>
172

  
173
Creates a new instance. Available C<PARAMS>:
174

  
175
=over 4
176

  
177
=item C<login>
178

  
179
The user for this storage. Defaults to current user login.
180

  
181
=item C<namespace>
182

  
183
A unique namespace. Defaults to the calling package.
184

  
185
=item C<upgrade_callbacks>
186

  
187
A hashref with version numbers as keys and subs as values. These subs are
188
expected to take a value and return an upgraded value for the version of their
189
key.
190

  
191
No default. Mandatory.
192

  
193
=item C<current_version>
194

  
195
The version object that is considered current for stored information. Defaults
196
to the version of the calling package. MUST be a number, and not a version
197
object, so that versions can be used as hash keys in the ugrade_callbacks.
198

  
199
=item C<auto_store_back>
200

  
201
An otional flag indicating whether values from the database that were upgraded to a
202
newer version should be stored back automatically. Defaults to
203
C<$::lx_office_conf{debug}{auto_store_back_upgraded_user_preferences}> which in
204
turn defaults to true.
205

  
206
=back
207

  
208
=item C<store KEY VALUE>
209

  
210
Stores a key-value tuple. If there exists already a value for this key, it will
211
be overwritten.
212

  
213
=item C<get KEY>
214

  
215
Retrieves a value.
216

  
217
Returns the value. If no such value exists returns undef instead.
218

  
219
This is for easy of use, and does no distinction between non-existing values
220
and valid undefined values. Use C<get_tuple> if you need this.
221

  
222
=item C<get_tuple KEY>
223

  
224
Retrieves a key-value tuple.
225

  
226
Returns a hashref with C<key> and C<value> entries. If no such value
227
exists returns undef instead.
228

  
229
=item C<get_all>
230

  
231
Retrieve all key-value tuples in this namespace and user.
232

  
233
Returns an arrayref of hashrefs.
234

  
235
=item C<get_keys>
236

  
237
Retrieve all keys for this namespace. Note: Unless you store vast amount of
238
data, it's most likely easier to just C<get_all>.
239

  
240
Returns an arrayref of keys.
241

  
242
=item C<delete KEY>
243

  
244
Deletes a tuple.
245

  
246
=item C<delete_all>
247

  
248
Delete all tuples for this namespace and user.
249

  
250
=back
251

  
252
=head1 VERSIONING
253

  
254
Every entry in the user prefs must have a version to be compatible in case of
255
code upgrades.
256

  
257
Code reading user prefs must check if the version is the expected one, and must
258
have upgrade code to upgrade out of date preferences to the current version.
259

  
260
Code SHOULD write the upgraded version back to the store at the earliest time
261
to keep preferences up to date. This should be able to be disabled to have
262
developer versions not overwrite preferences with unsupported versions.
263

  
264
Example:
265

  
266
Initial code dealing with prefs:
267

  
268
  our $VERSION = v1;
269

  
270
  $user_prefs->store("selected tab", $::form->{selected_tab});
271

  
272
And the someone edits the code and removes the tab "Webdav". To ensure
273
favorites with webdav selected are upgraded:
274

  
275
  our $VERSION = v2;
276

  
277
  my $upgrade_callbacks = {
278
    2 => sub { $_[0] eq 'WebDav' ? 'MasterData' : $_[0]; },
279
  };
280

  
281
  my $val = $user_prefs->get("selected tab");
282

  
283
=head1 LACK OF TYPING
284

  
285
This controller will not attempt to preserve types. All data will be
286
stringified. If your code needs to preserve numbers, you MUST encode the data
287
to JSON or YAML before storing.
288

  
289
=head1 PLANNED BEST PRACTICE
290

  
291
To be able to decouple controllers and the schema upgrading required for this,
292
there should be exactly one module responsible for managing user preferences for
293
each namespace. You should find the corresponding preferences owners in the
294
class namespace C<SL::Helper::UserPreferences>.
295

  
296
For example the namespace C<PartsSearchFavorites> should only be managed by
297
C<SL::Helper::UserPreferences::PartsSearchFavorites>. This way, it's possible
298
to keep the upgrades in one place, and to migrate upgrades out of there into
299
database upgrades during major releases. They also don't clutter up
300
controllers.
301

  
302
It is planned to strip all modules located there of their upgrade for a release
303
and do automatic database upgrades.
304

  
305
To avoid version clashes when developing customer branches, please only use
306
stable version bumps in the unstable branch, and use dev versions in customer
307
branches.
308

  
309
=head1 BEHAVIOUR
310

  
311
=over 4
312

  
313
=item *
314

  
315
If a (namepace, key) tuple exists, a store will overwrite the last version
316

  
317
=item *
318

  
319
If the value retrieved from the database is newer than the code version, an
320
error must be thrown.
321

  
322
=item *
323

  
324
get will check the version against the current version and apply all upgrade
325
steps.
326

  
327
=item *
328

  
329
If the final step is not the current version, behaviour is undefined
330

  
331
=item *
332

  
333
get_all will always return scalar context.
334

  
335
=back
336

  
337
=head1 TODO AND SPECIAL CASES
338

  
339
* not defined whether it should be possible to retrieve the version of a tuple
340

  
341
* it's not specified how to return invalidation from upgrade, nor how to handle
342
  that
343

  
344
* it's not specified whether admin is a user. for now it dies.
345

  
346
* We're missing user agnostic methods for database upgrades
347

  
348
=head1 BUGS
349

  
350
None yet :)
351

  
352
=head1 AUTHOR
353

  
354
Sven Schöling <s.schoeling@linet-services.de>
355

  
356
=cut
sql/Pg-upgrade2/user_preferences.sql
1
-- @tag: user_preferences
2
-- @description: Benutzereinstellungen
3
-- @depends: release_3_4_1
4
-- @encoding: utf-8
5

  
6
CREATE TABLE user_preferences (
7
  id         SERIAL PRIMARY KEY,
8
  login      TEXT NOT NULL,
9
  namespace  TEXT NOT NULL,
10
  version    NUMERIC(15,5),
11
  key        TEXT NOT NULL,
12
  value      TEXT,
13
  UNIQUE (login, namespace, version, key)
14
);
t/helper/user_preferencess.t
1
use Test::More;
2
use Test::Exception;
3
use Test::Deep qw(bag cmp_deeply);
4

  
5
use strict;
6
use lib 't';
7

  
8
use Support::TestSetup;
9
use_ok 'SL::Helper::UserPreferences';
10

  
11
Support::TestSetup::login();
12

  
13
my $prefs;
14
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1 ];
15

  
16

  
17
$prefs->store('test1', "val");
18
$prefs->store('test2', "val2");
19

  
20
cmp_deeply [ $prefs->get_keys ], bag('test1', 'test2'), 'get_keys works';
21

  
22
is $prefs->get('test1'), 'val', 'get works';
23
is $prefs->get_tuple('test2')->{value}, 'val2', 'get tuple works';
24
is $prefs->get_all->[1]{value}, 'val2', 'get all works';
25
is scalar @{ $prefs->get_all }, 2, 'get all works 2';
26

  
27
$prefs = new_ok 'SL::Helper::UserPreferences', [
28
  current_version => 2,
29
  upgrade_callbacks => {
30
    2 => sub { my ($val) = @_; $val . ' in space!'; }
31
  }
32
];
33

  
34
is $prefs->get('test1'), 'val in space!', 'upgrading works';
35

  
36
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
37
is $prefs->get('test1'), 'val in space!', 'auto store back works';
38

  
39
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1, namespace => 'namespace2' ];
40
is $prefs->get('test1'), undef, 'other namespace does not find prior data';
41

  
42
$prefs->store('test1', "namespace2 test");
43
is $prefs->get('test1'), 'namespace2 test', 'other namespace finds data with same key';
44

  
45
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
46
is $prefs->get('test1'), 'val in space!', 'original namepsace is not affected';
47

  
48
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1, login => 'demo2' ];
49
$prefs->store('test1', "login test");
50

  
51
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
52
is $prefs->get('test1'), 'val in space!', 'original login is not affected';
53

  
54
$prefs->store('test1', 'new value');
55
is scalar @{ $prefs->get_all }, 2, 'storing an existing value overwrites';
56

  
57
my @array = $prefs->get_all;
58
is scalar @array, 1, 'get_all in list context returns 1 element';
59
isa_ok $array[0], 'ARRAY', 'get_all in list context returns 1 arrayref';
60

  
61
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 1 ];
62
dies_ok { $prefs->get('test1') } 'reading newer version dies';
63

  
64
$prefs = new_ok 'SL::Helper::UserPreferences', [ current_version => 2 ];
65
$prefs->delete('test1');
66
is $prefs->get('test1'), undef, 'deleting works';
67

  
68
$prefs->delete_all;
69
is $prefs->get('test2'), undef, 'delete_all works';
70

  
71
done_testing;

Auch abrufbar als: Unified diff