File Coverage

blib/lib/Rubric/User.pm
Criterion Covered Total %
statement 99 99 100.0
branch 33 36 91.6
condition 21 21 100.0
subroutine 17 17 100.0
pod 10 10 100.0
total 180 183 98.3


line stmt bran cond sub pod time code
1 12     12   14230 use strict;
  12         23  
  12         412  
2 12     12   66 use warnings;
  12         23  
  12         655  
3             package Rubric::User;
4             # ABSTRACT: a Rubric user
5             $Rubric::User::VERSION = '0.156';
6             #pod =head1 DESCRIPTION
7             #pod
8             #pod This class provides an interface to Rubric users. It inherits from
9             #pod Rubric::DBI, which is a Class::DBI class.
10             #pod
11             #pod =cut
12              
13 12     12   62 use base qw(Rubric::DBI);
  12         20  
  12         1607  
14 12     12   68 use Digest::MD5 qw(md5_hex);
  12         23  
  12         567  
15 12     12   1023 use Time::Piece;
  12         11088  
  12         116  
16              
17             __PACKAGE__->table('users');
18              
19             #pod =head1 COLUMNS
20             #pod
21             #pod username - the user's login name
22             #pod password - the hex md5sum of the user's password
23             #pod email - the user's email address
24             #pod created - the user's date of registration
25             #pod
26             #pod verification_code - the code sent to the user for verification
27             #pod NULL if verified
28             #pod
29             #pod =cut
30              
31             __PACKAGE__->columns(
32             All => qw(username password email created verification_code reset_code)
33             );
34              
35             #pod =head1 RELATIONSHIPS
36             #pod
37             #pod =head2 entries
38             #pod
39             #pod Every user has_many entries, which are Rubric::Entry objects. They can be
40             #pod retrieved with the C accessor, as usual.
41             #pod
42             #pod =cut
43              
44             __PACKAGE__->has_many(entries => 'Rubric::Entry' );
45              
46             #pod =head2 tags
47             #pod
48             #pod A user has as "his" tags all the tags that occur on his entries. There exist a
49             #pod number of accessors for his tag list.
50             #pod
51             #pod =head3 tags
52             #pod
53             #pod This returns an arrayref of all the user's (non-system) tags in their database
54             #pod colation order.
55             #pod
56             #pod =cut
57              
58             __PACKAGE__->set_sql(tags => <<'' );
59             SELECT DISTINCT tag
60             FROM entrytags
61             WHERE entry IN (SELECT id FROM entries WHERE username = ?)
62             AND tag NOT LIKE '@%%'
63             ORDER BY tag
64              
65             sub tags {
66 1     1 1 19760 my ($self) = @_;
67 1         5 my $sth = $self->sql_tags;
68 1         283 $sth->execute($self->username);
69 1         281 my $tags = $sth->fetchall_arrayref;
70 1         3 [ map { @$_ } @$tags ];
  3         13  
71             }
72              
73             #pod =head3 tags_counted
74             #pod
75             #pod This returns an arrayref of arrayrefs, each containing a tag name and the
76             #pod number of entries tagged with that tag. The pairs are sorted in colation order
77             #pod by tag name.
78             #pod
79             #pod =cut
80              
81             __PACKAGE__->set_sql(tags_counted => <<'' );
82             SELECT DISTINCT tag, COUNT(*) AS count
83             FROM entrytags
84             WHERE entry IN (SELECT id FROM entries WHERE username = ?)
85             AND tag NOT LIKE '@%%'
86             GROUP BY tag
87             ORDER BY tag
88              
89             sub tags_counted {
90 13     13 1 28 my ($self) = @_;
91 13         68 my $sth = $self->sql_tags_counted;
92 13         2633 $sth->execute($self->username);
93 13         3294 my $tags = $sth->fetchall_arrayref;
94 13         69 return $tags;
95             }
96              
97             #pod =head3 related_tags(\@tags, \%context)
98             #pod
99             #pod This method returns a reference to an array of tags related to all the given
100             #pod tags. Tags are related if they occur together on entries.
101             #pod
102             #pod =cut
103              
104             sub related_tags {
105 6     6 1 22520 my ($self, $tags, $context) = @_;
106 6 50       37 $tags = [ keys %$tags ] if ref $tags eq 'HASH';
107 6 100 100     68 return unless $tags and my @tags = @$tags;
108              
109             # or an exception?
110 4         44 return [] if (grep { $_ eq '@private' } @$tags)
111 4 100 100     10 and (($context->{user}||'') ne $self->username);
      100        
112              
113             my $query = q|
114             SELECT DISTINCT tag FROM entrytags
115             WHERE entry IN (SELECT id FROM entries WHERE username = ?)
116 2         16 AND tag NOT IN (| . join(',',map { $self->db_Main->quote($_) } @tags) . q|)
117             AND tag NOT LIKE '@%'
118             AND | .
119             join ' AND ',
120 2         131 map { "entry IN (SELECT entry FROM entrytags WHERE tag=$_)" }
121 2         60 map { $self->db_Main->quote($_) }
  2         305  
122             @tags;
123              
124 2         13 $self->db_Main->selectcol_arrayref($query, undef, $self->username);
125             }
126              
127             #pod =head3 related_tags_counted(\@tags, \%context)
128             #pod
129             #pod This is the obvious conjunction of C and C. It
130             #pod returns an arrayref of arrayrefs, each a pair of tag/occurance values.
131             #pod
132             #pod =cut
133              
134             sub related_tags_counted {
135 15     15 1 12872 my ($self, $tags, $context) = @_;
136 15 100       82 return unless $tags;
137 7 100       39 $tags = [ keys %$tags ] if ref $tags eq 'HASH';
138 7 100       38 return unless my @tags = @$tags;
139              
140             # or an exception?
141 6         52 return [] if (grep { $_ eq '@private' } @$tags)
142 6 100 100     18 and (($context->{user}||'') ne $self->username);
      100        
143              
144             my $query = q|
145             SELECT DISTINCT tag, COUNT(*) AS count
146             FROM entrytags
147             WHERE entry IN (SELECT id FROM entries WHERE username = ?)
148 4         22 AND tag NOT IN (| . join(',',map { $self->db_Main->quote($_) } @tags) . q|)
149             AND tag NOT LIKE '@%'
150             AND | .
151             join ' AND ',
152 4         254 map { "entry IN (SELECT entry FROM entrytags WHERE tag=$_)" }
153 4         65 map { $self->db_Main->quote($_) }
  4         346  
154             @tags;
155              
156 4         18 $query .= " GROUP BY tag";
157              
158 4         20 $self->db_Main->selectall_arrayref($query, undef, $self->username);
159             }
160              
161             #pod =head1 INFLATIONS
162             #pod
163             #pod =head2 created
164             #pod
165             #pod The created column is stored as seconds since epoch, but inflated to
166             #pod Time::Piece objects.
167             #pod
168             #pod =cut
169              
170             __PACKAGE__->has_a(created => 'Time::Piece', deflate => 'epoch');
171              
172             __PACKAGE__->add_trigger(before_create => \&_create_times);
173              
174             sub _create_times {
175 5     5   68058 my $self = shift;
176 5 100       61 $self->created(scalar gmtime) unless defined $self->{created};
177             }
178              
179             #pod =head1 METHODS
180             #pod
181             #pod =head2 quick_entry(\%entry)
182             #pod
183             #pod This method creates or updates an entry for the user. The passed entry should
184             #pod include the following data:
185             #pod
186             #pod uri - the URI for the entry
187             #pod tags - the tags for the entry, as a space delimited string
188             #pod title - the title for the entry
189             #pod description - the description for the entry
190             #pod body - the body for the entry
191             #pod
192             #pod If an entry for the link exists, it is updated. Existing tags are replaced
193             #pod with the new tags. If no entry exists, the Rubric::Link is created if needed,
194             #pod and a new entry is then created.
195             #pod
196             #pod The Rubric::Entry object is returned.
197             #pod
198             #pod =cut
199              
200             sub quick_entry {
201 13     13 1 171284 my ($self, $entry) = @_;
202              
203 13 100       89 return unless $entry->{title};
204 12         141 $entry->{tags} = Rubric::Entry->tags_from_string($entry->{tags});
205              
206 12         6829 my $link;
207 12 100       73 if ($entry->{uri}) {
208 8         24 $link = eval { Rubric::Link->find_or_create({ uri => $entry->{uri} }) };
  8         108  
209 8 50       94046 return unless $link;
210             }
211              
212             my $new_entry = $entry->{entryid}
213             ? Rubric::Entry->retrieve($entry->{entryid})
214 12 100       989 : $link
    100          
215             ? Rubric::Entry->find_or_create({ link => $link, username => $self })
216             : Rubric::Entry->create({ username => $self });
217              
218 12         928880 $new_entry->link($link);
219 12         4294 $new_entry->title($entry->{title});
220 12         2557 $new_entry->description($entry->{description});
221 12   100     2856 $new_entry->body($entry->{body} || undef);
222 12         2650 $new_entry->update;
223 12         442760 $new_entry->set_new_tags($entry->{tags});
224              
225 12         464602 return $new_entry;
226             }
227              
228             #pod =head2 verify($code)
229             #pod
230             #pod If the given code matches this user's C, the user will be
231             #pod verified; that is, his C will be undefined.
232             #pod
233             #pod =cut
234              
235             sub verify {
236 4     4 1 31869 my ($self, $code) = @_;
237              
238 4 100       18 return unless $self->verification_code;
239              
240 3 100 100     263 if ($code and $code eq $self->verification_code) {
241 1         80 $self->verification_code(undef);
242 1         239 $self->update;
243 1         59723 return 1;
244             }
245 2         90 return;
246             }
247              
248             #pod =head2 reset_password($code)
249             #pod
250             #pod If the given code matches this user's C, the user's password will be
251             #pod reset via C and his reset code will be undefined. If
252             #pod successful, the new password is returned. Otherwise, the routine returns
253             #pod false.
254             #pod
255             #pod =cut
256              
257             sub reset_password {
258 3     3 1 434471 my ($self, $code) = @_;
259              
260 3 50       13 return unless $self->reset_code;
261              
262 3 100 100     262 if ($code and $code eq $self->reset_code) {
263 1         79 my $password = $self->randomize_password;
264 1         11 $self->reset_code(undef);
265 1         276 $self->update;
266 1         43432 return $password;
267             }
268 2         91 return;
269             }
270              
271             #pod =head2 randomize_password
272             #pod
273             #pod This method resets the user's password to a pseudo-random string and returns
274             #pod the new password.
275             #pod
276             #pod =cut
277              
278             sub __random_string {
279 3     3   11 my $length = 15;
280 3         56 my @legal = ('a'..'z', 'A'..'Z', 0..9);
281 3         16 my $string = join '', map { @legal[rand @legal] } 1 .. $length;
  45         212  
282              
283 3 100       142 return wantarray ? (md5_hex($string), $string) : md5_hex($string);
284             }
285              
286             sub randomize_password {
287 1     1 1 3 my ($self) = @_;
288 1         6 my ($pass_md5, $password) = $self->__random_string;
289            
290 1         8 $self->password($pass_md5);
291 1         418 $self->update;
292              
293 1         80409 return $password;
294             }
295              
296             #pod =head2 randomize_reset_code
297             #pod
298             #pod This method resets the user's reset code to the md5sum of a pseudo-random
299             #pod string.
300             #pod
301             #pod =cut
302              
303             sub randomize_reset_code {
304 1     1 1 733 my ($self) = @_;
305 1         6 my $reset_code = $self->__random_string;
306 1         7 $self->reset_code($reset_code);
307 1         545 $self->update;
308             }
309              
310             #pod =head2 randomize_verification_code
311             #pod
312             #pod This method resets the user's verification code to the md5sum of a
313             #pod pseudo-random string.
314             #pod
315             #pod =cut
316              
317             sub randomize_verification_code {
318 1     1 1 70121 my ($self) = @_;
319 1         7 my $verification_code = $self->__random_string;
320 1         10 $self->verification_code($verification_code);
321 1         523 $self->update;
322             }
323              
324             1;
325              
326             __END__