Assignment 4: Models, Controllers, and Views

November 14th, 2009 Leave a comment Go to comments

Errata

  1. (14-Nov-2009) The AddTags migration is missing one tag and had the wrong name for another. Without this tweak, clicking on a couple of publications will raise an exception. So, in AddTags, change the lines that add the tags to this:
    %W[ java ruby ruby1.9 rails projects crud models rest activerecord ].each do |tag_name|
    Tag.create!(:name => tag_name, :user => john)
     end
    
  2. (9-Nov-2009) At the request of Ron, the download version 1.0.004 wraps up a number of small changes from these errata, namely: (a) the use of :uniq on Tag.index_publications; (b) a similar addition on Publication.index_tags; (c) removal of .uniq in views/publications/_publication.html.erb; (d) fix for doubled occurrence of the word “for” in views/index_entries/new.html.erb; (e) a check that there’s a current_user before displaying the link to add an index entry in views/publications/show.html.erb, and, in this same template, another check for a current_user before displaying annotations, as well as the use of the “for_user” association extension on Publication.annotations; (f) from Keith, functional tests of the PublicationsController and SessionsController. That should be it.
  3. (8-Nov-2009) Thanks to a mistake on my part, I forgot to constrain the indexed publications for a tag so that the results are unique. Ron found the bug and suggested the fix, which works. In app/models/tag.rb, add “:uniq => true” to the indexed_publications association:
    has_many :indexed_publications,
      :source => :publication,
      :through => :index_entries,
      :uniq => true
    

    “uniq” is described on p. 371 (print), p. 375 (PDF).

  4. (5-Nov-2009) For item 1, the CreateIndexEntries migration: The self.down method is already done for you — disregard the comment in the code that says you need to do something. I accidentally left in the drop_table. :-)
  5. (4-Nov-2009) As I was revising some slides on Rails filtering, a stream of invective emerged from my mouth as I discovered that I had not “turned on” the filtering completely in the code download for assignment 4. This tweak does not affect your ability to get the assignment done, but I recommend it nonetheless. For all controllers except ApplicationController, RequiresAuthenticationController, and SessionController, make sure that the controllers subclass RequiresAuthenticationController. Example:
    class TagsController < RequiresAuthenticationController
    

    This tweak is reflected in e168-assignments4and5-jgn-1.0.003.zip on the downloads page. Again, this goof doesn’t really affect your ability to complete the assignment. The sympton is that if you log out and then go to a page such as /tags/new, you won’t properly be kicked over to authentication.

  6. In app/models/publication.rb, there may be a “for_tag” association extension on on the wrong association. The has_many associations are as follows. This is the only difference between e168-assignments4and5-jgn-1.0.000.zip, and e168-assignments4and5-jgn-1.0.002.zip (001 was missing the update). You can either download e168-assignments4and5-jgn-1.0.002.zip from the downloads page, or put this code below in place of the associations in the Publication model. The reason I wanted you to have this is because I’ll be discussion association extensions Thursday (at Keith’s request) and these additional methods should make it easier to avoid putting “find” code into your views.
      belongs_to :user
      has_many :annotations, :dependent => :destroy do
        # Students: This is an "association extension":
        # See AWDR, p. 372 (print), p. 376 (PDF)
        def for_user(user)
          find(:first, :conditions => ['annotations.user_id = ?', user.id])
        end
      end
      has_many :publication_tags, :dependent => :destroy
      has_many :tags, :through => :publication_tags
      has_many :index_entries, :dependent => :destroy do
        # Another "association extension"; see references above.
        def for_tag(tag)
          find(:all, :conditions => { :tag_id => tag.id })
        end
      end
      has_many :index_tags,
        :source => :tag,
        :through => :index_entries
    

    This is fixed in the freshest download: e168-assignments4and5-jgn-1.0.002.zip on the downloads page.


Both assignments 4 and 5 are due 23-Nov.; automatic extension to 25-Nov. If you complete assignment 4 early, submit it, and we’ll see if we can get comments back to you early.

In this assignment you will implement parts of a full-blown web application: migrations, models, views, and controllers. These components are the foundation of any Rails application. If you can do this well, it should be clear sailing to the end of the course, because most of the other aspects of Rails go deeper or decorate the application with plugin or gem functionality.

You can find a package for the assignment on the downloads page.

The application is essentially an online bibliography/index of publications. (We actually wanted to use the word “resource,” but that has a technical meaning in Rails, so we’re going with something a bit more ordinary.)  Here’s the home page of the reference implementation (which you can try out at http://publications.plugh.org):

assn4-home-small

For the most part, the application just gives a list of books and links to web sites. Each publication may have tags and annotations. There are also links to Amazon and Safari when appropriate.

[NOTE regard "Safari" links: For the "reference implementation" and your own app, you can log in as john@7fff.com to see the Safari links; or, in your own implementation, you can set user.safari = true [and save that user] to turn on the Safari links — I decided to leave user management out of this assignment, because it gets complicated fast.]

If a user logs in, there are buttons on the page that allow the user to see the detail for a publication, edit a publication (if the user added it in the first place), and add an annotation (if the user has never added an annotation for the publication). Additionally, in the “detail” view, there are some additional buttons for adding index entries. You will need to play around with the application to get a feel for what’s there. Here’s what the home page looks after a user logs in:

assn4-home-logged-in-small

Here’s the schema (click to enlarge):

assn4-schema-small

The central table is publications. As you can see, a Publication has many annotations, publication tags, and index entries. A publication has tags through both index entries and through publication tags. For example, the book Agile Web Development with Rails might be tagged as “rails.” That means there would be a row in the publication_tags table with publication_id set to the id of AWDR in the publications table, and tag_id set to the id for the “rails” tag. There might also be an index entry for the tag “models.” This would be represented by a row in index_entries. The publication_id would be set to the id for AWDR, and the tag_id would be set to the id for the “models” tag.

The annotations table has brief comments on publications. A publication has many of these. Note that a user is only allowed one annotation per publication.

Finally, notice that all of the tables except users have a foreign key user_id pointing back to the users table. By this means, we know who created every row in the application. In general, we only want the user who created a row to be able to edit or delete it. I believe this is implemented completely in the reference implementation, but I might have forgotten a case!

Here’s what you need to do. We have left a number of parts of the application unimplemented. You need to implement them so that they behave in the same manner as the reference implemention (link above). Here’s the list:

  1. You must implement the CreateIndexEntries migration. The full set of migrations won’t complete without it. The down method is already done (disregard the code comment that says there’s something for you to do — there isn’t).
  2. You must complete the Annotation model.
    • You must validate the uniqueness of the publication_id for a particular value of the user_id. In other words, the combination of publication_id and user_id must be unique. Hint: Study the validations for PublicationTag.
    • The presence of the ‘brief’ attribute is required.
    • Add a before validation callback that converts all whitespace in the ‘brief’ and ‘full’ attributes to one space.
  3. On the Publication model, add a custom validation that ensures that there is a value for either the ‘title’ or ‘url’ attributes, or both.
  4. Add helpers link_to_amazon and link_to_safari in ApplicationHelper that format links to Amazon and Safari. For the formats, hover over the links in the reference implementation. Hint: Implement these by calling the link_to helper with the appropriate values. See the “Note regarding Safari links” above, regarding how to see them in the application.
  5. In AnnotationController, implement the new, edit, create, update, and destroy actions. It is critical that it be impossible for a user to edit or delete another user’s annotation. For example, the edit URL (in the reference implemenation) is http://localhost:3000/annotations/5/edit. If the current user didn’t create annotation 5, the action should fail (it doesn’t have to fail gracefully; it just has to not work). Hint: See the other controllers. It would also help to compare how the other controllers are implemented, vs. what script/generate scaffold gives you.
  6. In PublicationController, change create and update so that they can handle the keywords. This is by far the hardest part of the assignment because of the management of the checkboxes. To get some ideas for handling the free-form tag box, take a look at IndexEntriesController. The “Child-Care Coop” application also provides some guidance for checkbox handling.
  7. Implement views/annotations/_annotation.html.erb
  8. Implement views/shared/_indexed_publication.html.erb
  9. Implement the missing parts of views/publications/show.html.erb

That’s it!

  1. Ron Newman
    October 29th, 2009 at 10:53 | #1

    Is there a download we should start with, or should we be implementing this from scratch?

  2. October 29th, 2009 at 10:57 | #2

    @Ron Newman

    My last step is the Rakefile. Should be ready this evening.

  3. Ron Newman
    November 1st, 2009 at 01:06 | #3

    Does the reference implementation have 'Safari links' implemented? When I added a book to it just now, I didn't see any place to enter such a link. Also, is an ASIN the same thing as an ISBN (International Standard Book Number)?

  4. Keith
    November 1st, 2009 at 01:58 | #4

    @Ron Newman
    ASIN is an Amazon Standard Identification Number (http://en.wikipedia.org/wiki/Amazon_Standard_Identification_Number).

    Take a look at app/helpers/application_helper.rb for methods that will generate the links you need (link_to_amazon, link_to_safari).

  5. November 1st, 2009 at 07:55 | #5

    @Ron Newman

    For the display of the Safari links: Yes, the reference implementation DOES have it implemented: Just log in as john@7fff.com to see. I'll add a note to the assignment regarding that.

    I wanted to keep the user management as simple as possible (so there is no user management) and no way to change that flag except via the console or database.

  6. Ron Newman
    November 1st, 2009 at 08:33 | #6

    OK, so in the reference implementation, if you are not logged in as a user with 'safari'=true, you can't even see Safari links. I wasn't expecting that (and probably won't do that in my implementation)

  7. November 1st, 2009 at 08:51 | #7

    @Ron Newman

    Your submitted implementation must work exactly like the RI, or points will be taken off. If you want to maintain a private version, that's fine. Besides, pedagogically, it's more interesting to have link display be conditional on state.

    The Safari links go to the Harvard subscription, so the Safari links are really irrelevant to anyone who isn't associated with Harvard. Were we showing the links to non-Harvard people, they'd just be disappointed because they'd get a weird login screen they've never seen.

    When I put this out for future versions of the course, the users table will be initialized with login information for registered students -- and they will all have their safari boolean's set. Forcing such users to log in is an incentive to add annotations, index entries, etc.

  8. Ron Newman
    November 1st, 2009 at 08:55 | #8

    Ahh, OK. I thought maybe that User.safari should control only the ability to *add* or *edit* the Safari link, but I'll do it your way.

  9. Ron Newman
    November 1st, 2009 at 09:07 | #9

    One bug I see in the reference implementation: if a book doesn't have an ASIN specified, and you click on the 'details' button, it displays a bogus 'Asin' link. rather than omitting the display of 'Asin'.

  10. November 1st, 2009 at 09:22 | #10

    @Ron Newman

    That's funny -- I thought I fixed that. Let me check that out.

  11. Ron Newman
    November 1st, 2009 at 09:24 | #11

    Also I was wondering if it might be better to display the 'Url' as a live link if it is present.

  12. Gabriel
    November 1st, 2009 at 09:45 | #12

    The add_publications.rb data migration has a little peculiarity:

    It uses build to create the annotation of a user. If I validate_presence_of :user_id, :publication_id in the Annotation.rb the migration will fail.
    I had the same problem in assignment 3. Since I think that the validate_presence_of :user_id, :publication_id is actually worth it, I decided to save before using build there.
    For now I disabled my validate_presence_of :user_id, :publication_id in the Annotation.rb but you might also want to change the data migration.

  13. November 1st, 2009 at 09:45 | #13

    @Ron Newman

    Like this? http://publications.plugh.org/publications/14

    Incidentally, the operation of edit buttons on interior pages is not well-defined if the user is not logged in. The user pattern is that we expect people to come in through home, so if someone is looking at a "detail" page, logs out, and goes back, code that depends on a non-null current_user may blow up.

  14. Ron Newman
    November 1st, 2009 at 10:08 | #14

    Yep, that looks better.

    The reference implementation (now) appears to always display, on the "details" page, the 'Asin:' and 'Url:' and 'Asin:" line-items, even if blank, but displays the 'Safari code:" line-item only if there is a real Safari link. I assume you'll want our code to work the same way now.

  15. November 1st, 2009 at 10:15 | #15

    @Gabriel

    That's a decent point. For what it's worth, the use of .build is so common, even when the parent record is unsaved, that you see it a lot, and thus requiring the presence of the foreign keys is left out. Indeed, I think the assumption is that if you're creating "child" objects, you would ALWAYS be using .build, and then you will be sure that at least the foreign key to the parent is set automatically. The user id is a different matter: I think it will work with validates_presence_of left in for :user_id (but not :publication_id).

    Another thing to think about is this:

    -- Once you add a validation, it is quite likely you will break your migrations. Since your app is changing, validations in the present are hardly likely to apply to data created "in the past," which is really what a migration is.

    For instance, suppose you added an attribute to an annotation such as "importance" and validated that it's non-nil. But now any migration that adds an annotation will fail.

    The new "seed" feature of Rails 2.3.4 is an attempt to split off the data generation from schema evolution.

  16. November 1st, 2009 at 10:25 | #16
  17. Gabriel
    November 1st, 2009 at 15:56 | #17

    In the writeup you mention a change in app/models/publication.rb from version 00 to version 01, but I couldn't see any. Am I looking at the wrong file? Is it possible to provide .patch files for changes, so I could stick with the version I am implementing my solutions in?

  18. November 1st, 2009 at 16:12 | #18

    @Gabriel

    It is the change given at the top; in fact, I see that I had added an association extension on has_many :annotations as well.

    Version 1.0.000, publication.rb:

      belongs_to :user
      has_many :annotations, :dependent => :destroy
      has_many :publication_tags, :dependent => :destroy
      has_many :tags, :through => :publication_tags
      has_many :index_entries, :dependent => :destroy
      has_many :index_tags,
        :source => :tag,
        :through => :index_entries
    

    Version 1.0.001:

      belongs_to :user
      has_many :annotations, :dependent => :destroy do
        # Students: This is an "association extension":
        # See AWDR, p. 372 (print), p. 376 (PDF)
        def for_user(user)
          find(:first, :conditions => ['annotations.user_id = ?', user.id])
        end
      end
      has_many :publication_tags, :dependent => :destroy
      has_many :tags, :through => :publication_tags
      has_many :index_entries, :dependent => :destroy do
        # Another "association extension"; see references above.
        def for_tag(tag)
          find(:all, :conditions => { :tag_id => tag.id })
        end
      end
      has_many :index_tags,
        :source => :tag,
        :through => :index_entries
    

    The purpose of this change is to give you an easy opportunity to avoid having to use the find method in your views. If you did want to provide a special "find," you could simple add it as a class method on your model, but I am giving it to you.

    I'm not going to be able to provide patch files for three reasons: (1) In some cases the "stubs" that are used to provide code with left-out sections (to be implemented) would force me to make error-prone hand-edits to the patches; (2) it would make it just that more complicated for me to get an update out. It is already pretty complicated; (3) I would have to teach students how to apply the patches.

  19. Ron Newman
    November 1st, 2009 at 19:22 | #19

    What order are the publications supposed to display in? It does not look to me like http://publications.plugh.org/ is sorted by title or author (neither "Firstname Lastname" nor "Lastname, Firstname")

  20. Ron Newman
    November 1st, 2009 at 19:25 | #20

    Also, regarding this:

    [quote]On the Publication model, add a custom validation that ensures that there is a value for either the ‘title’ or ‘url’ attributes, or both.[/quote]

    Is it ever useful to have a publication without a title? (I don't really want to pollute your reference database with a bogus title-less publication just to see how it displays.)

  21. November 1st, 2009 at 19:28 | #21

    @Ron Newman

    No order is defined! That's for Assignment 5!! (After you implement some way to assess the value/popularity of a publication, order by that.)

    http://e168f09.plugh.org/assignments/assignment-5-new-feature/

    OK?

  22. November 1st, 2009 at 19:30 | #22

    @Ron Newman

    Yes. Someone might enter a URL where there just isn't a reasonable title. That was my thought, at least.

    My concern was this: Suppose we required a title. . . .

    A user enters a publication that is a URL, but the title is gross, hard to type, who knows. Then they enter something like "sdhfjsdhfsd." That seems so unsightly that if we can get the display to be "reasonable" with no title -- for instance, just showing the link -- then that's pretty good.

    (Besides, it introduces the need for a custom validation.)

  23. Gabriel Hase
    November 2nd, 2009 at 05:04 | #23

    Sorry, I think I just messed up your reference implementation...
    I added a test annotation while thinking I could delete it afterwards. But there seems to be no option to destroy an annotation. (or did I just not see it?)
    Also the task 5 seems to require us only to delete an annotation if the format is xml. is this correct?

  24. Gabriel Hase
    November 2nd, 2009 at 05:31 | #24

    One other thing. As mentioned above I added an annotation with your email for publication 1. From the requirements I see that it shouldn't be allowed to add more than one annotation (as the same user) for the same publication. Now if I go in and hack the url like this (maybe by pressing the back button...):
    http://publications.plugh.org/publications/1/annotations/new

    I am presented a form to add a new annotation. When trying to save it though I get the error that I am allowed to only one annotation per publication. Well, if I were a real user and provided a huge text for "full" I'd be quite disappointed.
    So my idea would be to not present the user a new form if he already has an annotation for this publication at all (just do the constraint already in Publication#new).
    What do you think?

  25. Gabriel Hase
    November 2nd, 2009 at 05:32 | #25

    short errata for above: I mean PublicationsController#new

  26. Gabriel Hase
    November 2nd, 2009 at 07:06 | #26

    just curious:
    what's the magic behind redirect_to(@index_entry.publication)

    I see that it works, but I am just not sure how... how does this build up the form publication/id ?

  27. November 2nd, 2009 at 07:29 | #27

    @Gabriel Hase

    Gabriel,

    You can't delete annotations.

    The intended behavior is for URL hacking via the browser to fail -- but we don't try to keep people from doing it. Users of the API, though, can do everything. The main reason it's implemented this way is so that there doesn't have to be separate behavior inside of the respond_to . . . do sections. It would make the code diverge too much from what is described in the book, etc. So there is a pedagogical limitation for how fancy this can be, right now. For your final project, you can try to prevent URL hacking to your heart's content.

    I wouldn't diverge from the RI's behavior for your submission: You will make it harder to get graded.

    I'll be talking about redirect_to(@index_entry.publication) when we talk about RESTful routing.

  28. Gabriel Hase
    November 2nd, 2009 at 08:40 | #28

    how can we compare the AnnotationController#destroy methods behavior if it is not callable in the RI implementation? Same behavior as index_entry?

  29. November 2nd, 2009 at 09:15 | #29

    @Gabriel Hase

    It is the same behavior; just make sure that it's not possible to delete another user's annotation by any means (even through the API).

  30. Ron Newman
    November 2nd, 2009 at 20:58 | #30

    I'm confused by the display of annotations in the reference implementation.

    http://publications.plugh.org/ shows only your annotation for Programming Ruby 1.9, even though there are two others as well (one of which is mine). It doesn't seem to matter whether I'm logged in as myself or logged out.

    My annotation has both a Brief and a Full version, but I can't figure out what would cause the Full text to be displayed.

  31. November 2nd, 2009 at 21:23 | #31

    @Ron Newman

    That's right -- it only shows the first one, on the assumption that some may choose to rank the annotations, and provide a better means to select which one should be shown. (To be sure, one could show more, or none, but either option would probably make the home page even more busy than it is.) At present, you can't show the full version on the home page.

    If there is a full annotation, just show the additional comment "(NOTE: There is also a full annotation for this publication.)".

    I debated even having a "full" annotation, and I probably should have just left it out.

  32. Ron Newman
    November 2nd, 2009 at 21:31 | #32

    "At present, you can't show the full version on the home page." -- but is there some other page that should show it? I don't see it on the Details page either.

  33. November 2nd, 2009 at 21:42 | #33

    @Ron Newman

    Nope -- the display of the publication is shared.

    If you can come up with a way to tackle this so that it isn't confusing for user, you can try something out in Assignment 5. The hard part as an app designer will be deciding if votes should be on publications or annotations, and how much to show for an annotation. Both the brief and full annotations are probably too much; voting at two levels is probably too much . . .

  34. Ron Newman
    November 2nd, 2009 at 22:32 | #34

    I was thinking that "(NOTE: There is also a full annotation)" should either link to the full annotation or Ajax-ly expand to the full annotation .... but I haven't thought at all yet about how that would be implemented.

  35. Ron Newman
    November 2nd, 2009 at 23:02 | #35

    @john : I downloaded the 1.001 version , and I don't see your edits to app/models/publication.rb there at all. I also don't understand what you mean by this:

    there may be a “for_tag” association extension on on the wrong association.

    since I don't see the string "for_tag" anywhere in either the 1.000 or 1.001 version of the download.

  36. Ron Newman
    November 2nd, 2009 at 23:16 | #36

    I just did a recursive diff of e168-assignments4and5-jgn-1.0.000 and e168-assignments4and5-jgn-1.0.001 , and the only difference I found was the PROJECT_VERSION string in the Rakefile.

  37. November 3rd, 2009 at 00:09 | #37

    @Ron Newman

    Argh! As I've said, our build process for the assignments is non-trivial -- I didn't copy that change over to the "stubs" that create the student assignment, from the code that represents the solution. Let me fix that up and put it up.

  38. November 3rd, 2009 at 00:23 | #38

    @Ron Newman

    I haven't diff'd this myself, but it should have those extra association extensions.

    I have always wanted to provide code via subversion or git, but the problem is that it is just hard enough to set up on Windows that many students have big problems; and it would introduce yet one more thing to teach, since very many students have no experience with source control.

  39. Ron Newman
    November 3rd, 2009 at 00:33 | #39

    TextWrangler's "twdiff" command on the Mac is quite cool. I assume TextMate has something similar but I don't have that program so I don't know for sure. There's also good old "diff -r" which should work on any computer.

    Still not sure what you mean by for_tag being on the wrong association ....

  40. November 3rd, 2009 at 00:41 | #40

    @Ron Newman

    I thought that for about 15 minutes there was a download that had the for_tag method on index_tags, not index_entries (which would break). But since I hadn't updated the stub with the change in associations, the mistake probably never made it into the download.

  41. Gabriel Hase
    November 3rd, 2009 at 05:40 | #41

    As I don't want to screw with the RI can I ask you what happens if I create a new publication and provide the same tag twice (like "this is a test test")? Does it handle this?

  42. November 3rd, 2009 at 06:54 | #42

    @Gabriel Hase

    Try it.

    Please try and break the RI. If you've already broken something, send a URL that demonstrates it. The RI is not designed to handle previously-logged-in users coming back to URLs. Other than that, let me know of error messages, and give the steps to reproduce and any URLs.

    Shortly there will be a cron job to reset the database every 24 hours.

  43. Gabriel Hase
    November 3rd, 2009 at 06:55 | #43

    One more thing in general: Can I assume that the helper function text_field escapes dangerous injections (javascript and the like) for me? I tried to inject some "js tags" into the RI, but no success.

  44. November 3rd, 2009 at 06:58 | #44

    @Gabriel Hase

    "h" means html_escape (http://api.rubyonrails.org/classes/ERB/Util.html#M000315). The use of it in the RI may be irregular, but you should use it. In Rails 3.0 it will be on by default.

  45. Karin Berger
    November 5th, 2009 at 10:01 | #45

    Hi,

    In assgmt.4, task 1, I have implemented the method "self.up", but there is also a note for method "self.down" on that file (20091020035948_create_index_entries.rb). Can you please clarify what exactly needs to be implemented there?

    Thanks,
    Karin

  46. November 5th, 2009 at 10:08 | #46

    @Karin Berger

    The down method should reverse whatever up does.

    For example, if you create a table, drop it. If you add column(s), remove it/them.

    If you want examples, look in the back of AWDR for the sample code for the "Depot" app -- you should see sample migrations with both up and down methods. Also, of course, for all of the other migrations in the assn4 download, there are up and down migrations that will give you an idea.

    The *reason* for adding the down method is so that if your database is fully migrated to the newest version, and you have to go back, you can do stuff like

    rake db:migrate VERSION=20091020033440

    where that number is the version number of the migration to which you want to go.

    Say more if that's not enough.

  47. Ron Newman
    November 5th, 2009 at 10:12 | #47

    What does '?tag=cel-20' do in the Amazon links? Is that the affiliate code?

  48. November 5th, 2009 at 10:15 | #48

    @Ron Newman

    That's my Amazon affiliate code. As the internal comment says, there is a constant in environment.rb that defines it. You want to add it to any links to Amazon.

    Any purchases made through publications.plugh.org make money for the course which we will squander on soda and beer after the "professional panel" Dec. 3.

  49. Ron Newman
    November 5th, 2009 at 10:15 | #49

    I understand what the down() method is for, but the one you provided already drops the table. It's not evident to me that there's anything else we students need to add to it.

  50. November 5th, 2009 at 10:18 | #50

    @Ron Newman

    Well silly me, it looks like I fulfilled the assignment for you -- so there is nothing to do.

Comment pages
1 2 3 ... 5 493
  1. No trackbacks yet.