source: bookmarks/trunk/lib/Bookmarks/Controller.pm @ 128

Last change on this file since 128 was 112, checked in by peter, 9 years ago
  • Added the sidebar resource.
  • Moved the list resources to list.
  • Respond with a 406 if the required modules are not available for XBEL, Atom, or CSV.
File size: 9.1 KB
Line 
1package Bookmarks::Controller;
2
3use Moose;
4
5use Encode;
6use HTTP::Date qw{time2str str2time};
7use JSON;
8use Bookmarks;
9use Bookmarks::List;
10use URI;
11use Template;
12
13has dbname => (
14    is => 'ro',
15    required => 1,
16);
17has bookmarks => (
18    is => 'ro',
19    handles => [qw{get_bookmark}],
20    builder => '_build_bookmarks',
21    lazy => 1,
22);
23has base_uri => (
24    is => 'ro',
25    builder => '_build_base_uri',
26    lazy => 1,
27);
28has request => (
29    is => 'rw',
30);
31
32sub _build_bookmarks {
33    my $self = shift;
34    return Bookmarks->new({
35        dbname   => $self->dbname,
36        base_uri => $self->base_uri,
37    });
38}
39
40sub _build_base_uri {
41    my $self = shift;
42    my $url = $self->request->base;
43
44    $url .= '/' unless $url =~ m{/$};
45    return URI->new($url);
46}
47
48sub find_or_new {
49    my $self = shift;
50
51    my $bookmark = $self->bookmarks->get_bookmark({ uri => $self->request->param('uri') });
52    if ($bookmark) {
53        # redirect to the view of the existing bookmark
54        return [301, [Location => $bookmark->bookmark_uri], []];
55    } else {
56        # bookmark was not found; show the form to create a new bookmark
57        require File::Basename;
58        my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) });
59        $template->process(
60            'bookmark.tt',
61            { 
62                bookmark => {
63                    uri   => $self->request->param('uri'),
64                    title => $self->request->param('title') || '',
65                },
66            },
67            \my $output,
68        );
69        return [404, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
70    }
71}
72
73sub _get_search_from_request {
74    my $self = shift;
75
76    my @tags   = grep { $_ ne '' } $self->request->param('tag');
77    my $query  = $self->request->param('q');
78    my $limit  = $self->request->param('limit');
79    my $offset = $self->request->param('offset');
80    my $page   = $self->request->param('page');
81
82    return $self->bookmarks->search({
83        query  => $query,
84        tags   => \@tags,
85        limit  => $limit,
86        offset => $offset,
87        page   => $page,
88    });
89}
90
91sub list {
92    my $self = shift;
93
94    # list all the bookmarks
95    my $mtime = $self->bookmarks->get_last_modified_time;
96
97    my $format = $self->request->param('format') || 'html';
98
99    my $list = Bookmarks::List->new({
100        bookmarks => $self->bookmarks,
101        search    => $self->_get_search_from_request,
102    });
103
104    my $as_format = "as_$format";
105    if (!$list->meta->has_method($as_format)) {
106        return [406, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$format" is not a supported format}]];
107    }
108    return $list->$as_format;
109}
110
111sub sidebar {
112    my $self = shift;
113
114    my $list = Bookmarks::List->new({
115        bookmarks => $self->bookmarks,
116        search    => $self->_get_search_from_request,
117    });
118
119    require Template;
120    require File::Basename;
121    my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/List.pm'}) });
122
123    my @all_tags = $list->bookmarks->get_tags({ selected => @{ $list->tags }[0] });
124    my @cotags = $list->bookmarks->get_cotags({ search => $list->search });
125
126    $template->process(
127        'list.tt',
128        {
129            base_url     => $list->bookmarks->base_uri,
130            title        => $list->title,
131            query        => $list->query,
132            selected_tag => @{ $list->tags }[0],
133            search_tags  => $list->tags,
134            links        => [ $list->_get_list_links('text/html', { q => $list->query, tag => $list->tags }) ],
135            all_tags     => \@all_tags,
136            cotags       => \@cotags,
137            resources    => $list->results,
138            pages        => $list->search->pages,
139        },
140        \my $output,
141    );
142    return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
143}
144
145sub feed {
146    my $self = shift;
147
148    my $query = $self->request->param('q');
149    my @tags = grep { $_ ne '' } $self->request->param('tag');
150
151    # construct a feed from the most recent 12 bookmarks
152    my $list = Bookmarks::List->new({
153        bookmarks => $self->bookmarks,
154        search    => $self->bookmarks->search({ query => $query, tags => \@tags, limit => 12 }),
155    });
156    return $list->as_atom;
157}
158
159# returns 1 if there is an If-Modified-Since header and it is newer than the given $mtime
160# returns 0 if there is an If-Modified-Since header but the $mtime is newer
161# returns undef if there is no If-Modified-Since header
162sub _request_is_newer_than {
163    my $self = shift;
164    my $mtime = shift;
165
166    # check If-Modified-Since header to return cache response
167    if ($self->request->env->{HTTP_IF_MODIFIED_SINCE}) {
168        my $cache_time = str2time($self->request->env->{HTTP_IF_MODIFIED_SINCE});
169        return $mtime <= $cache_time ? 1 : 0;
170    } else {
171        return;
172    }
173}
174
175sub view {
176    my ($self, $id) = @_;
177
178    my $format = $self->request->param('format') || 'html';
179
180    my $bookmark = $self->get_bookmark({ id => $id });
181    if ($bookmark) {
182        return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime);
183
184        my $last_modified = time2str($bookmark->mtime);
185       
186        if ($format eq 'json') {
187            my $json = decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark->to_hashref));
188            return [200, ['Content-Type' => 'application/json; charset=UTF-8', 'Last-Modified' => $last_modified], [$json]];
189        } else {
190            # display the bookmark form for this bookmark
191            require File::Basename;
192            my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) });
193            $template->process(
194                'bookmark.tt',
195                { bookmark => $bookmark },
196                \my $output,
197            );
198            return [200, ['Content-Type' => 'text/html; charset=UTF-8', 'Last-Modified' => $last_modified], [$output]];
199        }
200    } else {
201        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]];
202    }
203}
204
205sub view_field {
206    my ($self, $id, $field) = @_;
207
208    my $bookmark = $self->get_bookmark({ id => $id });
209    if ($bookmark) {
210        return [304, [], []] if $self->_request_is_newer_than($bookmark->mtime);
211
212        # check whether the requested field is part of the bookmark
213        if (!$bookmark->meta->has_attribute($field)) {
214            return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$field" is not a valid bookmark data field}]];
215        }
216
217        # respond with just the requested field as plain text
218        my $value = $bookmark->$field;
219        my $last_modified = time2str($bookmark->mtime);
220        return [200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Last-Modified' => $last_modified], [ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value]];
221    } else {
222        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]];
223    }
224}
225
226sub create_and_redirect {
227    my $self = shift;
228
229    my $uri   = $self->request->param('uri');
230    my $title = $self->request->param('title');
231    my @tags  = split ' ', $self->request->param('tags');
232
233    my $bookmark = $self->bookmarks->add({
234        uri   => $uri,
235        title => $title,
236        tags  => \@tags,
237    });
238
239    #TODO: not RESTful; the proper RESTful response would be a 201
240    return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
241}
242
243sub update_and_redirect {
244    my $self = shift;
245    my $id = shift;
246
247    my $bookmark = $self->bookmarks->get_bookmark({ id => $id });
248    if ($bookmark) {
249        # update the URI, title, and tags
250        $bookmark->uri($self->request->param('uri'));
251        $bookmark->title($self->request->param('title'));
252        $bookmark->tags([ split ' ', $self->request->param('tags') || '' ]);
253
254        # write to the database
255        $bookmark->update;
256
257        #TODO: not RESTful; proper response would be a 200
258        return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
259    } else {
260        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Bookmark $id not found"]];
261    }
262}
263
264sub tag_tree {
265    my $self = shift;
266    my $tags = shift || [];
267
268    my $list = Bookmarks::List->new({
269        bookmarks => $self->bookmarks,
270        search    => $self->bookmarks->search({
271            tags   => $tags,
272        }),
273    });
274
275    my @tags = map {
276        {
277            path => join('/', @$tags[0 .. $_ - 1]),
278            tag  => $tags->[$_ - 1],
279        }
280    } 1 .. scalar(@{ $tags });
281
282    require Template;
283    require File::Basename;
284    my @cotags = $self->bookmarks->get_cotags({ search => $list->search });
285
286    #TODO: get the actual request URI and makre sure it ends with a /
287    my $base_url = join '/', ($self->base_uri . 'tags'), @$tags;
288    $base_url .= '/';
289    my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/Controller.pm'}) });
290
291    $template->process(
292        'tagtree.tt',
293        {
294            base_url => $self->base_uri,
295            request_url => $base_url,
296            tags      => \@tags,
297            cotags    => \@cotags,
298            bookmarks => $list->results,
299        },
300        \my $output,
301    );
302
303
304    return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
305}
306
3071;
Note: See TracBrowser for help on using the repository browser.