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