]> git.decadent.org.uk Git - maypole.git/blob - doc/Flox.pod
Reorder the way we look for stringification column.
[maypole.git] / doc / Flox.pod
1 =head1 Flox: A Free Social Networking Site
2
3 Friendster, Tribe, and now Google's Orkut - it seems like in early 2004,
4 everyone wanted to be a social networking site. At the time, I was too
5 busy to be a social networking site, as I was working on my own project
6 at the time - Maypole. However, I realised that if I could implement a
7 social networking system using Maypole, then Maypole could probably do
8 anything.
9
10 I'd already decided there was room for a free, open-source networking
11 site, and then Peter Sergeant came up with the hook - localizing it to
12 universities and societies, and tying in meet-ups with restaurant
13 bookings. I called it Flox, partially because it flocks people together
14 and partially because it's localised for my home town of Oxford and its
15 university student population.
16
17 Flox is still in, uh, flux, but it does the essentials. We're going to
18 see how it was put together, and how the techniques shown in the
19 L<Request.pod> chapter can help to create a sophisticated web
20 application. Of course, I didn't have this manual available at the time,
21 so it took a bit longer than it should have done...
22
23 =head2 Mapping the concepts
24
25 Any Maypole application should start with two things: a database schema,
26 and some idea of what the pages involved are going to look like.
27 Usually, these pages will be tying to displaying or editing some element
28 of the database, so these two concepts should come hand in hand.
29
30 When I started looking at social networking sites, I began by
31 identifying the concepts which were going to make up the tables of the
32 application. At its most basic, a site like Orkut or Flox has two
33 distinct concepts: a user, and a connection between two users.
34 Additionally, there's the idea of an invitation to a new user, which can
35 be extended, accepted, declined or ignored. These three will make up the
36 key tables; there are an extra two tables in Flox, but they're
37 essentially enumerations that are a bit easier to edit: each user has an
38 affiliation to a particular college or department, and a status in the
39 university. (Undergraduate, graduate, and so on.)
40
41 For this first run-through, we're going to ignore the ideas of societies
42 and communities, and end up with a schema like so:
43
44     CREATE TABLE user (
45         id int not null auto_increment primary key,
46         first_name varchar(50),
47         last_name varchar(50),
48         email varchar(255),
49         profile text,
50         password varchar(255),
51         affiliation int,
52         unistatus int,
53         status ENUM("real", "invitee"),
54         photo blob,
55         photo_type varchar(30)
56     );
57
58     CREATE TABLE connection (
59         id int not null auto_increment primary key,
60         from_user int,
61         to_user int,
62         status ENUM("offered", "confirmed")
63     );
64
65     CREATE TABLE invitation (
66         id char(32) not null primary key,
67         issuer int,
68         recipient int,
69         expires date
70     );
71
72 Plus the definition of our two auxilliary tables:
73
74     CREATE TABLE affiliation (
75         id int not null auto_increment primary key,
76         name varchar(255)
77     );
78
79     CREATE TABLE unistatus (
80         id int not null auto_increment primary key,
81         name varchar(255)
82     );
83
84 Notice that, for simplicity, invitations and friendship connections are
85 quite similar: they are extended from one user to another. This means
86 that people who haven't accepted an invite yet still have a place in the
87 user table, with a different C<status>. Similarly, a connection between
88 users can be offered, and when it is accepted, its status is changed to
89 "confirmed" and a reciprocal relationship put in place.
90
91 We also have some idea, based on what we want to happen, of what pages
92 and actions we're going to define. Leaving the user aside for the
93 moment, we want an action which extends an invitation from the current
94 user to a new user. We want a page the new user can go to in order to
95 accept that invitation. Similarly, we want an action which offers a
96 friendship connection to an existing user, and a page the user can go to
97 to accept or reject it. This gives us five pages so far:
98
99     invitation/issue
100     invitation/accept
101
102     user/befriend
103     connection/accept
104     connection/reject
105
106 Notice that the C<befriend> action is performed on a user, not a
107 connection. This is distinct from C<invitation/issue> because when
108 befriending, we have a real user on the system that we want to do
109 something to. This makes sense if you think of it in terms of object
110 oriented programming - we could say
111
112     Flox::Connection->create(to => $user)
113
114 but it's clearer to say
115
116     $user->befriend
117
118 Similarly, we could say
119
120     Flox::User->create({ ... })->issue_invitation_to
121
122 but it's clearer to say
123
124     Flox::Invitation->issue( to => Flox::User->create({ ... }) )
125
126 because it more accurately reflects the principal subject and object of
127 these actions.
128
129 Returning to look at the user class, we want to be able to view a user's
130 profile, edit one's own profile, set up the profile for the first
131 time, upload pictures and display pictures. We also need to handle the
132 concepts of logging in and logging out.
133
134 As usual, though, we'll start with a handler class which sets up the
135 database:
136
137     package Flox;
138     use base qw(Apache::MVC);
139     Flox->setup("dbi:mysql:flox");
140     Flox->config->{display_tables} = [qw[user invitation connection]];
141     1;
142
143 Very simple, as these things are meant to be. Now let's build on it.
144
145 =head2 Authentication
146
147 The concept of a current user is absolutely critical in a site like
148 Flox; it represents "me", the viewer of the page, as the site explores
149 the connections in my world. We've described the authentication hacks
150 briefly in the L<Request.pod> chapter, but now it's time to go into a
151 little more detail about how user handling is done.
152
153 XXX
154
155 =head2 Viewing a user
156
157 The first page that a user will see after logging in will be their own
158 profile, so in order to speed development, we'll start by getting a
159 C<user/view> page up.
160
161 The only difference from a programming point of view between this action
162 and the default C<view> action is that, if no user ID is given, then we
163 want to view "me", the current user. Remembering that the default view
164 action does nothing, our C<Flox::User::view> action only needs to do
165 nothing plus ensure it has a user in the C<objects> slot, putting
166 C<$r-E<gt>{user}> in there if not:
167
168     sub view :Exported {
169         my ($class, $r) = @_;
170         $r->{objects} = [ $r->{user} ] unless @{$r->{objects}||[]};
171     }
172
173 Maypole, unfortunately, is very good at making programming boring. The
174 downside of having to write very little code at all is that we now have
175 to spend most of our time writing nice HTML for the templates.
176
177 XXX
178
179 The next stage is viewing the user's photo. Assuming we've got the photo
180 stored in the database already (which is a reasonable assumption for the
181 moment since we don't have a way to upload a photo quite yet) then we
182 can use the a variation of the "Displaying pictures" hack from the 
183 Requests chapter:
184
185     sub view_picture :Exported {
186         my ($self, $r) = @_;
187         my $user = $r->{objects}->[0] || $r->{user};
188         if ($r->{content_type} = $user->photo_type) {
189            $r->{output} = $user->photo;
190         } else {
191            # Read no-photo photo
192            $r->{content_type} = "image/png";
193            $r->{output} = slurp_file("images/no-photo.png");
194         }
195     }
196
197 We begin by getting a user object, just like in the C<view> action: either
198 the user whose ID was passed in on the URL, or the current user. Then
199 we check if a C<photo_type> has been set in this user's record. If so,
200 then we'll use that as the content type for this request, and the data
201 in the C<photo> attribute as the data to send out. The trick here is
202 that setting C<$r-E<gt>{output}> overrides the whole view class processing
203 and allows us to write the content out directly.
204
205 In our template, we can now say
206
207     <IMG SRC="/user/view_picture/[% user.id %]">
208
209 and the appropriate user's mugshot will appear.
210
211 However, if we're throwing big chunks of data around like C<photo>, it's
212 now worth optimizing the C<User> class to ensure that only pertitent
213 data is fetched by default, and C<photo> and friends are only fetched on
214 demand. The "lazy population" section of C<Class::DBI>'s man page
215 explains how to group the columns by usage so that we can optimize
216 fetches:
217
218     Flox::User->columns(Primary   => qw/id/);
219     Flox::User->columns(Essential => qw/status/);
220     Flox::User->columns(Helpful   => qw/ first_name last_name email password/)
221     Flox::User->columns(Display   => qw/ profile affiliation unistatus /);
222     Flox::User->columns(Photo     => qw/ photo photo_type /);
223
224 This means that the status and ID columns will always be retrieved when
225 we deal with a user; next, any one of the name, email or password
226 columns will cause that group of data to be retrieved; if we go on to
227 display more information about a user, we also load up the profile,
228 affiliation and university status; finally, if we're throwing around
229 photos, then we load in the photo type and photo data.
230
231 These groupings are somewhat arbitrary, and there needs to be a lot of
232 profiling to determine the most efficient groupings of columns to load,
233 but they demonstrate one principle about working in Maypole: this is the
234 first time in dealing with Maypole that we've had to explicitly list the
235 columns of a table, but Maypole has so far Just Worked. There's a
236 difference, though, between Maypole just working and Maypole working
237 well, and if you want to optimize your application, then you need to
238 start putting in the code to do that. The beauty of Maypole is that you
239 can do as much or as little of such optimization as you want or need.
240
241 So now we can view users and their photos. It's time to allow the users
242 to edit their profiles and upload a new photo.
243
244 =head2 Editing users
245
246 XXX Editing a profile
247
248 I introduced Flox to a bunch of friends and told them to be as ruthless
249 as possible in finding bugs and trying to break it. And break it they
250 did; within an hour the screens were thoroughly messed up as users had
251 nasty HTML tags in their profiles, names, email addresses and so on. 
252 This spawned another hack in the request cookbook: "Limiting data for
253 display". I changed the untaint columns to use C<html> untainting, and
254 all was better:
255
256     Flox::User->untaint_columns(
257         html      => [qw/first_name last_name profile/],
258         printable => [qw/password/],
259         integer   => [qw/affiliation unistatus /],
260         email     => [qw/email/]
261     );
262
263 The next stage was the ability to upload a photo. We unleash the "Uploading
264 files" recipe, with an additional check to make sure the photo is of a
265 sensible size:
266
267     use constant MAX_IMAGE_SIZE => 512 * 1024;
268     sub do_upload :Exported {
269         my ($class, $r) = @_;
270         my $user = $r->{user};
271         my $upload = $r->{ar}->upload("picture");
272         if ($upload) {
273             my $ct = $upload->info("Content-type");
274             return $r->error("Unknown image file type $ct")
275                 if $ct !~ m{image/(jpeg|gif|png)};
276             return $r->error("File too big! Maximum size is ".MAX_IMAGE_SIZE)
277                 if $upload->size > MAX_IMAGE_SIZE;
278
279             my $fh = $upload->fh;
280             my $image = do { local $/; <$fh> };
281
282             use Image::Size;
283             my ($x, $y) = imgsize(\$image);
284             return $r->error("Image too big! ($x, $y) Maximum size is 350x350")
285                 if $y > 350 or $x > 350;
286             $r->{user}->photo_type($ct);
287             $r->{user}->photo($image);
288         }
289
290         $r->objects([ $user ]);
291         $r->{template} = "view";
292     }
293
294 Now we've gone as far as we want to go about user editing at the moment.
295 Let's have a look at the real meat of a social networking site: getting
296 other people involved, and registering connections between users. 
297
298 =head2 Invitations
299
300 =head2 Friendship Connections
301
302 =head2