fix has_tags([$names]) and add has_tag($name)/has_tag($tag_object)
[bse.git] / site / cgi-bin / modules / BSE / TB / TagOwner.pm
CommitLineData
76c6b28e
TC
1# mix-in (or close) for classes that keep tags
2# currently just articles
3# the owner class should implement a tag_owner_type method
4package BSE::TB::TagOwner;
5use strict;
6use BSE::TB::Tags;
7use BSE::TB::TagMembers;
8
da19080c 9our $VERSION = "1.005";
599fe373
TC
10
11=head1 NAME
12
13BSE::TB::TagOwner - mixin for objects with tags
14
15=head1 SYNOPSIS
16
17 my $article = ...;
18
19 $article->set_tags([ qw/tag1 tag2/ ], \$error);
20 $article->remove_tags;
21
22 my @tags = $article->tag_objects;
23 my @tag_names = $article->tags;
24 my @tag_ids = $article->tag_ids;
25
26 if ($article->has_tags([ "tag1", "tag2" ])) {
27 ...
28 }
29
30=head1 DESCRIPTION
31
32This class is a mix-in that implements tags for the mixed-into object.
33
34=head1 METHODS PROVIDED
35
36=over
37
38=item set_tags(\@tags, \$error)
39
40Set the specified tags on the object, replacing all existing tags.
41
42=cut
76c6b28e
TC
43
44sub set_tags {
45 my ($self, $rtags, $rerror) = @_;
46
47 my @current_tags = $self->tag_objects;
48 my %current = map { $_->canon_name => $_ } @current_tags;
49 my %remove = %current;
50 my %save;
51 my %add;
52 for my $name (@$rtags) {
53 my $work = BSE::TB::Tags->name($name, $rerror);
54 defined $work or return;
55
56 my $lower = lc $work;
57 if ($current{$lower}) {
58 delete $remove{$lower};
59 if (!$save{$lower} && $name ne $current{$lower}->name) {
60 $save{$lower} = $name;
61 }
62 }
63 else {
64 $add{$lower} = $name;
65 }
66 }
67
68 for my $add (values %add) {
69 # look for or make the tag
70 my $tag = BSE::TB::Tags->getByName($self->tag_owner_type, $add);
71 if ($tag) {
72 if ($tag->name ne $add && !$save{lc $add}) {
73 $current{lc $add} = $tag;
74 $save{lc $add} = $add;
75 }
76 }
77 else {
78 $tag = BSE::TB::Tags->make_with_name($self->tag_owner_type, $add);
79 }
80
81 # add the reference
82 BSE::TB::TagMembers->make
83 (
84 owner_type => $self->tag_owner_type,
85 owner_id => $self->id,
86 tag_id => $tag->id,
87 );
88 }
89
90 for my $save (keys %save) {
91 my $new_name = $save{$save};
92 my $tag = $current{$save};
93 $tag->set_name($new_name);
94 $tag->save;
95 }
96
97 # remove any leftovers
98 for my $remove (values %remove) {
99 BSE::TB::TagMembers->remove_by_tag($self, $remove);
100 }
101
102 return 1;
103}
104
599fe373
TC
105=item remove_tags
106
107Remove all tags from the object.
108
109=cut
110
76c6b28e
TC
111sub remove_tags {
112 my ($self) = @_;
113
114 BSE::TB::TagMembers->remove_owned_by($self);
115}
116
599fe373
TC
117=item tag_objects
118
119Return all existing tags on the object as tag objects.
120
121=cut
122
76c6b28e
TC
123sub tag_objects {
124 my ($self) = @_;
125
126 return BSE::TB::Tags->getSpecial(object_tags => $self->tag_owner_type, $self->id);
127}
128
599fe373
TC
129=item tags
130
131Returns all existing tags on the object as tag names.
132
133=cut
134
76c6b28e
TC
135sub tags {
136 my ($self) = @_;
137
138 return map $_->name, $self->tag_objects;
139}
140
599fe373
TC
141=item tag_ids
142
143Returns all existing tags on the object as tag ids.
144
145=cut
146
76c6b28e
TC
147sub tag_ids {
148 my ($self) = @_;
149
60a62554 150 return map $_->{id}, BSE::DB->single->query("Tag_ids.by_owner", $self->tag_owner_type, $self->id);
76c6b28e
TC
151}
152
b6a28bd1
TC
153=item tag_members
154
155Return all tag membership links for the object.
156
157=cut
158
159sub tag_members {
160 my ($self) = @_;
161
162 require BSE::TB::TagMembers;
163 return BSE::TB::TagMembers->getBy
164 (
165 owner_id => $self->id,
166 owner_type => $self->tag_owner_type,
167 );
168}
169
599fe373
TC
170=item has_tags(\@tags)
171
172Check that all of the specified tags are on the object.
173
da19080c
TC
174The array can contain either tag names or tag objects.
175
599fe373
TC
176=cut
177
76c6b28e
TC
178sub has_tags {
179 my ($self, $rtags) = @_;
180
181 my %my_tag_ids = map { $_ => 1 } $self->tag_ids;
182
183 # make sure we have objects, if there's no tag, we don't have that
184 # tage and can immediately return false
185 for my $tag (@$rtags) {
186 my $work = $tag;
187 unless (ref $work) {
188 $work = BSE::TB::Tags->getByName($self->tag_owner_type, $tag)
189 or return;
190 }
191
da19080c 192 $my_tag_ids{$work->id}
76c6b28e
TC
193 or return;
194 }
195
196 return 1;
197}
198
da19080c
TC
199=item has_tag($tag_name)
200
201=item has_tag($tag_object)
202
203Return true if the given tag can be found on the object.
204
205=cut
206
207sub has_tag {
208 my ($self, $tag) = @_;
209
210 unless (ref $tag) {
211 $tag = BSE::TB::Tags->getByName($self->tag_owner_type, $tag)
212 or return;
213 }
214
215 my @members = BSE::TB::TagMembers->getBy
216 (
217 owner_id => $self->id,
218 owner_type => $self->tag_owner_type,
219 tag_id => $tag->id,
220 );
221
222 @members
223 or return;
224
225 return 1;
226}
227
b6a28bd1
TC
228=item tag_by_name
229
230Return the tag (if any) by name for this object type.
231
232Returns an empty list if no such tag is found.
233
234=cut
235
236sub tag_by_name {
237 my ($self, $name) = @_;
238
239 my ($tag) = BSE::TB::Tags->getByName($self->tag_owner_type, $name)
240 or return;
241
242 return $tag;
243}
244
245=item collection_with_tags()
246
247This is a wrapper for L<BSE::TB::TagOwners/collection_with_tags()>
248that passes $self as the C<self> parameter in \%opts.
249
250=cut
251
252sub collection_with_tags {
253 my ($self, $name, $tags, $opts) = @_;
254
255 return $self->tableClass->collection_with_tags
256 (
257 $name,
258 $tags,
259 {
260 ($opts ? %$opts : ()),
261 self => $self,
262 },
263 );
264}
265
76c6b28e 2661;
599fe373
TC
267
268__END__
269
270=back
271
272=head1 REQUIRED METHODS
273
274These need to be implemented by the class that wants tags.
275
276=over
277
278=item tag_owner_type
279
280Return a short constant string identifying owner class of the tags.
281
282=item id
283
284The numeric id of the specific owner object of the tags.
285
b6a28bd1
TC
286=item tableClass
287
288The name of the class for collections of the tag owner.
289
599fe373
TC
290=back
291
292=head1 AUTHOR
293
294Tony Cook <tony@develop-help.com>
295
296=cut
297