From 47992c07d3ff309b0921e7f01a62f7c665badb4d Mon Sep 17 00:00:00 2001 From: Alan Bradburne Date: Mon, 14 Apr 2008 17:45:16 +0100 Subject: [PATCH] inital import --- app/apis/backend_api.rb | 2 + app/apis/blogger_api.rb | 36 + app/apis/blogger_structs/blog.rb | 7 + app/apis/blogger_structs/post.rb | 8 + app/apis/blogger_structs/user.rb | 8 + app/controllers/account_controller.rb | 22 + app/controllers/application.rb | 8 + app/controllers/articles_controller.rb | 76 + app/controllers/backend_controller.rb | 5 + app/controllers/blogger_service.rb | 70 + app/controllers/blogs_controller.rb | 10 + app/controllers/categories_controller.rb | 61 + app/controllers/comments_controller.rb | 24 + app/controllers/entries_controller.rb | 73 + app/controllers/forums_controller.rb | 76 + app/controllers/friends_controller.rb | 54 + app/controllers/mobile/account_controller.rb | 20 + app/controllers/mobile/articles_controller.rb | 19 + app/controllers/mobile/blogs_controller.rb | 3 + app/controllers/mobile/entries_controller.rb | 19 + app/controllers/mobile/forums_controller.rb | 8 + app/controllers/mobile/pages_controller.rb | 3 + app/controllers/mobile/photos_controller.rb | 3 + app/controllers/mobile/posts_controller.rb | 17 + app/controllers/mobile/topics_controller.rb | 21 + .../mobile/user_photos_controller.rb | 3 + app/controllers/mobile/users_controller.rb | 3 + app/controllers/newsletters_controller.rb | 67 + app/controllers/pages_controller.rb | 48 + app/controllers/photos_controller.rb | 11 + app/controllers/posts_controller.rb | 97 + app/controllers/roles_controller.rb | 27 + app/controllers/tags_controller.rb | 9 + app/controllers/topics_controller.rb | 90 + app/controllers/user_photos_controller.rb | 138 + app/controllers/user_tags_controller.rb | 11 + app/controllers/users_controller.rb | 72 + app/controllers/usertemplates_controller.rb | 30 + app/drops/comment_drop.rb | 17 + app/drops/entry_drop.rb | 25 + app/drops/user_drop.rb | 21 + app/filters/text_filters.rb | 11 + app/helpers/account_helper.rb | 2 + app/helpers/application_helper.rb | 61 + app/helpers/articles_helper.rb | 2 + app/helpers/blogs_helper.rb | 2 + app/helpers/categories_helper.rb | 2 + app/helpers/comments_helper.rb | 2 + app/helpers/entries_helper.rb | 5 + app/helpers/forums_helper.rb | 2 + app/helpers/friends_helper.rb | 2 + app/helpers/newsletters_helper.rb | 2 + app/helpers/pages_helper.rb | 2 + app/helpers/photos_helper.rb | 2 + app/helpers/posts_helper.rb | 2 + app/helpers/tags_helper.rb | 2 + app/helpers/topics_helper.rb | 2 + app/helpers/user_tags_helper.rb | 2 + app/helpers/users_helper.rb | 2 + app/helpers/usertemplates_helper.rb | 2 + app/models/article.rb | 17 + app/models/category.rb | 4 + app/models/comment.rb | 9 + app/models/email.rb | 2 + app/models/entry.rb | 15 + app/models/forum.rb | 7 + app/models/friendship.rb | 61 + app/models/newsletter.rb | 5 + app/models/notifier.rb | 20 + app/models/page.rb | 14 + app/models/photo.rb | 18 + app/models/post.rb | 12 + app/models/role.rb | 3 + app/models/topic.rb | 8 + app/models/user.rb | 120 + app/models/usertemplate.rb | 4 + app/views/account/authenticate.rhtml | 2 + app/views/account/login.rhtml | 7 + app/views/account/logout.rhtml | 2 + app/views/articles/admin.rhtml | 21 + app/views/articles/atom.rxml | 28 + app/views/articles/create.rhtml | 2 + app/views/articles/destroy.rhtml | 2 + app/views/articles/edit.rhtml | 13 + app/views/articles/index.rhtml | 29 + app/views/articles/new.rhtml | 12 + app/views/articles/rss.rxml | 23 + app/views/articles/show.rhtml | 14 + app/views/articles/update.rhtml | 2 + app/views/blogs/index.rhtml | 14 + app/views/categories/admin.rhtml | 14 + app/views/categories/create.rhtml | 2 + app/views/categories/destroy.rhtml | 2 + app/views/categories/edit.rhtml | 8 + app/views/categories/index.rhtml | 8 + app/views/categories/new.rhtml | 6 + app/views/categories/show.rhtml | 2 + app/views/categories/update.rhtml | 2 + app/views/comments/edit.rhtml | 12 + app/views/comments/index.rhtml | 18 + app/views/comments/new.rhtml | 11 + app/views/comments/show.rhtml | 3 + app/views/entries/edit.rhtml | 11 + app/views/entries/index.rhtml | 31 + app/views/entries/new.rhtml | 11 + app/views/entries/show.rhtml | 35 + app/views/forums/_form.rhtml | 2 + app/views/forums/edit.rhtml | 10 + app/views/forums/index.rhtml | 35 + app/views/forums/new.rhtml | 7 + app/views/forums/show.rhtml | 3 + app/views/friends/_friendship.rhtml | 15 + app/views/friends/_friendship_with_edit.rhtml | 20 + app/views/friends/edit.rhtml | 68 + app/views/friends/index.rhtml | 9 + app/views/friends/new.rhtml | 61 + app/views/layouts/_menu.rhtml | 38 + app/views/layouts/_mobilemenu.rhtml | 14 + app/views/layouts/application.rhtml | 29 + app/views/layouts/mobile.rhtml | 28 + app/views/mobile/account/login.rhtml | 7 + app/views/mobile/articles/index.rhtml | 30 + app/views/mobile/articles/show.rhtml | 6 + app/views/mobile/blogs/index.rhtml | 14 + app/views/mobile/entries/index.rhtml | 32 + app/views/mobile/entries/show.rhtml | 3 + app/views/mobile/forums/index.rhtml | 10 + app/views/mobile/pages/show.rhtml | 2 + app/views/mobile/photos/index.rhtml | 21 + app/views/mobile/posts/index.rhtml | 34 + app/views/mobile/posts/new.rhtml | 11 + app/views/mobile/topics/index.rhtml | 30 + app/views/mobile/topics/new.rhtml | 11 + app/views/mobile/user_photos/index.rhtml | 21 + app/views/mobile/user_photos/show.rhtml | 6 + app/views/mobile/users/show.rhtml | 22 + app/views/newsletters/edit.rhtml | 24 + app/views/newsletters/index.rhtml | 27 + app/views/newsletters/new.rhtml | 22 + .../newsletters/newsletter.text.plain.rhtml | 3 + app/views/newsletters/show.rhtml | 31 + .../new_comment_notification.text.html.rhtml | 13 + .../new_comment_notification.text.plan.rhtml | 10 + app/views/pages/create.rhtml | 2 + app/views/pages/destroy.rhtml | 2 + app/views/pages/edit.rhtml | 9 + app/views/pages/index.rhtml | 13 + app/views/pages/new.rhtml | 9 + app/views/pages/show.rhtml | 2 + app/views/pages/update.rhtml | 2 + app/views/photos/_page_links.rhtml | 13 + app/views/photos/_photo.rhtml | 4 + app/views/photos/index.rhtml | 9 + app/views/posts/_textile_help.rhtml | 3 + app/views/posts/edit.rhtml | 14 + app/views/posts/index.rhtml | 64 + app/views/posts/new.rhtml | 14 + app/views/posts/show.rhtml | 3 + app/views/roles/_role.rhtml | 12 + app/views/roles/index.rhtml | 7 + app/views/tags/index.rhtml | 5 + app/views/tags/show.rhtml | 5 + app/views/topics/edit.rhtml | 11 + app/views/topics/index.rhtml | 51 + app/views/topics/new.rhtml | 6 + app/views/topics/show.rhtml | 3 + app/views/user_photos/_edit_tag.rhtml | 9 + app/views/user_photos/add_tag.rjs | 3 + app/views/user_photos/edit.rhtml | 40 + app/views/user_photos/index.rhtml | 13 + app/views/user_photos/new.rhtml | 29 + app/views/user_photos/remove_tag.rjs | 2 + app/views/user_photos/show.rhtml | 41 + app/views/user_tags/index.rhtml | 7 + app/views/user_tags/show.rhtml | 9 + app/views/users/_user.rhtml | 14 + app/views/users/create.rhtml | 2 + app/views/users/destroy.rhtml | 2 + app/views/users/edit.rhtml | 16 + app/views/users/enable.rhtml | 2 + app/views/users/index.rhtml | 11 + app/views/users/new.rhtml | 13 + app/views/users/show.rhtml | 58 + app/views/users/update.rhtml | 2 + app/views/usertemplates/edit.rhtml | 12 + app/views/usertemplates/index.rhtml | 17 + config/boot.rb | 45 + config/database.yml | 33 + config/environment.rb | 62 + config/environments/development.rb | 22 + config/environments/production.rb | 18 + config/environments/test.rb | 19 + config/gmaps_api_key.yml | 13 + config/routes.rb | 134 + db/migrate/001_create_pages.rb | 19 + db/migrate/002_create_users.rb | 19 + db/migrate/003_create_roles.rb | 13 + db/migrate/004_create_roles_users_join.rb | 21 + db/migrate/005_create_articles.rb | 19 + db/migrate/006_create_categories.rb | 14 + db/migrate/007_add_editor_role.rb | 14 + db/migrate/008_create_forums.rb | 15 + db/migrate/009_create_topics.rb | 17 + db/migrate/010_create_posts.rb | 16 + db/migrate/011_add_user_posts_count.rb | 9 + db/migrate/012_add_moderator_role.rb | 14 + db/migrate/013_create_entries.rb | 17 + db/migrate/014_create_comments.rb | 18 + db/migrate/015_add_blog_settings_to_user.rb | 13 + db/migrate/016_create_photos.rb | 26 + db/migrate/017_create_emails.rb | 14 + db/migrate/018_create_newsletters.rb | 15 + db/migrate/019_create_friendships.rb | 31 + db/migrate/020_add_users_latest_activity.rb | 11 + db/migrate/021_add_tagging_support.rb | 22 + db/migrate/022_add_geo_to_photos.rb | 13 + db/migrate/023_add_flickr_user_fields.rb | 11 + db/migrate/024_create_usertemplates.rb | 13 + db/migrate/025_add_sessions.rb | 16 + db/schema.rb | 203 ++ doc/.gitignore | 0 doc/README_FOR_APP | 2 + lib/login_system.rb | 105 + log/.gitignore | 0 public/.htaccess | 40 + public/404.html | 30 + public/500.html | 30 + public/dispatch.cgi | 10 + public/dispatch.fcgi | 24 + public/dispatch.rb | 10 + public/favicon.ico | 0 public/images/h-grad.png | Bin 0 -> 2817 bytes public/images/logo.png | Bin 0 -> 3738 bytes public/images/rails.png | Bin 0 -> 1787 bytes public/images/xfn-child.png | Bin 0 -> 588 bytes public/images/xfn-colleague-met.png | Bin 0 -> 430 bytes public/images/xfn-colleague.png | Bin 0 -> 413 bytes public/images/xfn-friend-met.png | Bin 0 -> 435 bytes public/images/xfn-friend.png | Bin 0 -> 413 bytes public/images/xfn-me.png | Bin 0 -> 385 bytes public/images/xfn-parent.png | Bin 0 -> 605 bytes public/images/xfn-small.png | Bin 0 -> 1333 bytes public/images/xfn-spouse.png | Bin 0 -> 853 bytes public/images/xfn-sweetheart-met.png | Bin 0 -> 402 bytes public/images/xfn-sweetheart.png | Bin 0 -> 390 bytes public/javascripts/application.js | 10 + public/javascripts/clusterer.js | 444 +++ public/javascripts/controls.js | 833 ++++++ public/javascripts/dragdrop.js | 942 ++++++ public/javascripts/effects.js | 1088 +++++++ public/javascripts/geoRssOverlay.js | 194 ++ public/javascripts/markerGroup.js | 114 + public/javascripts/prototype.js | 2515 +++++++++++++++++ public/javascripts/wms-gs.js | 69 + public/javascripts/ym4r-gm.js | 117 + public/robots.txt | 1 + public/stylesheets/main.css | 147 + public/stylesheets/mobile.css | 9 + public/stylesheets/scaffold.css | 74 + script/about | 3 + script/breakpointer | 3 + script/console | 3 + script/destroy | 3 + script/generate | 3 + script/performance/benchmarker | 3 + script/performance/profiler | 3 + script/plugin | 3 + script/process/inspector | 3 + script/process/reaper | 3 + script/process/spawner | 3 + script/runner | 3 + script/server | 3 + test/fixtures/articles.yml | 22 + test/fixtures/categories.yml | 6 + test/fixtures/comments.yml | 6 + test/fixtures/emails.yml | 5 + test/fixtures/entries.yml | 7 + test/fixtures/forums.yml | 4 + test/fixtures/friendships.yml | 5 + test/fixtures/newsletters.yml | 5 + test/fixtures/pages.yml | 14 + test/fixtures/photos.yml | 28 + test/fixtures/posts.yml | 6 + test/fixtures/roles.yml | 9 + test/fixtures/roles_users.yml | 9 + test/fixtures/topics.yml | 6 + test/fixtures/users.yml | 26 + test/fixtures/usertemplates.yml | 15 + test/functional/account_controller_test.rb | 35 + test/functional/articles_controller_test.rb | 53 + test/functional/backend_api_test.rb | 46 + test/functional/blogs_controller_test.rb | 18 + test/functional/categories_controller_test.rb | 18 + test/functional/comments_controller_test.rb | 42 + test/functional/entries_controller_test.rb | 61 + test/functional/forums_controller_test.rb | 61 + test/functional/friends_controller_test.rb | 64 + .../functional/newsletters_controller_test.rb | 64 + test/functional/pages_controller_test.rb | 18 + test/functional/photos_controller_test.rb | 18 + test/functional/posts_controller_test.rb | 62 + test/functional/tags_controller_test.rb | 18 + test/functional/topics_controller_test.rb | 65 + .../functional/user_photos_controller_test.rb | 90 + test/functional/user_tags_controller_test.rb | 18 + test/functional/users_controller_test.rb | 36 + .../usertemplates_controller_test.rb | 48 + test/integration/articles_stories_test.rb | 19 + test/integration/login_stories_test.rb | 34 + test/integration/mobile_login_stories_test.rb | 29 + test/test_helper.rb | 31 + test/unit/article_test.rb | 10 + test/unit/category_test.rb | 10 + test/unit/comment_test.rb | 10 + test/unit/email_test.rb | 10 + test/unit/entry_test.rb | 10 + test/unit/forum_test.rb | 10 + test/unit/friendship_test.rb | 10 + test/unit/newsletter_test.rb | 10 + test/unit/notifier_test.rb | 37 + test/unit/page_test.rb | 25 + test/unit/photo_test.rb | 47 + test/unit/post_test.rb | 10 + test/unit/role_test.rb | 10 + test/unit/topic_test.rb | 10 + test/unit/user_test.rb | 18 + test/unit/usertemplate_test.rb | 10 + .../acts_as_taggable_on_steroids/CHANGELOG | 52 + .../acts_as_taggable_on_steroids/MIT-LICENSE | 20 + .../acts_as_taggable_on_steroids/README | 116 + .../acts_as_taggable_on_steroids/Rakefile | 22 + .../acts_as_taggable_migration_generator.rb | 11 + .../templates/migration.rb | 26 + .../acts_as_taggable_on_steroids/init.rb | 4 + .../lib/acts_as_taggable.rb | 155 + .../acts_as_taggable_on_steroids/lib/tag.rb | 22 + .../lib/tag_counts_extension.rb | 2 + .../lib/tag_list.rb | 57 + .../lib/tagging.rb | 4 + .../test/abstract_unit.rb | 82 + .../test/acts_as_taggable_test.rb | 211 ++ .../test/database.yml | 10 + .../test/fixtures/photo.rb | 8 + .../test/fixtures/photos.yml | 24 + .../test/fixtures/post.rb | 7 + .../test/fixtures/posts.yml | 24 + .../test/fixtures/taggings.yml | 126 + .../test/fixtures/tags.yml | 19 + .../test/fixtures/user.rb | 4 + .../test/fixtures/users.yml | 7 + .../test/schema.rb | 27 + .../test/tag_list_test.rb | 98 + .../test/tag_test.rb | 34 + .../test/tagging_test.rb | 13 + vendor/plugins/attachment_fu/CHANGELOG | 19 + vendor/plugins/attachment_fu/README | 162 ++ vendor/plugins/attachment_fu/Rakefile | 22 + .../plugins/attachment_fu/amazon_s3.yml.tpl | 14 + vendor/plugins/attachment_fu/init.rb | 14 + vendor/plugins/attachment_fu/install.rb | 5 + vendor/plugins/attachment_fu/lib/geometry.rb | 93 + .../lib/technoweenie/attachment_fu.rb | 405 +++ .../attachment_fu/backends/db_file_backend.rb | 39 + .../backends/file_system_backend.rb | 97 + .../attachment_fu/backends/s3_backend.rb | 309 ++ .../processors/image_science_processor.rb | 55 + .../processors/mini_magick_processor.rb | 56 + .../processors/rmagick_processor.rb | 53 + .../plugins/attachment_fu/test/amazon_s3.yml | 6 + .../test/backends/db_file_test.rb | 16 + .../test/backends/file_system_test.rb | 80 + .../test/backends/remote/s3_test.rb | 103 + .../test/base_attachment_tests.rb | 57 + .../plugins/attachment_fu/test/basic_test.rb | 64 + .../plugins/attachment_fu/test/database.yml | 18 + .../test/extra_attachment_test.rb | 57 + .../attachment_fu/test/fixtures/attachment.rb | 127 + .../test/fixtures/files/fake/rails.png | Bin 0 -> 4217 bytes .../attachment_fu/test/fixtures/files/foo.txt | 1 + .../test/fixtures/files/rails.png | Bin 0 -> 1787 bytes .../attachment_fu/test/geometry_test.rb | 101 + .../test/processors/image_science_test.rb | 31 + .../test/processors/mini_magick_test.rb | 31 + .../test/processors/rmagick_test.rb | 240 ++ vendor/plugins/attachment_fu/test/schema.rb | 86 + .../plugins/attachment_fu/test/test_helper.rb | 142 + .../attachment_fu/test/validation_test.rb | 55 + vendor/plugins/liquid/CHANGELOG | 38 + vendor/plugins/liquid/MIT-LICENSE | 20 + vendor/plugins/liquid/Manifest.txt | 60 + vendor/plugins/liquid/README | 38 + vendor/plugins/liquid/Rakefile | 24 + .../liquid/example/server/example_servlet.rb | 37 + .../liquid/example/server/liquid_servlet.rb | 28 + .../plugins/liquid/example/server/server.rb | 12 + .../example/server/templates/index.liquid | 6 + .../example/server/templates/products.liquid | 45 + vendor/plugins/liquid/init.rb | 6 + .../plugins/liquid/lib/extras/liquid_view.rb | 27 + vendor/plugins/liquid/lib/liquid.rb | 66 + vendor/plugins/liquid/lib/liquid/block.rb | 102 + vendor/plugins/liquid/lib/liquid/condition.rb | 99 + vendor/plugins/liquid/lib/liquid/context.rb | 243 ++ vendor/plugins/liquid/lib/liquid/document.rb | 17 + vendor/plugins/liquid/lib/liquid/drop.rb | 48 + vendor/plugins/liquid/lib/liquid/errors.rb | 7 + .../plugins/liquid/lib/liquid/extensions.rb | 56 + .../plugins/liquid/lib/liquid/file_system.rb | 62 + vendor/plugins/liquid/lib/liquid/htmltags.rb | 64 + .../liquid/lib/liquid/standardfilters.rb | 133 + vendor/plugins/liquid/lib/liquid/strainer.rb | 43 + vendor/plugins/liquid/lib/liquid/tag.rb | 26 + .../plugins/liquid/lib/liquid/tags/assign.rb | 33 + .../plugins/liquid/lib/liquid/tags/capture.rb | 35 + vendor/plugins/liquid/lib/liquid/tags/case.rb | 83 + .../plugins/liquid/lib/liquid/tags/comment.rb | 9 + .../plugins/liquid/lib/liquid/tags/cycle.rb | 60 + vendor/plugins/liquid/lib/liquid/tags/for.rb | 118 + vendor/plugins/liquid/lib/liquid/tags/if.rb | 66 + .../liquid/lib/liquid/tags/ifchanged.rb | 20 + .../plugins/liquid/lib/liquid/tags/include.rb | 55 + .../plugins/liquid/lib/liquid/tags/unless.rb | 33 + vendor/plugins/liquid/lib/liquid/template.rb | 145 + vendor/plugins/liquid/lib/liquid/variable.rb | 52 + vendor/plugins/liquid/test/block_test.rb | 58 + vendor/plugins/liquid/test/condition_test.rb | 75 + vendor/plugins/liquid/test/context_test.rb | 411 +++ vendor/plugins/liquid/test/drop_test.rb | 141 + .../liquid/test/error_handling_test.rb | 65 + .../plugins/liquid/test/extra/breakpoint.rb | 547 ++++ vendor/plugins/liquid/test/extra/caller.rb | 80 + .../plugins/liquid/test/file_system_test.rb | 30 + vendor/plugins/liquid/test/filter_test.rb | 98 + vendor/plugins/liquid/test/helper.rb | 20 + vendor/plugins/liquid/test/html_tag_test.rb | 24 + vendor/plugins/liquid/test/if_else_test.rb | 104 + .../plugins/liquid/test/include_tag_test.rb | 96 + vendor/plugins/liquid/test/output_test.rb | 121 + .../liquid/test/parsing_quirks_test.rb | 22 + vendor/plugins/liquid/test/regexp_test.rb | 40 + vendor/plugins/liquid/test/security_test.rb | 41 + .../liquid/test/standard_filter_test.rb | 107 + .../plugins/liquid/test/standard_tag_test.rb | 385 +++ vendor/plugins/liquid/test/statements_test.rb | 137 + vendor/plugins/liquid/test/strainer_test.rb | 16 + vendor/plugins/liquid/test/template_test.rb | 26 + vendor/plugins/liquid/test/test_helper.rb | 20 + .../plugins/liquid/test/unless_else_test.rb | 27 + vendor/plugins/liquid/test/variable_test.rb | 135 + vendor/plugins/ym4r_gm/README | 391 +++ .../plugins/ym4r_gm/gmaps_api_key.yml.sample | 14 + vendor/plugins/ym4r_gm/init.rb | 3 + vendor/plugins/ym4r_gm/install.rb | 10 + .../plugins/ym4r_gm/javascript/clusterer.js | 444 +++ .../ym4r_gm/javascript/geoRssOverlay.js | 194 ++ .../plugins/ym4r_gm/javascript/markerGroup.js | 114 + vendor/plugins/ym4r_gm/javascript/wms-gs.js | 69 + vendor/plugins/ym4r_gm/javascript/ym4r-gm.js | 117 + .../plugins/ym4r_gm/lib/gm_plugin/control.rb | 64 + .../ym4r_gm/lib/gm_plugin/geocoding.rb | 116 + .../plugins/ym4r_gm/lib/gm_plugin/helper.rb | 41 + vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb | 37 + vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb | 125 + vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb | 268 ++ .../plugins/ym4r_gm/lib/gm_plugin/mapping.rb | 128 + .../plugins/ym4r_gm/lib/gm_plugin/overlay.rb | 386 +++ vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb | 34 + vendor/plugins/ym4r_gm/lib/ym4r_gm.rb | 11 + vendor/plugins/ym4r_gm/rakefile.rb | 22 + vendor/plugins/ym4r_gm/tasks/gm_tasks.rake | 4 + vendor/plugins/ym4r_gm/test/gm_test.rb | 79 + 471 files changed, 23821 insertions(+) create mode 100644 app/apis/backend_api.rb create mode 100644 app/apis/blogger_api.rb create mode 100644 app/apis/blogger_structs/blog.rb create mode 100644 app/apis/blogger_structs/post.rb create mode 100644 app/apis/blogger_structs/user.rb create mode 100644 app/controllers/account_controller.rb create mode 100644 app/controllers/application.rb create mode 100644 app/controllers/articles_controller.rb create mode 100644 app/controllers/backend_controller.rb create mode 100644 app/controllers/blogger_service.rb create mode 100644 app/controllers/blogs_controller.rb create mode 100644 app/controllers/categories_controller.rb create mode 100644 app/controllers/comments_controller.rb create mode 100644 app/controllers/entries_controller.rb create mode 100644 app/controllers/forums_controller.rb create mode 100644 app/controllers/friends_controller.rb create mode 100644 app/controllers/mobile/account_controller.rb create mode 100644 app/controllers/mobile/articles_controller.rb create mode 100644 app/controllers/mobile/blogs_controller.rb create mode 100644 app/controllers/mobile/entries_controller.rb create mode 100644 app/controllers/mobile/forums_controller.rb create mode 100644 app/controllers/mobile/pages_controller.rb create mode 100644 app/controllers/mobile/photos_controller.rb create mode 100644 app/controllers/mobile/posts_controller.rb create mode 100644 app/controllers/mobile/topics_controller.rb create mode 100644 app/controllers/mobile/user_photos_controller.rb create mode 100644 app/controllers/mobile/users_controller.rb create mode 100644 app/controllers/newsletters_controller.rb create mode 100644 app/controllers/pages_controller.rb create mode 100644 app/controllers/photos_controller.rb create mode 100644 app/controllers/posts_controller.rb create mode 100644 app/controllers/roles_controller.rb create mode 100644 app/controllers/tags_controller.rb create mode 100644 app/controllers/topics_controller.rb create mode 100644 app/controllers/user_photos_controller.rb create mode 100644 app/controllers/user_tags_controller.rb create mode 100644 app/controllers/users_controller.rb create mode 100644 app/controllers/usertemplates_controller.rb create mode 100644 app/drops/comment_drop.rb create mode 100644 app/drops/entry_drop.rb create mode 100644 app/drops/user_drop.rb create mode 100644 app/filters/text_filters.rb create mode 100644 app/helpers/account_helper.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/articles_helper.rb create mode 100644 app/helpers/blogs_helper.rb create mode 100644 app/helpers/categories_helper.rb create mode 100644 app/helpers/comments_helper.rb create mode 100644 app/helpers/entries_helper.rb create mode 100644 app/helpers/forums_helper.rb create mode 100644 app/helpers/friends_helper.rb create mode 100644 app/helpers/newsletters_helper.rb create mode 100644 app/helpers/pages_helper.rb create mode 100644 app/helpers/photos_helper.rb create mode 100644 app/helpers/posts_helper.rb create mode 100644 app/helpers/tags_helper.rb create mode 100644 app/helpers/topics_helper.rb create mode 100644 app/helpers/user_tags_helper.rb create mode 100644 app/helpers/users_helper.rb create mode 100644 app/helpers/usertemplates_helper.rb create mode 100644 app/models/article.rb create mode 100644 app/models/category.rb create mode 100644 app/models/comment.rb create mode 100644 app/models/email.rb create mode 100644 app/models/entry.rb create mode 100644 app/models/forum.rb create mode 100644 app/models/friendship.rb create mode 100644 app/models/newsletter.rb create mode 100644 app/models/notifier.rb create mode 100644 app/models/page.rb create mode 100644 app/models/photo.rb create mode 100644 app/models/post.rb create mode 100644 app/models/role.rb create mode 100644 app/models/topic.rb create mode 100644 app/models/user.rb create mode 100644 app/models/usertemplate.rb create mode 100644 app/views/account/authenticate.rhtml create mode 100644 app/views/account/login.rhtml create mode 100644 app/views/account/logout.rhtml create mode 100644 app/views/articles/admin.rhtml create mode 100644 app/views/articles/atom.rxml create mode 100644 app/views/articles/create.rhtml create mode 100644 app/views/articles/destroy.rhtml create mode 100644 app/views/articles/edit.rhtml create mode 100644 app/views/articles/index.rhtml create mode 100644 app/views/articles/new.rhtml create mode 100644 app/views/articles/rss.rxml create mode 100644 app/views/articles/show.rhtml create mode 100644 app/views/articles/update.rhtml create mode 100644 app/views/blogs/index.rhtml create mode 100644 app/views/categories/admin.rhtml create mode 100644 app/views/categories/create.rhtml create mode 100644 app/views/categories/destroy.rhtml create mode 100644 app/views/categories/edit.rhtml create mode 100644 app/views/categories/index.rhtml create mode 100644 app/views/categories/new.rhtml create mode 100644 app/views/categories/show.rhtml create mode 100644 app/views/categories/update.rhtml create mode 100644 app/views/comments/edit.rhtml create mode 100644 app/views/comments/index.rhtml create mode 100644 app/views/comments/new.rhtml create mode 100644 app/views/comments/show.rhtml create mode 100644 app/views/entries/edit.rhtml create mode 100644 app/views/entries/index.rhtml create mode 100644 app/views/entries/new.rhtml create mode 100644 app/views/entries/show.rhtml create mode 100644 app/views/forums/_form.rhtml create mode 100644 app/views/forums/edit.rhtml create mode 100644 app/views/forums/index.rhtml create mode 100644 app/views/forums/new.rhtml create mode 100644 app/views/forums/show.rhtml create mode 100644 app/views/friends/_friendship.rhtml create mode 100644 app/views/friends/_friendship_with_edit.rhtml create mode 100644 app/views/friends/edit.rhtml create mode 100644 app/views/friends/index.rhtml create mode 100644 app/views/friends/new.rhtml create mode 100644 app/views/layouts/_menu.rhtml create mode 100644 app/views/layouts/_mobilemenu.rhtml create mode 100644 app/views/layouts/application.rhtml create mode 100644 app/views/layouts/mobile.rhtml create mode 100644 app/views/mobile/account/login.rhtml create mode 100644 app/views/mobile/articles/index.rhtml create mode 100644 app/views/mobile/articles/show.rhtml create mode 100644 app/views/mobile/blogs/index.rhtml create mode 100644 app/views/mobile/entries/index.rhtml create mode 100644 app/views/mobile/entries/show.rhtml create mode 100644 app/views/mobile/forums/index.rhtml create mode 100644 app/views/mobile/pages/show.rhtml create mode 100644 app/views/mobile/photos/index.rhtml create mode 100644 app/views/mobile/posts/index.rhtml create mode 100644 app/views/mobile/posts/new.rhtml create mode 100644 app/views/mobile/topics/index.rhtml create mode 100644 app/views/mobile/topics/new.rhtml create mode 100644 app/views/mobile/user_photos/index.rhtml create mode 100644 app/views/mobile/user_photos/show.rhtml create mode 100644 app/views/mobile/users/show.rhtml create mode 100644 app/views/newsletters/edit.rhtml create mode 100644 app/views/newsletters/index.rhtml create mode 100644 app/views/newsletters/new.rhtml create mode 100644 app/views/newsletters/newsletter.text.plain.rhtml create mode 100644 app/views/newsletters/show.rhtml create mode 100644 app/views/notifier/new_comment_notification.text.html.rhtml create mode 100644 app/views/notifier/new_comment_notification.text.plan.rhtml create mode 100644 app/views/pages/create.rhtml create mode 100644 app/views/pages/destroy.rhtml create mode 100644 app/views/pages/edit.rhtml create mode 100644 app/views/pages/index.rhtml create mode 100644 app/views/pages/new.rhtml create mode 100644 app/views/pages/show.rhtml create mode 100644 app/views/pages/update.rhtml create mode 100644 app/views/photos/_page_links.rhtml create mode 100644 app/views/photos/_photo.rhtml create mode 100644 app/views/photos/index.rhtml create mode 100644 app/views/posts/_textile_help.rhtml create mode 100644 app/views/posts/edit.rhtml create mode 100644 app/views/posts/index.rhtml create mode 100644 app/views/posts/new.rhtml create mode 100644 app/views/posts/show.rhtml create mode 100644 app/views/roles/_role.rhtml create mode 100644 app/views/roles/index.rhtml create mode 100644 app/views/tags/index.rhtml create mode 100644 app/views/tags/show.rhtml create mode 100644 app/views/topics/edit.rhtml create mode 100644 app/views/topics/index.rhtml create mode 100644 app/views/topics/new.rhtml create mode 100644 app/views/topics/show.rhtml create mode 100644 app/views/user_photos/_edit_tag.rhtml create mode 100644 app/views/user_photos/add_tag.rjs create mode 100644 app/views/user_photos/edit.rhtml create mode 100644 app/views/user_photos/index.rhtml create mode 100644 app/views/user_photos/new.rhtml create mode 100644 app/views/user_photos/remove_tag.rjs create mode 100644 app/views/user_photos/show.rhtml create mode 100644 app/views/user_tags/index.rhtml create mode 100644 app/views/user_tags/show.rhtml create mode 100644 app/views/users/_user.rhtml create mode 100644 app/views/users/create.rhtml create mode 100644 app/views/users/destroy.rhtml create mode 100644 app/views/users/edit.rhtml create mode 100644 app/views/users/enable.rhtml create mode 100644 app/views/users/index.rhtml create mode 100644 app/views/users/new.rhtml create mode 100644 app/views/users/show.rhtml create mode 100644 app/views/users/update.rhtml create mode 100644 app/views/usertemplates/edit.rhtml create mode 100644 app/views/usertemplates/index.rhtml create mode 100644 config/boot.rb create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/gmaps_api_key.yml create mode 100644 config/routes.rb create mode 100644 db/migrate/001_create_pages.rb create mode 100644 db/migrate/002_create_users.rb create mode 100644 db/migrate/003_create_roles.rb create mode 100644 db/migrate/004_create_roles_users_join.rb create mode 100644 db/migrate/005_create_articles.rb create mode 100644 db/migrate/006_create_categories.rb create mode 100644 db/migrate/007_add_editor_role.rb create mode 100644 db/migrate/008_create_forums.rb create mode 100644 db/migrate/009_create_topics.rb create mode 100644 db/migrate/010_create_posts.rb create mode 100644 db/migrate/011_add_user_posts_count.rb create mode 100644 db/migrate/012_add_moderator_role.rb create mode 100644 db/migrate/013_create_entries.rb create mode 100644 db/migrate/014_create_comments.rb create mode 100644 db/migrate/015_add_blog_settings_to_user.rb create mode 100644 db/migrate/016_create_photos.rb create mode 100644 db/migrate/017_create_emails.rb create mode 100644 db/migrate/018_create_newsletters.rb create mode 100644 db/migrate/019_create_friendships.rb create mode 100644 db/migrate/020_add_users_latest_activity.rb create mode 100644 db/migrate/021_add_tagging_support.rb create mode 100644 db/migrate/022_add_geo_to_photos.rb create mode 100644 db/migrate/023_add_flickr_user_fields.rb create mode 100644 db/migrate/024_create_usertemplates.rb create mode 100644 db/migrate/025_add_sessions.rb create mode 100644 db/schema.rb create mode 100644 doc/.gitignore create mode 100644 doc/README_FOR_APP create mode 100644 lib/login_system.rb create mode 100644 log/.gitignore create mode 100644 public/.htaccess create mode 100644 public/404.html create mode 100644 public/500.html create mode 100755 public/dispatch.cgi create mode 100755 public/dispatch.fcgi create mode 100755 public/dispatch.rb create mode 100644 public/favicon.ico create mode 100644 public/images/h-grad.png create mode 100644 public/images/logo.png create mode 100644 public/images/rails.png create mode 100644 public/images/xfn-child.png create mode 100644 public/images/xfn-colleague-met.png create mode 100644 public/images/xfn-colleague.png create mode 100644 public/images/xfn-friend-met.png create mode 100644 public/images/xfn-friend.png create mode 100644 public/images/xfn-me.png create mode 100644 public/images/xfn-parent.png create mode 100644 public/images/xfn-small.png create mode 100644 public/images/xfn-spouse.png create mode 100644 public/images/xfn-sweetheart-met.png create mode 100644 public/images/xfn-sweetheart.png create mode 100644 public/javascripts/application.js create mode 100644 public/javascripts/clusterer.js create mode 100644 public/javascripts/controls.js create mode 100644 public/javascripts/dragdrop.js create mode 100644 public/javascripts/effects.js create mode 100644 public/javascripts/geoRssOverlay.js create mode 100644 public/javascripts/markerGroup.js create mode 100644 public/javascripts/prototype.js create mode 100644 public/javascripts/wms-gs.js create mode 100644 public/javascripts/ym4r-gm.js create mode 100644 public/robots.txt create mode 100644 public/stylesheets/main.css create mode 100644 public/stylesheets/mobile.css create mode 100644 public/stylesheets/scaffold.css create mode 100755 script/about create mode 100755 script/breakpointer create mode 100755 script/console create mode 100755 script/destroy create mode 100755 script/generate create mode 100755 script/performance/benchmarker create mode 100755 script/performance/profiler create mode 100755 script/plugin create mode 100755 script/process/inspector create mode 100755 script/process/reaper create mode 100755 script/process/spawner create mode 100755 script/runner create mode 100755 script/server create mode 100644 test/fixtures/articles.yml create mode 100644 test/fixtures/categories.yml create mode 100644 test/fixtures/comments.yml create mode 100644 test/fixtures/emails.yml create mode 100644 test/fixtures/entries.yml create mode 100644 test/fixtures/forums.yml create mode 100644 test/fixtures/friendships.yml create mode 100644 test/fixtures/newsletters.yml create mode 100644 test/fixtures/pages.yml create mode 100644 test/fixtures/photos.yml create mode 100644 test/fixtures/posts.yml create mode 100644 test/fixtures/roles.yml create mode 100644 test/fixtures/roles_users.yml create mode 100644 test/fixtures/topics.yml create mode 100644 test/fixtures/users.yml create mode 100644 test/fixtures/usertemplates.yml create mode 100644 test/functional/account_controller_test.rb create mode 100644 test/functional/articles_controller_test.rb create mode 100644 test/functional/backend_api_test.rb create mode 100644 test/functional/blogs_controller_test.rb create mode 100644 test/functional/categories_controller_test.rb create mode 100644 test/functional/comments_controller_test.rb create mode 100644 test/functional/entries_controller_test.rb create mode 100644 test/functional/forums_controller_test.rb create mode 100644 test/functional/friends_controller_test.rb create mode 100644 test/functional/newsletters_controller_test.rb create mode 100644 test/functional/pages_controller_test.rb create mode 100644 test/functional/photos_controller_test.rb create mode 100644 test/functional/posts_controller_test.rb create mode 100644 test/functional/tags_controller_test.rb create mode 100644 test/functional/topics_controller_test.rb create mode 100644 test/functional/user_photos_controller_test.rb create mode 100644 test/functional/user_tags_controller_test.rb create mode 100644 test/functional/users_controller_test.rb create mode 100644 test/functional/usertemplates_controller_test.rb create mode 100644 test/integration/articles_stories_test.rb create mode 100644 test/integration/login_stories_test.rb create mode 100644 test/integration/mobile_login_stories_test.rb create mode 100644 test/test_helper.rb create mode 100644 test/unit/article_test.rb create mode 100644 test/unit/category_test.rb create mode 100644 test/unit/comment_test.rb create mode 100644 test/unit/email_test.rb create mode 100644 test/unit/entry_test.rb create mode 100644 test/unit/forum_test.rb create mode 100644 test/unit/friendship_test.rb create mode 100644 test/unit/newsletter_test.rb create mode 100644 test/unit/notifier_test.rb create mode 100644 test/unit/page_test.rb create mode 100644 test/unit/photo_test.rb create mode 100644 test/unit/post_test.rb create mode 100644 test/unit/role_test.rb create mode 100644 test/unit/topic_test.rb create mode 100644 test/unit/user_test.rb create mode 100644 test/unit/usertemplate_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/README create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/Rakefile create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/init.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/database.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb create mode 100644 vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb create mode 100644 vendor/plugins/attachment_fu/CHANGELOG create mode 100644 vendor/plugins/attachment_fu/README create mode 100644 vendor/plugins/attachment_fu/Rakefile create mode 100644 vendor/plugins/attachment_fu/amazon_s3.yml.tpl create mode 100644 vendor/plugins/attachment_fu/init.rb create mode 100644 vendor/plugins/attachment_fu/install.rb create mode 100644 vendor/plugins/attachment_fu/lib/geometry.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb create mode 100644 vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb create mode 100644 vendor/plugins/attachment_fu/test/amazon_s3.yml create mode 100644 vendor/plugins/attachment_fu/test/backends/db_file_test.rb create mode 100644 vendor/plugins/attachment_fu/test/backends/file_system_test.rb create mode 100644 vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb create mode 100644 vendor/plugins/attachment_fu/test/base_attachment_tests.rb create mode 100644 vendor/plugins/attachment_fu/test/basic_test.rb create mode 100644 vendor/plugins/attachment_fu/test/database.yml create mode 100644 vendor/plugins/attachment_fu/test/extra_attachment_test.rb create mode 100644 vendor/plugins/attachment_fu/test/fixtures/attachment.rb create mode 100644 vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png create mode 100644 vendor/plugins/attachment_fu/test/fixtures/files/foo.txt create mode 100644 vendor/plugins/attachment_fu/test/fixtures/files/rails.png create mode 100644 vendor/plugins/attachment_fu/test/geometry_test.rb create mode 100644 vendor/plugins/attachment_fu/test/processors/image_science_test.rb create mode 100644 vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb create mode 100644 vendor/plugins/attachment_fu/test/processors/rmagick_test.rb create mode 100644 vendor/plugins/attachment_fu/test/schema.rb create mode 100644 vendor/plugins/attachment_fu/test/test_helper.rb create mode 100644 vendor/plugins/attachment_fu/test/validation_test.rb create mode 100644 vendor/plugins/liquid/CHANGELOG create mode 100644 vendor/plugins/liquid/MIT-LICENSE create mode 100644 vendor/plugins/liquid/Manifest.txt create mode 100644 vendor/plugins/liquid/README create mode 100755 vendor/plugins/liquid/Rakefile create mode 100644 vendor/plugins/liquid/example/server/example_servlet.rb create mode 100644 vendor/plugins/liquid/example/server/liquid_servlet.rb create mode 100644 vendor/plugins/liquid/example/server/server.rb create mode 100644 vendor/plugins/liquid/example/server/templates/index.liquid create mode 100644 vendor/plugins/liquid/example/server/templates/products.liquid create mode 100644 vendor/plugins/liquid/init.rb create mode 100644 vendor/plugins/liquid/lib/extras/liquid_view.rb create mode 100644 vendor/plugins/liquid/lib/liquid.rb create mode 100644 vendor/plugins/liquid/lib/liquid/block.rb create mode 100644 vendor/plugins/liquid/lib/liquid/condition.rb create mode 100644 vendor/plugins/liquid/lib/liquid/context.rb create mode 100644 vendor/plugins/liquid/lib/liquid/document.rb create mode 100644 vendor/plugins/liquid/lib/liquid/drop.rb create mode 100644 vendor/plugins/liquid/lib/liquid/errors.rb create mode 100644 vendor/plugins/liquid/lib/liquid/extensions.rb create mode 100644 vendor/plugins/liquid/lib/liquid/file_system.rb create mode 100644 vendor/plugins/liquid/lib/liquid/htmltags.rb create mode 100644 vendor/plugins/liquid/lib/liquid/standardfilters.rb create mode 100644 vendor/plugins/liquid/lib/liquid/strainer.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tag.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/assign.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/capture.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/case.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/comment.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/cycle.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/for.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/if.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/include.rb create mode 100644 vendor/plugins/liquid/lib/liquid/tags/unless.rb create mode 100644 vendor/plugins/liquid/lib/liquid/template.rb create mode 100644 vendor/plugins/liquid/lib/liquid/variable.rb create mode 100644 vendor/plugins/liquid/test/block_test.rb create mode 100644 vendor/plugins/liquid/test/condition_test.rb create mode 100644 vendor/plugins/liquid/test/context_test.rb create mode 100644 vendor/plugins/liquid/test/drop_test.rb create mode 100644 vendor/plugins/liquid/test/error_handling_test.rb create mode 100755 vendor/plugins/liquid/test/extra/breakpoint.rb create mode 100755 vendor/plugins/liquid/test/extra/caller.rb create mode 100644 vendor/plugins/liquid/test/file_system_test.rb create mode 100644 vendor/plugins/liquid/test/filter_test.rb create mode 100644 vendor/plugins/liquid/test/helper.rb create mode 100644 vendor/plugins/liquid/test/html_tag_test.rb create mode 100644 vendor/plugins/liquid/test/if_else_test.rb create mode 100644 vendor/plugins/liquid/test/include_tag_test.rb create mode 100644 vendor/plugins/liquid/test/output_test.rb create mode 100644 vendor/plugins/liquid/test/parsing_quirks_test.rb create mode 100644 vendor/plugins/liquid/test/regexp_test.rb create mode 100644 vendor/plugins/liquid/test/security_test.rb create mode 100644 vendor/plugins/liquid/test/standard_filter_test.rb create mode 100644 vendor/plugins/liquid/test/standard_tag_test.rb create mode 100644 vendor/plugins/liquid/test/statements_test.rb create mode 100644 vendor/plugins/liquid/test/strainer_test.rb create mode 100644 vendor/plugins/liquid/test/template_test.rb create mode 100644 vendor/plugins/liquid/test/test_helper.rb create mode 100644 vendor/plugins/liquid/test/unless_else_test.rb create mode 100644 vendor/plugins/liquid/test/variable_test.rb create mode 100644 vendor/plugins/ym4r_gm/README create mode 100644 vendor/plugins/ym4r_gm/gmaps_api_key.yml.sample create mode 100644 vendor/plugins/ym4r_gm/init.rb create mode 100644 vendor/plugins/ym4r_gm/install.rb create mode 100644 vendor/plugins/ym4r_gm/javascript/clusterer.js create mode 100644 vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js create mode 100644 vendor/plugins/ym4r_gm/javascript/markerGroup.js create mode 100644 vendor/plugins/ym4r_gm/javascript/wms-gs.js create mode 100644 vendor/plugins/ym4r_gm/javascript/ym4r-gm.js create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/control.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/geocoding.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb create mode 100644 vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb create mode 100644 vendor/plugins/ym4r_gm/lib/ym4r_gm.rb create mode 100644 vendor/plugins/ym4r_gm/rakefile.rb create mode 100644 vendor/plugins/ym4r_gm/tasks/gm_tasks.rake create mode 100644 vendor/plugins/ym4r_gm/test/gm_test.rb diff --git a/app/apis/backend_api.rb b/app/apis/backend_api.rb new file mode 100644 index 0000000..e3efe37 --- /dev/null +++ b/app/apis/backend_api.rb @@ -0,0 +1,2 @@ +class BackendApi < ActionWebService::API::Base +end diff --git a/app/apis/blogger_api.rb b/app/apis/blogger_api.rb new file mode 100644 index 0000000..590069f --- /dev/null +++ b/app/apis/blogger_api.rb @@ -0,0 +1,36 @@ +class BloggerAPI < ActionWebService::API::Base + inflect_names false + + api_method :getUsersBlogs, + :expects => [ {:appkey => :string}, {:username => :string}, + {:password => :string} ], + :returns => [[BloggerStructs::Blog]] + + api_method :getUserInfo, + :expects => [ {:appkey => :string}, {:username => :string}, + {:password => :string} ], + :returns => [BloggerStructs::User] + + api_method :getPost, + :expects => [ {:appkey => :string}, {:postid => :string}, + {:username => :string}, {:password => :string} ], + :returns => [BloggerStructs::Post] + + api_method :getRecentPosts, + :expects => [ {:appkey => :string}, {:blogid => :string}, + {:username => :string}, {:password => :string}, + {:numberOfPosts => :integer} ], + :returns => [[BloggerStructs::Post]] + + api_method :newPost, + :expects => [ {:appkey => :string}, {:blogid => :string}, + {:username => :string}, {:password => :string}, + {:content => :string}, {:publish => :boolean} ], + :returns => [:int] + + api_method :editPost, + :expects => [ {:appkey => :string}, {:postid => :string}, + {:username => :string}, {:password => :string}, + {:content => :string}, {:publish => :boolean} ], + :returns => [:boolean] +end diff --git a/app/apis/blogger_structs/blog.rb b/app/apis/blogger_structs/blog.rb new file mode 100644 index 0000000..c954ac3 --- /dev/null +++ b/app/apis/blogger_structs/blog.rb @@ -0,0 +1,7 @@ +module BloggerStructs + class Blog < ActionWebService::Struct + member :url, :string + member :blogId, :string + member :blogName, :string + end +end diff --git a/app/apis/blogger_structs/post.rb b/app/apis/blogger_structs/post.rb new file mode 100644 index 0000000..b88619e --- /dev/null +++ b/app/apis/blogger_structs/post.rb @@ -0,0 +1,8 @@ +module BloggerStructs + class Post < ActionWebService::Struct + member :userId, :string + member :postId, :string + member :dateCreated, :string + member :content, :string + end +end diff --git a/app/apis/blogger_structs/user.rb b/app/apis/blogger_structs/user.rb new file mode 100644 index 0000000..79beb67 --- /dev/null +++ b/app/apis/blogger_structs/user.rb @@ -0,0 +1,8 @@ +module BloggerStructs + class User < ActionWebService::Struct + member :userId, :string + member :username, :string + member :email, :string + member :url, :string + end +end diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb new file mode 100644 index 0000000..540b138 --- /dev/null +++ b/app/controllers/account_controller.rb @@ -0,0 +1,22 @@ +class AccountController < ApplicationController + + def authenticate + self.logged_in_user = User.authenticate(params[:user][:username], + params[:user][:password]) + if is_logged_in? + redirect_to index_url + else + flash[:error] = "I'm sorry, either your username or password was incorrect." + redirect_to :action => 'login' + end + end + + def logout + if request.post? + reset_session + flash[:notice] = "You have been logged out." + end + redirect_to index_url + end + +end diff --git a/app/controllers/application.rb b/app/controllers/application.rb new file mode 100644 index 0000000..6c3a5f6 --- /dev/null +++ b/app/controllers/application.rb @@ -0,0 +1,8 @@ +# Filters added to this controller apply to all controllers in the application. +# Likewise, all the methods added will be available for all controllers. + +class ApplicationController < ActionController::Base + # Pick a unique cookie name to distinguish our session data from others' + session :session_key => '_railscoders_session_id' + include LoginSystem +end diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb new file mode 100644 index 0000000..d91daf5 --- /dev/null +++ b/app/controllers/articles_controller.rb @@ -0,0 +1,76 @@ +class ArticlesController < ApplicationController + before_filter :check_editor_role, :except => [:index, :show] + + def index + if params[:category_id] + @articles_pages, @articles = paginate(:articles, + :include => :user, + :order => 'published_at DESC', + :conditions => "category_id=#{params[:category_id].to_i} AND published=true") + else + @articles = Article.find_all_by_published(true) + @articles_pages, @articles = paginate(:articles, + :include => :user, + :order => 'published_at DESC', + :conditions => "published = true") + end + respond_to do |wants| + wants.html + wants.xml { render :xml => @articles.to_xml } + wants.rss { render :action => 'rss.rxml', :layout => false } + wants.atom { render :action => 'atom.rxml', :layout => false } + end + end + + def show + if is_logged_in? && @logged_in_user.has_role?('Editor') + @article = Article.find(params[:id]) + else + @article = Article.find_by_id_and_published(params[:id], true) + end + respond_to do |wants| + wants.html + wants.xml { render :xml => @article.to_xml } + end + end + + def new + @article = Article.new + end + + def create + @article = Article.create(params[:article]) + @logged_in_user.articles << @article + respond_to do |wants| + wants.html { redirect_to admin_articles_url } + wants.xml { render :xml => @article.to_xml } + end + end + + def edit + @article = Article.find(params[:id]) + end + + def update + @article = Article.find(params[:id]) + @article.update_attributes(params[:article]) + respond_to do |wants| + wants.html { redirect_to admin_articles_url } + wants.xml { render :xml => @article.to_xml } + end + end + + def destroy + @article = Article.find(params[:id]) + @article.destroy + respond_to do |wants| + wants.html { redirect_to admin_articles_url } + wants.xml { render :nothing => true } + end + end + + def admin + @articles_pages, @articles = paginate(:articles, :order => 'published_at DESC') + end + +end diff --git a/app/controllers/backend_controller.rb b/app/controllers/backend_controller.rb new file mode 100644 index 0000000..1b64dba --- /dev/null +++ b/app/controllers/backend_controller.rb @@ -0,0 +1,5 @@ +class BackendController < ApplicationController + web_service_scaffold 'invoke' + web_service_dispatching_mode :layered + web_service :blogger, BloggerService.new +end diff --git a/app/controllers/blogger_service.rb b/app/controllers/blogger_service.rb new file mode 100644 index 0000000..055766f --- /dev/null +++ b/app/controllers/blogger_service.rb @@ -0,0 +1,70 @@ +class BloggerService < ActionWebService::Base + web_service_api BloggerAPI + + def getUsersBlogs(appkey, username, password) + if @user = User.authenticate(username, password) + [BloggerStructs::Blog.new( + :url => "http://localhost:3000/users/#{@user.id}/entries", + :blogId => @user.id, + :blogName => @user.blog_title ||= @user.username + )] + end + end + + def getPost(appkey, postid, username, password) + if @user = User.authenticate(username, password) + entry = @user.entries.find(postid) + BloggerStructs::Post.new( + :userId => @user.id, + :postId => entry.id, + :dateCreated => entry.created_at.to_s(:db), + :content => [entry.body] + ) + end + end + + def getRecentPosts(appkey, blogid, username, password, numberofposts) + if @user = User.authenticate(username, password) + @user.entries.find(:all, + :order => 'created_at DESC', + :limit => numberofposts).collect do |entry| + BloggerStructs::Post.new( + :userId => entry.user_id, + :postId => entry.id, + :dateCreated => entry.created_at.to_s(:db), + :content => entry.body + ) + end + end + end + + def getUserInfo(appkey, username, password) + if @user = User.authenticate(username, password) + BloggerStructs::User.new( + :userId => @user.id, + :username => @user.username, + :url => "http://localhost:3000/users/#{@user.id}/entries" + ) + end + end + + def newPost(appkey, blogid, username, password, content, publish) + if @user = User.authenticate(username, password) + entry = Entry.new + entry.title = "New entry" + entry.body = content.to_s + entry.user = @user + entry.save + return entry.id + end + end + + def editPost(appkey, postid, username, password, content, publish) + if @user = User.authenticate(username, password) + entry = @user.entries.find(postid) + entry.body = content + entry.save + return true + end + end +end diff --git a/app/controllers/blogs_controller.rb b/app/controllers/blogs_controller.rb new file mode 100644 index 0000000..9bdf47f --- /dev/null +++ b/app/controllers/blogs_controller.rb @@ -0,0 +1,10 @@ +class BlogsController < ApplicationController + def index + @entry_pages = Paginator.new(self, Entry.count, 10, params[:page]) + @entries = Entry.find(:all, + :limit => @entry_pages.items_per_page, + :offset => @entry_pages.current.offset, + :order => 'entries.created_at DESC', + :include => :user) + end +end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb new file mode 100644 index 0000000..2aaf954 --- /dev/null +++ b/app/controllers/categories_controller.rb @@ -0,0 +1,61 @@ +class CategoriesController < ApplicationController + before_filter :check_editor_role, :except => [:index, :show] + + def index + @categories = Category.find(:all) + respond_to do |wants| + wants.html + wants.xml { render :xml => @categories.to_xml } + end + end + + def show + @category = Category.find(params[:id]) + respond_to do |wants| + wants.html { redirect_to category_articles_url(:category_id => @category.id) } + wants.xml { render :xml => @category.to_xml } + end + end + + def new + @category = Category.new + end + + def create + @category = Category.create(params[:category]) + respond_to do |wants| + wants.html { redirect_to admin_categories_url } + wants.xml { render :xml => @category.to_xml } + end + end + + def edit + @category = Category.find(params[:id]) + end + + def update + @category = Category.find(params[:id]) + @category.update_attributes(params[:category]) + respond_to do |wants| + wants.html { redirect_to admin_categories_url } + wants.xml { render :xml => @category.to_xml } + end + end + + def destroy + @category = Category.find(params[:id]) + @category.find(params[:id]).destroy + respond_to do |wants| + wants.html { redirect_to admin_categories_url } + wants.xml { render :nothing => true } + end + end + + def admin + @categories = Category.find(:all) + respond_to do |wants| + wants.html + wants.xml { render :xml => @categories.to_xml } + end + end +end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 0000000..222fc1a --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,24 @@ +class CommentsController < ApplicationController + before_filter :login_required + + def create + @entry = Entry.find_by_user_id_and_id(params[:user_id], params[:entry_id]) + @comment = Comment.new(:user_id => @logged_in_user.id, :body => params[:comment][:body]) + + if @entry.comments << @comment + flash[:notice] = 'Comment was successfully created.' + Notifier.deliver_new_comment_notification(@comment) + redirect_to entry_path(:user_id => @entry.user, :id => @entry) + else + render :controller => 'entries', :action => 'show', :user_id => @entry.user, :id => @entry + end + end + + def destroy + @entry = Entry.find_by_user_id_and_id(@logged_in_user.id, params[:entry_id], :include => :user) + @comment = @entry.comments.find(params[:id]) + @comment.destroy + + redirect_to entry_path(:user_id => @entry.user.id, :id => @entry.id) + end +end diff --git a/app/controllers/entries_controller.rb b/app/controllers/entries_controller.rb new file mode 100644 index 0000000..32e990f --- /dev/null +++ b/app/controllers/entries_controller.rb @@ -0,0 +1,73 @@ +class EntriesController < ApplicationController + before_filter :login_required, :except => [:index, :show] + + def index + @user = User.find(params[:user_id]) + @entry_pages = Paginator.new(self, @user.entries_count, 10, params[:page]) + @entries = @user.entries.find(:all, :order => 'created_at DESC', + :limit => @entry_pages.items_per_page, + :offset => @entry_pages.current.offset) + + @usertemplate = @user.usertemplates.find_by_name('blog_index') + if @usertemplate and @usertemplate.body.any? + @page = Liquid::Template.parse(@usertemplate.body) + render :text => @page.render({'user' => @user, 'entries' => @entries}, [TextFilters]) + end + end + + def show + @user = User.find(params[:user_id], :include => :usertemplates) + @entry = Entry.find_by_id_and_user_id(params[:id], + params[:user_id], + :include => [:user, [:comments => :user]]) + + @usertemplate = @user.usertemplates.find_by_name('blog_entry') + if @usertemplate and @usertemplate.body.any? + @page = Liquid::Template.parse(@usertemplate.body) + render :text => @page.render({'user' => @user, 'entry' => @entry, 'comments' => @entry.comments}, [TextFilters]) + end + end + + def new + @entry = Entry.new + end + + def edit + @entry = @logged_in_user.entries.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def create + @entry = Entry.new(params[:entry]) + + if logged_in_user.entries << @entry + flash[:notice] = 'Entry was successfully created.' + redirect_to entry_path(:user_id => logged_in_user, + :id => @entry) + else + render :action => "new" + end + end + + def update + @entry = @logged_in_user.entries.find(params[:id]) + + if @entry.update_attributes(params[:entry]) + flash[:notice] = 'Entry was successfully updated.' + redirect_to entry_path(logged_in_user.id, @entry) + #render :action => "edit" + end + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def destroy + @entry = @logged_in_user.entries.find(params[:id]) + @entry.destroy + + redirect_to entries_path + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end +end diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb new file mode 100644 index 0000000..e5e8493 --- /dev/null +++ b/app/controllers/forums_controller.rb @@ -0,0 +1,76 @@ +class ForumsController < ApplicationController + before_filter :check_moderator_role, :except => [:index, :show] + + # GET /forums + # GET /forums.xml + def index + @forums = Forum.find(:all) + + respond_to do |format| + format.html # index.rhtml + format.xml { render :xml => @forums.to_xml } + end + end + + # GET /forums/1 + # GET /forums/1.xml + def show + redirect_to topics_path(:forum_id => params[:id]) + end + + # GET /forums/new + def new + @forum = Forum.new + end + + # GET /forums/1;edit + def edit + @forum = Forum.find(params[:id]) + end + + # POST /forums + # POST /forums.xml + def create + @forum = Forum.new(params[:forum]) + + respond_to do |format| + if @forum.save + flash[:notice] = 'Forum was successfully created.' + format.html { redirect_to forums_path } + format.xml { head :created, :location => forum_path(@forum) } + else + format.html { render :action => "new" } + format.xml { render :xml => @forum.errors.to_xml } + end + end + end + + # PUT /forums/1 + # PUT /forums/1.xml + def update + @forum = Forum.find(params[:id]) + + respond_to do |format| + if @forum.update_attributes(params[:forum]) + flash[:notice] = 'Forum was successfully updated.' + format.html { redirect_to forum_path(@forum) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @forum.errors.to_xml } + end + end + end + + # DELETE /forums/1 + # DELETE /forums/1.xml + def destroy + @forum = Forum.find(params[:id]) + @forum.destroy + + respond_to do |format| + format.html { redirect_to forums_path } + format.xml { head :ok } + end + end +end diff --git a/app/controllers/friends_controller.rb b/app/controllers/friends_controller.rb new file mode 100644 index 0000000..9a4e786 --- /dev/null +++ b/app/controllers/friends_controller.rb @@ -0,0 +1,54 @@ +class FriendsController < ApplicationController + before_filter :login_required, :except => [:index, :show] + + def index + @user = User.find(params[:user_id], :include => [:friendships => :friend]) + end + + def show + redirect_to user_path(params[:id]) + end + + def new + @user = User.find(logged_in_user) + @friend = User.find(params[:friend_id]) + unless @user.friends.include?(@friend) + @friendship = @user.friendships.new(:friend_id => @friend.id) + else + redirect_to friend_path(:user_id => logged_in_user, :id => @friend) + end + end + + def edit + @user = User.find(logged_in_user) + @friendship = @user.friendships.find_by_friend_id(params[:id]) + @friend = @friendship.friend if @friendship + if !@friendship + redirect_to friend_path(:user_id => logged_in_user, :id => params[:id]) + end + end + + def create + @user = User.find(logged_in_user) + params[:friendship][:friend_id] = params[:friend_id] + @friendship = @user.friendships.create(params[:friendship]) + redirect_to friends_path(:user_id => logged_in_user) + rescue ActiveRecord::RecordInvalid + render :action => 'new' + end + + def update + @user = User.find(logged_in_user) + @friendship = @user.friendships.find_by_friend_id(params[:id]) + @friendship.update_attributes(params[:friendship]) + redirect_to friends_path(:user_id => logged_in_user) + rescue ActiveRecord::RecordInvalid + render :action => 'edit' + end + + def destroy + @user = User.find(params[:user_id]) + @friendship = @user.friendships.find_by_friend_id(params[:id]).destroy + redirect_to friends_path(:user_id => logged_in_user) + end +end diff --git a/app/controllers/mobile/account_controller.rb b/app/controllers/mobile/account_controller.rb new file mode 100644 index 0000000..ef36728 --- /dev/null +++ b/app/controllers/mobile/account_controller.rb @@ -0,0 +1,20 @@ +class Mobile::AccountController < AccountController + layout 'mobile' + + def authenticate + self.logged_in_user = User.authenticate(params[:user][:username], params[:user][:password]) + if is_logged_in? + flash[:notice] = "You have successfully logged in." + redirect_to mobile_index_url + else + flash[:error] = "I'm sorry; either your email or password was incorrect." + redirect_to :action => 'login' + end + end + + def logout + reset_session + flash[:notice] = "You have been logged out." + redirect_to mobile_index_url + end +end diff --git a/app/controllers/mobile/articles_controller.rb b/app/controllers/mobile/articles_controller.rb new file mode 100644 index 0000000..4dabbb6 --- /dev/null +++ b/app/controllers/mobile/articles_controller.rb @@ -0,0 +1,19 @@ +class Mobile::ArticlesController < ArticlesController + layout 'mobile' + + def index + if params[:category_id] + @articles_pages, @articles = paginate :articles, + :include => :user, + :per_page => 5, + :order => 'published_at DESC', + :conditions => "category_id = #{params[:category_id].to_i} AND published = true" + else + @articles_pages, @articles = paginate :articles, + :include => :user, + :per_page => 5, + :order => 'published_at DESC', + :conditions => "published = true" + end + end +end \ No newline at end of file diff --git a/app/controllers/mobile/blogs_controller.rb b/app/controllers/mobile/blogs_controller.rb new file mode 100644 index 0000000..2baa69a --- /dev/null +++ b/app/controllers/mobile/blogs_controller.rb @@ -0,0 +1,3 @@ +class Mobile::BlogsController < BlogsController + layout 'mobile' +end \ No newline at end of file diff --git a/app/controllers/mobile/entries_controller.rb b/app/controllers/mobile/entries_controller.rb new file mode 100644 index 0000000..f8fe3ea --- /dev/null +++ b/app/controllers/mobile/entries_controller.rb @@ -0,0 +1,19 @@ +class Mobile::EntriesController < EntriesController + layout 'mobile' + + def index + @user = User.find(params[:user_id], :include => :usertemplates) + @entry_pages = Paginator.new(self, @user.entries_count, 5, params[:page]) + @entries = @user.entries.find(:all, :order => 'created_at DESC', + :limit => @entry_pages.items_per_page, + :offset => @entry_pages.current.offset) + end + + + def show + @user = User.find(params[:user_id], :include => :usertemplates) + @entry = Entry.find_by_id_and_user_id(params[:id], + params[:user_id], + :include => [:comments => :user]) + end +end \ No newline at end of file diff --git a/app/controllers/mobile/forums_controller.rb b/app/controllers/mobile/forums_controller.rb new file mode 100644 index 0000000..1dc3a57 --- /dev/null +++ b/app/controllers/mobile/forums_controller.rb @@ -0,0 +1,8 @@ +class Mobile::ForumsController < ForumsController + layout 'mobile' + + def show + redirect_to mobile_topics_path(:forum_id => params[:forum_id]) + end + +end \ No newline at end of file diff --git a/app/controllers/mobile/pages_controller.rb b/app/controllers/mobile/pages_controller.rb new file mode 100644 index 0000000..a73ea3c --- /dev/null +++ b/app/controllers/mobile/pages_controller.rb @@ -0,0 +1,3 @@ +class Mobile::PagesController < PagesController + layout 'mobile' +end \ No newline at end of file diff --git a/app/controllers/mobile/photos_controller.rb b/app/controllers/mobile/photos_controller.rb new file mode 100644 index 0000000..cd08e8a --- /dev/null +++ b/app/controllers/mobile/photos_controller.rb @@ -0,0 +1,3 @@ +class Mobile::PhotosController < PhotosController + layout 'mobile' +end \ No newline at end of file diff --git a/app/controllers/mobile/posts_controller.rb b/app/controllers/mobile/posts_controller.rb new file mode 100644 index 0000000..aa66053 --- /dev/null +++ b/app/controllers/mobile/posts_controller.rb @@ -0,0 +1,17 @@ +class Mobile::PostsController < PostsController + layout 'mobile' + + def create + @topic = Topic.find(params[:topic_id]) + @post = Post.new(:body => params[:post][:body], + :topic_id => @topic.id, + :user_id => logged_in_user.id) + + if @post.save + flash[:notice] = 'Post was successfully created.' + redirect_to mobile_posts_path(:forum_id => @topic.forum_id, :topic_id => @topic) + else + render :action => "new" + end + end +end \ No newline at end of file diff --git a/app/controllers/mobile/topics_controller.rb b/app/controllers/mobile/topics_controller.rb new file mode 100644 index 0000000..a91a7ca --- /dev/null +++ b/app/controllers/mobile/topics_controller.rb @@ -0,0 +1,21 @@ +class Mobile::TopicsController < TopicsController + layout 'mobile' + + def show + redirect_to mobile_posts_path(:forum_id => params[:forum_id], + :topic_id => params[:id]) + end + + def create + @topic = Topic.new(:name => params[:topic][:name], + :forum_id => params[:forum_id], + :user_id => logged_in_user.id) + @topic.save! + @post = Post.new(:body => params[:post][:body], + :topic_id => @topic.id, + :user_id => logged_in_user.id) + @post.save! + + redirect_to mobile_posts_path(:topic_id => @topic, :forum_id => @topic.forum) + end +end \ No newline at end of file diff --git a/app/controllers/mobile/user_photos_controller.rb b/app/controllers/mobile/user_photos_controller.rb new file mode 100644 index 0000000..28b78bd --- /dev/null +++ b/app/controllers/mobile/user_photos_controller.rb @@ -0,0 +1,3 @@ +class Mobile::UserPhotosController < UserPhotosController + layout 'mobile' +end \ No newline at end of file diff --git a/app/controllers/mobile/users_controller.rb b/app/controllers/mobile/users_controller.rb new file mode 100644 index 0000000..834c1cd --- /dev/null +++ b/app/controllers/mobile/users_controller.rb @@ -0,0 +1,3 @@ +class Mobile::UsersController < UsersController + layout 'mobile' +end \ No newline at end of file diff --git a/app/controllers/newsletters_controller.rb b/app/controllers/newsletters_controller.rb new file mode 100644 index 0000000..b2f6a97 --- /dev/null +++ b/app/controllers/newsletters_controller.rb @@ -0,0 +1,67 @@ +class NewslettersController < ApplicationController + before_filter :check_administrator_role + + # GET /newsletters + def index + @newsletters = Newsletter.find(:all) + end + + # GET /newsletters/1 + def show + @newsletter = Newsletter.find(params[:id]) + end + + # GET /newsletters/new + def new + @newsletter = Newsletter.new + end + + # GET /newsletters/1;edit + def edit + @newsletter = Newsletter.find_by_id_and_sent(params[:id], false) + end + + # POST /newsletters + def create + @newsletter = Newsletter.new(params[:newsletter]) + + if @newsletter.save + flash[:notice] = 'Newsletter was successfully created.' + redirect_to newsletter_path(@newsletter) + else + render :action => "new" + end + end + + # PUT /newsletters/1 + def update + @newsletter = Newsletter.find_by_id_and_sent(params[:id], false) + + if @newsletter.update_attributes(params[:newsletter]) + flash[:notice] = 'Newsletter was successfully updated.' + redirect_to newsletter_path(@newsletter) + else + render :action => "edit" + end + end + + # DELETE /newsletters/1 + def destroy + @newsletter = Newsletter.find_by_id_and_sent(params[:id], false) + @newsletter.destroy + + redirect_to newsletters_path + end + + # PUT /newsletters/1;send + def sendmails + newsletter = Newsletter.find_by_id_and_sent(params[:id], false) + users = User.find(:all) + users.each do |user| + Notifier.deliver_newsletter(user, @newsletter) + end + newsletter.update_attribute('sent', true) + redirect_to newsletters_path + end + +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 0000000..7831408 --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,48 @@ +class PagesController < ApplicationController + before_filter :check_administrator_role, :except => :show + + def index + @pages = Page.find(:all) + end + + def show + @page = Page.find(params[:id].to_i) + end + + def new + @page = Page.new + end + + def create + @page = Page.new(params[:page]) + @page.save! + flash[:notice] = 'Page saved' + redirect_to :action => 'index' + rescue ActiveRecord::RecordInvalid + render :action => 'new' + end + + def edit + @page = Page.find(params[:id].to_i) + end + + def update + @page = Page.find(params[:id].to_i) + @page.attributes = params[:page] + @page.save! + flash[:notice] = "Page updated" + redirect_to :action => 'index' + rescue + render :action => 'edit' + end + + def destroy + @page = Page.find(params[:id].to_i) + if @page.destroy + flash[:notice] = "Page deleted" + else + flash[:error] = "There was a problem deleting the page" + end + redirect_to :action => 'index' + end +end diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb new file mode 100644 index 0000000..2ccb5b4 --- /dev/null +++ b/app/controllers/photos_controller.rb @@ -0,0 +1,11 @@ +class PhotosController < ApplicationController + def index + photos_count = Photo.count(:conditions => 'thumbnail IS NULL') + @photo_pages = Paginator.new(self, photos_count, 9, params[:page]) + @photos = Photo.find(:all, + :conditions => 'thumbnail IS NULL', + :order => 'created_at DESC', + :limit => @photo_pages.items_per_page, + :offset => @photo_pages.current.offset) + end +end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb new file mode 100644 index 0000000..12736a9 --- /dev/null +++ b/app/controllers/posts_controller.rb @@ -0,0 +1,97 @@ +class PostsController < ApplicationController + # before_filter :check_moderator_role, :only => [:destroy, :edit, :update] + before_filter :check_moderator_role, :only => [:destroy] + before_filter :login_required, :except => [:index, :show] + + # GET /posts + # GET /posts.xml + def index + @topic = Topic.find(params[:topic_id], :include => :forum) + @posts_pages, @posts = paginate(:posts, + :include => :user, + :conditions => ['topic_id = ?', @topic]) + + respond_to do |format| + format.html # index.rhtml + format.xml { render :xml => @posts.to_xml } + end + end + + # GET /posts/1 + # GET /posts/1.xml + def show + @post = Post.find(params[:id]) + + respond_to do |format| + format.html # show.rhtml + format.xml { render :xml => @post.to_xml } + end + end + + # GET /posts/new + def new + @topic = Topic.find(params[:topic_id], :include => :forum) + @post = Post.new + end + + # GET /posts/1;edit + def edit + @post = @logged_in_user.posts.find(params[:id], :include => { :topic => :forum }) + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + # POST /posts + # POST /posts.xml + def create + @topic = Topic.find(params[:topic_id]) + @post = Post.new(:body => params[:post][:body], + :topic_id => @topic.id, + :user_id => logged_in_user.id) + + respond_to do |format| + if @post.save + flash[:notice] = 'Post was successfully created.' + format.html { redirect_to posts_path(:forum_id => @topic.forum_id, + :topic_id => @topic) } + format.xml { head :created, :location => post_path(@post) } + else + format.html { render :action => "new" } + format.xml { render :xml => @post.errors.to_xml } + end + end + end + + # PUT /posts/1 + # PUT /posts/1.xml + def update + @post = @logged_in_user.posts.find(params[:id]) + + respond_to do |format| + if @post.update_attributes(params[:post]) + flash[:notice] = 'Post was successfully updated.' + format.html { redirect_to posts_path(:forum_id => params[:forum_id], + :topic_id => params[:topic_id]) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @post.errors.to_xml } + end + end + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + # DELETE /posts/1 + # DELETE /posts/1.xml + def destroy + @post = Post.find(params[:id]) + @post.destroy + + respond_to do |format| + format.html { redirect_to posts_path(:forum_id => params[:forum_id], + :topic_id => params[:topic_id]) } + format.xml { head :ok } + end + end +end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb new file mode 100644 index 0000000..b2f3bc7 --- /dev/null +++ b/app/controllers/roles_controller.rb @@ -0,0 +1,27 @@ +class RolesController < ApplicationController + before_filter :check_administrator_role + + def index + @user = User.find(params[:user_id]) + @all_roles = Role.find(:all) + end + + def update + @user = User.find(params[:user_id]) + @role = Role.find(params[:id]) + unless @user.has_role?(@role.name) + @user.roles << @role + end + redirect_to :action => 'index' + end + + def destroy + @user = User.find(params[:user_id]) + @role = Role.find(params[:id]) + if @user.has_role?(@role.name) + @user.roles.delete(@role) + end + redirect_to :action => 'index' + end + +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 0000000..1a7d1bf --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,9 @@ +class TagsController < ApplicationController + def index + @tags = Photo.tag_counts(:order => 'name') + end + + def show + @photos = Photo.find_tagged_with(params[:id]) + end +end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb new file mode 100644 index 0000000..2ae058a --- /dev/null +++ b/app/controllers/topics_controller.rb @@ -0,0 +1,90 @@ +class TopicsController < ApplicationController + before_filter :check_moderator_role, :only => [:destroy, :edit, :update] + before_filter :login_required, :except => [:index, :show] + + # GET /topics + # GET /topics.xml + def index + @forum = Forum.find(params[:forum_id]) + @topics_pages, @topics = paginate(:topics, + :include => :user, + :conditions => ['forum_id = ?', @forum], + :order => 'topics.updated_at DESC') + + respond_to do |format| + format.html # index.rhtml + format.xml { render :xml => @topics.to_xml } + end + end + + # GET /topics/1 + # GET /topics/1.xml + def show + redirect_to posts_path(:forum_id => params[:forum_id], :topic_id => params[:id]) + end + + # GET /topics/new + def new + @topic = Topic.new + @post = Post.new + end + + # GET /topics/1;edit + def edit + @topic = Topic.find(params[:id]) + end + + def create + @topic = Topic.new(:name => params[:topic][:name], + :forum_id => params[:forum_id], + :user_id => logged_in_user.id) + @topic.save! + @post = Post.new(:body => params[:post][:body], + :topic_id => @topic.id, + :user_id => logged_in_user.id) + @post.save! + respond_to do |format| + format.html { redirect_to posts_path(:topic_id => @topic, + :forum_id => @topic.forum.id) } + format.xml { head :created, :location => topic_path(:id => @topic, + :forum_id => @topic.forum.id) } + end + rescue ActiveRecord::RecordInvalid + respond_to do |format| + format.html { render :action => 'new' } + format.xml { render :xml => @post.errors.to_xml } + end + end + + # PUT /topics/1 + # PUT /topics/1.xml + def update + @topic = Topic.find(params[:id]) + + respond_to do |format| + if @topic.update_attributes(params[:topic]) + flash[:notice] = 'Topic was successfully updated.' + format.html { redirect_to posts_path(:topic_id => @topic, + :forum_id => @topic.forum) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @topic.errors.to_xml } + end + end + end + + # DELETE /topics/1 + # DELETE /topics/1.xml + def destroy + @topic = Topic.find(params[:id]) + @topic.posts.each { |post| post.destroy } + @topic.destroy + + respond_to do |format| + format.html { redirect_to topics_path(:forum_id => + params[:forum_id]) } + format.xml { head :ok } + end + end +end diff --git a/app/controllers/user_photos_controller.rb b/app/controllers/user_photos_controller.rb new file mode 100644 index 0000000..91d87a1 --- /dev/null +++ b/app/controllers/user_photos_controller.rb @@ -0,0 +1,138 @@ +class UserPhotosController < ApplicationController + before_filter :login_required, :except => [:index, :index_all, :show] + + def index + @user = User.find(params[:user_id]) + @photo_pages = Paginator.new(self, @user.photos.count, 9, params[:page]) + @photos = @user.photos.find(:all, :order => 'created_at DESC', + :limit => @photo_pages.items_per_page, + :offset => @photo_pages.current.offset) + respond_to do |format| + format.html # index.rhtml + format.xml { render :xml => @photos.to_xml } + end + end + + def show + @photo = Photo.find_by_user_id_and_id(params[:user_id], + params[:id], + :include => :user) + + if @photo.show_geo && (@photo.geo_lat && @photo.geo_long) + @map = GMap.new("map_div_id") + @map.control_init(:map_type => false, :small_zoom => true) + @map.center_zoom_init([@photo.geo_lat, @photo.geo_long], 8) + + marker = GMarker.new([@photo.geo_lat, @photo.geo_long], + :title => @photo.title, + :info_window => @photo.body) + @map.overlay_init(marker) + end + + respond_to do |format| + format.html # show.rhtml + format.xml { render :xml => @photo.to_xml } + end + end + + def new + @photo = Photo.new + + @map = GMap.new("map_div_id") + @map.control_init(:large_map => true) + @map.center_zoom_init([25,0], 1) + @map.record_init @map.on_click("function (overlay, point) { updateLocation(point); }") + end + + def edit + @photo = @logged_in_user.photos.find(params[:id]) + + @map = GMap.new("map_div_id") + @map.control_init(:large_map => true) + + if @photo.geo_lat && @photo.geo_long + @map.center_zoom_init([@photo.geo_lat, @photo.geo_long], 8) + + marker = GMarker.new([@photo.geo_lat, @photo.geo_long], + :title => @photo.title, :info_window => @photo.body) + @map.overlay_init(marker) + else + @map.center_zoom_init([25,0], 1) + end + + @map.record_init @map.on_click( + "function (overlay, point) { updateLocation(point); }") + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def create + @photo = Photo.new(params[:photo]) + + respond_to do |format| + if @logged_in_user.photos << @photo + flash[:notice] = 'Photo was successfully created.' + format.html { redirect_to(user_photos_path(:user_id=>@logged_in_user.id)) } + format.xml { head :created, + :location => user_photo_path(:user_id => @photo.user_id, :id => @photo)} + else + format.html { render :action => 'new' } + format.xml { render :xml => @photo.errors.to_xml } + end + end + rescue ActiveRecord::RecordInvalid + render :action => 'new' + end + + def update + @photo = @logged_in_user.photos.find(params[:id]) + + respond_to do |format| + if @photo.update_attributes(params[:photo]) + flash[:notice] = 'Photo was successfully updated.' + format.html { redirect_to user_photo_path(:user_id => @logged_in_user, + :id => @photo) } + format.xml { head :ok } + else + format.html { render :action => "edit" } + format.xml { render :xml => @photo.errors.to_xml } + end + end + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def destroy + @photo = @logged_in_user.photos.find(params[:id]) + @photo.destroy + + respond_to do |format| + format.html { redirect_to user_photos_path } + format.xml { head :ok } + end + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def add_tag + @photo = @logged_in_user.photos.find(params[:id]) + # changed to reflect latest version of acts_as_taggable_on_steroids + @photo.tag_list.names << params[:tag][:name] + if @photo.save + @new_tag = @photo.reload.tags.find_by_name params[:tag][:name] + else + render :nothing => true + end + end + + def remove_tag + @photo = @logged_in_user.photos.find(params[:id]) + @tag_to_delete = @photo.tags.find(params[:tag_id]) + if @tag_to_delete + @photo.tags.delete(@tag_to_delete) + else + render :nothing => true + end + end + +end diff --git a/app/controllers/user_tags_controller.rb b/app/controllers/user_tags_controller.rb new file mode 100644 index 0000000..c072cc2 --- /dev/null +++ b/app/controllers/user_tags_controller.rb @@ -0,0 +1,11 @@ +class UserTagsController < ApplicationController + def index + @user = User.find(params[:user_id]) + @tags = @user.photos.tag_counts(:order => 'name') + end + + def show + @user = User.find(params[:user_id]) + @photos = @user.photos.find_tagged_with(params[:id]) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..00c656b --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,72 @@ +class UsersController < ApplicationController + before_filter :check_administrator_role, :only => [:index, :destroy, :enable] + before_filter :login_required, :only => [:edit, :update] + + def index + @users = User.find(:all) + end + + def show + @user = User.find(params[:id]) + @entries = @user.entries.find(:all, :limit => 3, :order => 'created_at DESC') + @photos = @user.photos.find(:all, :limit => 3, :order => 'created_at DESC') + @flickr_feed = @user.flickr_feed if @user.flickr_id + end + + def show_by_username + @user = User.find_by_username(params[:username]) + @entries = @user.entries.find(:all, :limit => 3, :order => 'created_at DESC') + @photos = @user.photos.find(:all, :limit => 3, :order => 'created_at DESC') + @flickr_feed = @user.flickr_feed if @user.flickr_id + render :action => 'show' + end + + def new + @user = User.new + end + + def create + @user = User.new(params[:user]) + if @user.save + self.logged_in_user = @user + flash[:notice] = "Your account has been created." + redirect_to index_url + else + render :action => 'new' + end + end + + def edit + @user = logged_in_user + end + + def update + @user = User.find(params[:id]) + if @user.update_attributes(params[:user]) + flash[:notice] = "User updated" + redirect_to :action => 'index' + else + render :action => 'edit' + end + end + + def destroy + @user = User.find(params[:id]) + if @user.update_attribute(:enabled, false) + flash[:notice] = "User disabled" + else + flash[:error] = "There was a problem disabling this user." + end + redirect_to :action => 'index' + end + + def enable + @user = User.find(params[:id]) + if @user.update_attribute(:enabled, true) + flash[:notice] = "User enabled" + else + flash[:error] = "There was a problem enabling this user." + end + redirect_to :action => 'index' + end +end diff --git a/app/controllers/usertemplates_controller.rb b/app/controllers/usertemplates_controller.rb new file mode 100644 index 0000000..c74a235 --- /dev/null +++ b/app/controllers/usertemplates_controller.rb @@ -0,0 +1,30 @@ +class UsertemplatesController < ApplicationController + before_filter :login_required + def index + @usertemplates = @logged_in_user.usertemplates.find(:all) + + if @usertemplates.empty? + @logged_in_user.usertemplates << Usertemplate.new(:name => 'blog_index', + :body => '') + @logged_in_user.usertemplates << Usertemplate.new(:name => 'blog_entry', + :body => '') + @usertemplates = @logged_in_user.usertemplates.find(:all) + end + end + + def edit + @usertemplate = @logged_in_user.usertemplates.find(params[:id]) + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end + + def update + @usertemplate = @logged_in_user.usertemplates.find(params[:id]) + if @usertemplate.update_attributes(params[:usertemplate]) + flash[:notice] = 'Template was successfully updated.' + redirect_to usertemplates_path + end + rescue ActiveRecord::RecordNotFound + redirect_to :action => 'index' + end +end diff --git a/app/drops/comment_drop.rb b/app/drops/comment_drop.rb new file mode 100644 index 0000000..e4ec0ac --- /dev/null +++ b/app/drops/comment_drop.rb @@ -0,0 +1,17 @@ +class CommentDrop < Liquid::Drop + def initialize(comment) + @comment = comment + end + + def author + @comment.user.username + end + + def body + @comment[:body] + end + + def created_at + @comment[:created_at] + end +end \ No newline at end of file diff --git a/app/drops/entry_drop.rb b/app/drops/entry_drop.rb new file mode 100644 index 0000000..0e8bfc2 --- /dev/null +++ b/app/drops/entry_drop.rb @@ -0,0 +1,25 @@ +class EntryDrop < Liquid::Drop + def initialize(entry) + @entry = entry + end + + def title + @entry[:title] + end + + def body + @entry[:body] + end + + def comments_count + @entry[:comments_count] + end + + def permalink + "/users/#{@entry.user.id}/entries/#{@entry.id}" + end + + def comment_post_url + "/users/#{@entry.user.id}/entries/#{@entry.id}/comments" + end +end \ No newline at end of file diff --git a/app/drops/user_drop.rb b/app/drops/user_drop.rb new file mode 100644 index 0000000..4a1cb64 --- /dev/null +++ b/app/drops/user_drop.rb @@ -0,0 +1,21 @@ +class UserDrop < Liquid::Drop + def initialize(user) + @user = user + end + + def username + @user[:username] + end + + def email + @user[:email] + end + + def profile + @user[:profile] + end + + def blog_title + @user[:blog_title] + end +end \ No newline at end of file diff --git a/app/filters/text_filters.rb b/app/filters/text_filters.rb new file mode 100644 index 0000000..dc66662 --- /dev/null +++ b/app/filters/text_filters.rb @@ -0,0 +1,11 @@ +module TextFilters + include ActionView::Helpers::TagHelper + + def textilize(input) + RedCloth.new(input).to_html + end + + def link_to_entry(entry) + content_tag :a, entry['title'], :href => entry['permalink'] + end +end \ No newline at end of file diff --git a/app/helpers/account_helper.rb b/app/helpers/account_helper.rb new file mode 100644 index 0000000..6d3694a --- /dev/null +++ b/app/helpers/account_helper.rb @@ -0,0 +1,2 @@ +module AccountHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..dfa40d3 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,61 @@ +# Methods added to this helper will be available to all templates in the application. +module ApplicationHelper + def yes_no(bool) + if bool == true + "yes" + else + "no" + end + end + + def tag_cloud(tags, classes) + max, min = 0, 0 + tags.each do |tag| + max = tag.count if tag.count > max + min = tag.count if tag.count < min + end + + divisor = ((max - min) / classes.size) + 1 + tags.each do |tag| + yield tag.name, classes[(tag.count - min) / divisor] + end + end + + def xfn_rel_tag(user, friendship) + rel_tag = [] + if user.id == friendship.friend.id + # identity + rel_tag << 'me' + else + # friendship + rel_tag << 'friend' if friendship.xfn_friend + rel_tag << 'acquaintance' if friendship.xfn_acquaintance + rel_tag << 'contact' if friendship.xfn_contact + + # physical + rel_tag << 'met' if friendship.xfn_met + + # professional + rel_tag << 'co-worker' if friendship.xfn_coworker + rel_tag << 'colleague' if friendship.xfn_colleague + + # geographical + rel_tag << 'co-resident' if friendship.xfn_coresident + rel_tag << 'neighbor' if friendship.xfn_neighbor + + # family + rel_tag << 'child' if friendship.xfn_child + rel_tag << 'parent' if friendship.xfn_parent + rel_tag << 'sibling' if friendship.xfn_sibling + rel_tag << 'spouse' if friendship.xfn_spouse + rel_tag << 'kin' if friendship.xfn_kin + + # romantic + rel_tag << 'muse' if friendship.xfn_muse + rel_tag << 'crush' if friendship.xfn_crush + rel_tag << 'date' if friendship.xfn_date + rel_tag << 'sweetheart' if friendship.xfn_sweetheart + end + rel_tag.join(' ') + end +end diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb new file mode 100644 index 0000000..2968277 --- /dev/null +++ b/app/helpers/articles_helper.rb @@ -0,0 +1,2 @@ +module ArticlesHelper +end diff --git a/app/helpers/blogs_helper.rb b/app/helpers/blogs_helper.rb new file mode 100644 index 0000000..cc0dbd2 --- /dev/null +++ b/app/helpers/blogs_helper.rb @@ -0,0 +1,2 @@ +module BlogsHelper +end diff --git a/app/helpers/categories_helper.rb b/app/helpers/categories_helper.rb new file mode 100644 index 0000000..e06f315 --- /dev/null +++ b/app/helpers/categories_helper.rb @@ -0,0 +1,2 @@ +module CategoriesHelper +end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb new file mode 100644 index 0000000..0ec9ca5 --- /dev/null +++ b/app/helpers/comments_helper.rb @@ -0,0 +1,2 @@ +module CommentsHelper +end diff --git a/app/helpers/entries_helper.rb b/app/helpers/entries_helper.rb new file mode 100644 index 0000000..debf7f3 --- /dev/null +++ b/app/helpers/entries_helper.rb @@ -0,0 +1,5 @@ +module EntriesHelper + def blog_title(user) + user.blog_title ||= user.username + end +end diff --git a/app/helpers/forums_helper.rb b/app/helpers/forums_helper.rb new file mode 100644 index 0000000..2e531fd --- /dev/null +++ b/app/helpers/forums_helper.rb @@ -0,0 +1,2 @@ +module ForumsHelper +end diff --git a/app/helpers/friends_helper.rb b/app/helpers/friends_helper.rb new file mode 100644 index 0000000..0b69e9b --- /dev/null +++ b/app/helpers/friends_helper.rb @@ -0,0 +1,2 @@ +module FriendsHelper +end diff --git a/app/helpers/newsletters_helper.rb b/app/helpers/newsletters_helper.rb new file mode 100644 index 0000000..b980224 --- /dev/null +++ b/app/helpers/newsletters_helper.rb @@ -0,0 +1,2 @@ +module NewslettersHelper +end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb new file mode 100644 index 0000000..2c057fd --- /dev/null +++ b/app/helpers/pages_helper.rb @@ -0,0 +1,2 @@ +module PagesHelper +end diff --git a/app/helpers/photos_helper.rb b/app/helpers/photos_helper.rb new file mode 100644 index 0000000..0a10d47 --- /dev/null +++ b/app/helpers/photos_helper.rb @@ -0,0 +1,2 @@ +module PhotosHelper +end diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb new file mode 100644 index 0000000..a7b8cec --- /dev/null +++ b/app/helpers/posts_helper.rb @@ -0,0 +1,2 @@ +module PostsHelper +end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb new file mode 100644 index 0000000..23450bc --- /dev/null +++ b/app/helpers/tags_helper.rb @@ -0,0 +1,2 @@ +module TagsHelper +end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb new file mode 100644 index 0000000..488eed5 --- /dev/null +++ b/app/helpers/topics_helper.rb @@ -0,0 +1,2 @@ +module TopicsHelper +end diff --git a/app/helpers/user_tags_helper.rb b/app/helpers/user_tags_helper.rb new file mode 100644 index 0000000..57db676 --- /dev/null +++ b/app/helpers/user_tags_helper.rb @@ -0,0 +1,2 @@ +module UserTagsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/helpers/usertemplates_helper.rb b/app/helpers/usertemplates_helper.rb new file mode 100644 index 0000000..790799d --- /dev/null +++ b/app/helpers/usertemplates_helper.rb @@ -0,0 +1,2 @@ +module UsertemplatesHelper +end diff --git a/app/models/article.rb b/app/models/article.rb new file mode 100644 index 0000000..a1b9d18 --- /dev/null +++ b/app/models/article.rb @@ -0,0 +1,17 @@ +class Article < ActiveRecord::Base + belongs_to :user + belongs_to :category + validates_presence_of :title + validates_presence_of :synopsis + validates_presence_of :body + validates_presence_of :title + validates_length_of :title, :maximum => 255 + validates_length_of :synopsis, :maximum => 1000 + validates_length_of :body, :maximum => 20000 + + before_save :update_published_at + + def update_published_at + self.published_at = Time.now if published == true + end +end diff --git a/app/models/category.rb b/app/models/category.rb new file mode 100644 index 0000000..432759c --- /dev/null +++ b/app/models/category.rb @@ -0,0 +1,4 @@ +class Category < ActiveRecord::Base + has_many :articles, :dependent => :nullify + validates_length_of :name, :maximum => 80 +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..9c5eedc --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,9 @@ +class Comment < ActiveRecord::Base + belongs_to :entry, :counter_cache => true + belongs_to :user + validates_length_of :body, :maximum => 1000 + + def to_liquid + CommentDrop.new(self) + end +end diff --git a/app/models/email.rb b/app/models/email.rb new file mode 100644 index 0000000..7158376 --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,2 @@ +class Email < ActiveRecord::Base +end diff --git a/app/models/entry.rb b/app/models/entry.rb new file mode 100644 index 0000000..b80e901 --- /dev/null +++ b/app/models/entry.rb @@ -0,0 +1,15 @@ +class Entry < ActiveRecord::Base + belongs_to :user, :counter_cache => true + has_many :comments + validates_length_of :title, :maximum => 255 + validates_length_of :body, :maximum => 10000 + + def after_save + self.user.update_attribute(:last_activity, "Wrote a blog entry") + self.user.update_attribute(:last_activity_at, Time.now) + end + + def to_liquid + EntryDrop.new(self) + end +end diff --git a/app/models/forum.rb b/app/models/forum.rb new file mode 100644 index 0000000..a7e124f --- /dev/null +++ b/app/models/forum.rb @@ -0,0 +1,7 @@ +class Forum < ActiveRecord::Base + has_many :topics, :dependent => :delete_all + has_many :posts, :through => :topics + validates_presence_of :name + validates_length_of :name, :maximum => 255 + validates_length_of :description, :maximum => 1000 +end diff --git a/app/models/friendship.rb b/app/models/friendship.rb new file mode 100644 index 0000000..68c7df8 --- /dev/null +++ b/app/models/friendship.rb @@ -0,0 +1,61 @@ +class Friendship < ActiveRecord::Base + belongs_to :user + belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id' + + def xfn_friendship=(friendship_type) + self.xfn_friend = false + self.xfn_acquaintance = false + self.xfn_contact = false + case friendship_type + when 'xfn_friend' : self.xfn_friend = true + when 'xfn_acquaintance' : self.xfn_acquaintance = true + when 'xfn_contact' : self.xfn_contact = true + end + end + + def xfn_friendship + return 'xfn_friend' if self.xfn_friend == true + return 'xfn_acquaintance' if self.xfn_acquaintance == true + return 'xfn_contact' if self.xfn_contact == true + false + end + + def xfn_geographical=(geo_type) + self.xfn_coresident = false + self.xfn_neighbor = false + case geo_type + when 'xfn_coresident' : self.xfn_coresident = true + when 'xfn_neighbor' : self.xfn_neighbor = true + end + end + + def xfn_geographical + return 'xfn_coresident' if self.xfn_coresident + return 'xfn_neighbor' if self.xfn_neighbor + false + end + + def xfn_family=(family_type) + self.xfn_child = false + self.xfn_parent = false + self.xfn_sibling = false + self.xfn_spouse = false + self.xfn_kin = false + case family_type + when 'xfn_child' : self.xfn_child = true + when 'xfn_parent' : self.xfn_parent = true + when 'xfn_sibling' : self.xfn_sibling = true + when 'xfn_spouse' : self.xfn_spouse = true + when 'xfn_kin' : self.xfn_kin = true + end + end + + def xfn_family + return 'xfn_child' if self.xfn_child + return 'xfn_parent' if self.xfn_parent + return 'xfn_sibling' if self.xfn_sibling + return 'xfn_spouse' if self.xfn_spouse + return 'xfn_kin' if self.xfn_kin + false + end +end \ No newline at end of file diff --git a/app/models/newsletter.rb b/app/models/newsletter.rb new file mode 100644 index 0000000..f802487 --- /dev/null +++ b/app/models/newsletter.rb @@ -0,0 +1,5 @@ +class Newsletter < ActiveRecord::Base + validates_presence_of :subject, :body + validates_length_of :subject, :maximum => 255 + validates_length_of :body, :maximum => 10000 +end diff --git a/app/models/notifier.rb b/app/models/notifier.rb new file mode 100644 index 0000000..e67f033 --- /dev/null +++ b/app/models/notifier.rb @@ -0,0 +1,20 @@ +class Notifier < ActionMailer::ARMailer + def new_comment_notification(comment) + blog_owner = comment.entry.user + recipients blog_owner.email_with_username + + from "RailsCoders " + subject "A new comment has been left on your blog" + body :comment => comment, + :blog_owner => blog_owner, + :blog_owner_url => "http://railscoders.net/users/#{blog_owner.id}", + :blog_entry_url => "http://railscoders.net/users/#{blog_owner.id}/entries/#{comment.entry.id}" + end + + def newsletter(user, newsletter) + recipients user.email + from "RailsCoders " + subject newsletter.subject + body :body => newsletter.body, :user => user + end +end diff --git a/app/models/page.rb b/app/models/page.rb new file mode 100644 index 0000000..cd58aa0 --- /dev/null +++ b/app/models/page.rb @@ -0,0 +1,14 @@ +class Page < ActiveRecord::Base + validates_presence_of :title, :body + validates_length_of :title, :within => 3..255 + validates_length_of :body, :maximum => 10000 + + def before_create + @attributes['permalink'] = + title.downcase.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9_]+/, '') + end + + def to_param + "#{id}-#{permalink}" + end +end diff --git a/app/models/photo.rb b/app/models/photo.rb new file mode 100644 index 0000000..ff00338 --- /dev/null +++ b/app/models/photo.rb @@ -0,0 +1,18 @@ +class Photo < ActiveRecord::Base + has_attachment :storage => :file_system, + :resize_to => '640x480', + :thumbnails => { :thumb => '160x120', :tiny => '50>' }, + :max_size => 5.megabytes, + :content_type => :image, + :processor => 'Rmagick' + validates_as_attachment + acts_as_taggable + belongs_to :user + + def after_save + if self.user + self.user.update_attribute(:last_activity, "Uploaded a photo") + self.user.update_attribute(:last_activity_at, Time.now) + end + end +end diff --git a/app/models/post.rb b/app/models/post.rb new file mode 100644 index 0000000..25e7b9d --- /dev/null +++ b/app/models/post.rb @@ -0,0 +1,12 @@ +class Post < ActiveRecord::Base + belongs_to :topic, :counter_cache => true + belongs_to :user, :counter_cache => true + + validates_presence_of :body + validates_length_of :body, :maximum => 10000 + + def after_save + self.user.update_attribute(:last_activity, "Posted in the forum") + self.user.update_attribute(:last_activity_at, Time.now) + end +end diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 0000000..33cd819 --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,3 @@ +class Role < ActiveRecord::Base + has_and_belongs_to_many :users +end diff --git a/app/models/topic.rb b/app/models/topic.rb new file mode 100644 index 0000000..92b04f8 --- /dev/null +++ b/app/models/topic.rb @@ -0,0 +1,8 @@ +class Topic < ActiveRecord::Base + belongs_to :forum, :counter_cache => true + belongs_to :user + has_many :posts, :dependent => :delete_all + + validates_presence_of :name + validates_length_of :name, :maximum => 255 +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..7aeac77 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,120 @@ +require 'digest/sha2' +require 'rss/2.0' + +class User < ActiveRecord::Base + attr_protected :hashed_password, :enabled + attr_accessor :password + + validates_presence_of :username + validates_presence_of :email + validates_presence_of :password, :if => :password_required? + validates_presence_of :password_confirmation, :if => :password_required? + + validates_confirmation_of :password, :if => :password_required? + + validates_uniqueness_of :username, :case_sensitive => false + validates_uniqueness_of :email, :case_sensitive => false + + validates_length_of :username, :within => 3..64 + validates_length_of :email, :within => 5..128 + validates_length_of :password, :within => 4..20, :if => :password_required? + validates_length_of :profile, :maximum => 1000 + + has_and_belongs_to_many :roles + has_many :articles + has_many :entries + has_many :comments + has_many :photos, :extend => TagCountsExtension + + has_many :topics + has_many :posts + + has_many :friendships + has_many :friends, :through => :friendships, :class_name => 'User' + has_many :usertemplates + + def before_save + self.hashed_password = User.encrypt(password) if !password.blank? + if self.has_attribute?('flickr_username') && !self.flickr_username.blank? + self.flickr_id = self.get_flickr_id + end + end + + def password_required? + hashed_password.blank? || !password.blank? + end + + def self.encrypt(string) + return Digest::SHA256.hexdigest(string) + end + + def self.authenticate(username, password) + find_by_username_and_hashed_password_and_enabled(username, + User.encrypt(password), true) + end + + def has_role?(rolename) + self.roles.find_by_name(rolename) ? true : false + end + + def email_with_username + "#{username} <#{email}>" + end + + def get_flickr_id + # build the flickr request + flickr_request = "http://api.flickr.com/services/rest/?" + flickr_request += "method=flickr.people.findByUsername" + flickr_request += "&username=#{self.flickr_username}" + flickr_request += "&api_key=#{FLICKR_API_KEY}" + + # perform the API call + response = "" + open(flickr_request) do |s| + response = s.read + end + + # parse the result + xml_response = REXML::Document.new(response) + if xml_response.root.attributes["stat"] == 'ok' + xml_response.root.elements["user"].attributes["nsid"] + else + nil + end + end + + def flickr_feed + # build the flickr request + flickr_request = "http://api.flickr.com/services/rest/?" + flickr_request += "method=flickr.people.getPublicPhotos" + flickr_request += "&per_page=4" + flickr_request += "&user_id=#{self.flickr_id}" + flickr_request += "&api_key=#{FLICKR_API_KEY}" + + # perform the API call + response = "" + open(flickr_request) do |s| + response = s.read + end + + # parse the result + xml_response = REXML::Document.new(response) + if xml_response.root.attributes["stat"] == 'ok' + flickr_photos = [] + xml_response.root.elements.each("photos/photo") do |photo| + photo_url = "http://farm" + photo.attributes["farm"] + photo_url += ".static.flickr.com/" + photo.attributes["server"]+"/" + photo.attributes["id"] + photo_url += "_" + photo.attributes["secret"]+"_t.jpg" + flickr_photos << photo_url + end + return flickr_photos + else + nil + end + end + + def to_liquid + UserDrop.new(self) + end + +end diff --git a/app/models/usertemplate.rb b/app/models/usertemplate.rb new file mode 100644 index 0000000..56248e1 --- /dev/null +++ b/app/models/usertemplate.rb @@ -0,0 +1,4 @@ +class Usertemplate < ActiveRecord::Base + belongs_to :user + validates_length_of :body, :maximum => 10000 +end diff --git a/app/views/account/authenticate.rhtml b/app/views/account/authenticate.rhtml new file mode 100644 index 0000000..755ce59 --- /dev/null +++ b/app/views/account/authenticate.rhtml @@ -0,0 +1,2 @@ +

Account#authenticate

+

Find me in app/views/account/authenticate.rhtml

diff --git a/app/views/account/login.rhtml b/app/views/account/login.rhtml new file mode 100644 index 0000000..5f4d66a --- /dev/null +++ b/app/views/account/login.rhtml @@ -0,0 +1,7 @@ +

Login

+ +<% form_for :user, :url => {:action => 'authenticate'} do |f| -%> +

Username:
<%= f.text_field :username, :size => 30 %>

+

Password:
<%= f.password_field :password, :size => 30 %>

+ <%= submit_tag 'Login' %> +<% end %> diff --git a/app/views/account/logout.rhtml b/app/views/account/logout.rhtml new file mode 100644 index 0000000..5b8ae7e --- /dev/null +++ b/app/views/account/logout.rhtml @@ -0,0 +1,2 @@ +

Account#logout

+

Find me in app/views/account/logout.rhtml

diff --git a/app/views/articles/admin.rhtml b/app/views/articles/admin.rhtml new file mode 100644 index 0000000..ce9537a --- /dev/null +++ b/app/views/articles/admin.rhtml @@ -0,0 +1,21 @@ +

Edit Articles

+ +

<%= link_to 'Create New Article', new_article_path %>

+ +<% if @articles_pages.page_count > 1 %> +

Pages: + <%= pagination_links @articles_pages, :params => params %> +

+<% end %> + +
    +<% @articles.each do |article| %> +
  • + <%= link_to article.title, article_url(article) %> + [<%= link_to 'Edit', edit_article_path(article) %>] + [<%= link_to 'Delete', article_path(article), :method => :delete, + :confirm => 'Are you sure you wish to delete this article?' %>] + +
  • +<% end %> +
diff --git a/app/views/articles/atom.rxml b/app/views/articles/atom.rxml new file mode 100644 index 0000000..bf57146 --- /dev/null +++ b/app/views/articles/atom.rxml @@ -0,0 +1,28 @@ +xml.instruct! + +xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do + + xml.title "RailsCoders News" + xml.link "rel" => "self", "href" => articles_url + xml.link "rel" => "alternate", "href" => articles_url + xml.id articles_url + if @articles.any? + xml.updated @articles.first.updated_at.strftime "%Y-%m-%dT%H:%M:%SZ" + end + xml.author { xml.name "RailsCoders Site" } + + @articles.each do |article| + xml.article do + xml.title article.title + xml.link "rel" => "alternate", "href" => article_url(article) + xml.id article_url(article) + xml.updated article.updated_at.strftime "%Y-%m-%dT%H:%M:%SZ" + xml.author { xml.name article.user.username } + xml.summary article.synopsis + xml.content "type" => "html" do + xml.text! textilize(article.body) + end + end + end + +end diff --git a/app/views/articles/create.rhtml b/app/views/articles/create.rhtml new file mode 100644 index 0000000..228880e --- /dev/null +++ b/app/views/articles/create.rhtml @@ -0,0 +1,2 @@ +

Articles#create

+

Find me in app/views/articles/create.rhtml

diff --git a/app/views/articles/destroy.rhtml b/app/views/articles/destroy.rhtml new file mode 100644 index 0000000..5a691c5 --- /dev/null +++ b/app/views/articles/destroy.rhtml @@ -0,0 +1,2 @@ +

Articles#destroy

+

Find me in app/views/articles/destroy.rhtml

diff --git a/app/views/articles/edit.rhtml b/app/views/articles/edit.rhtml new file mode 100644 index 0000000..a5482dd --- /dev/null +++ b/app/views/articles/edit.rhtml @@ -0,0 +1,13 @@ +

Edit Article

+<% form_for :article, + :url => article_url(@article), + :html => { :method => :put } do |f| -%> +

Title:
<%= f.text_field :title, :size => 60 %>

+

Synopsis:
<%= f.text_area :synopsis, :rows => 4, :cols => 60 %>

+

Body:
<%= f.text_area :body, :rows => 20, :cols => 60 %>

+

Category:
+ <%= f.collection_select :category_id, + Category.find(:all), :id, :name, :include_blank => true %>

+

Published? <%= f.check_box :published %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', articles_url %> +<% end -%> diff --git a/app/views/articles/index.rhtml b/app/views/articles/index.rhtml new file mode 100644 index 0000000..c80d1bc --- /dev/null +++ b/app/views/articles/index.rhtml @@ -0,0 +1,29 @@ +<% content_for :head do %> + <%= auto_discovery_link_tag %> +<% end %> + +

News Articles

+ +<% if @articles_pages.page_count > 1 %> +

Pages: + <%= pagination_links @articles_pages, :params => params %> +

+<% end %> + +<% @articles.each do |article| %> +
+

<%= article.title %>

+ <% if article.category %> +

+ Category: '<%= link_to article.category.name, + category_articles_path(article.category) %>' +

+ <% end %> + +

+ <%= article.created_at.to_s(:short) %> by <%= article.user.username %>
+ <%= article.synopsis %>
+ <%= link_to 'Read the full article', article_url(article) %> +

+
+<% end %> diff --git a/app/views/articles/new.rhtml b/app/views/articles/new.rhtml new file mode 100644 index 0000000..a46e1f2 --- /dev/null +++ b/app/views/articles/new.rhtml @@ -0,0 +1,12 @@ +

Create Article

+<% form_for :article, + :url => articles_url, + :html => { :method => :post } do |f| -%> +

Title:
<%= f.text_field :title, :size => 60 %>

+

Synopsis:
<%= f.text_area :synopsis, :rows => 4, :cols => 60 %>

+

Body:
<%= f.text_area :body, :rows => 20, :cols => 60 %>

+

Category:
+ <%= f.collection_select :category_id, Category.find(:all), :id, :name %>

+

Published? <%= f.check_box :published %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', articles_url %> +<% end -%> \ No newline at end of file diff --git a/app/views/articles/rss.rxml b/app/views/articles/rss.rxml new file mode 100644 index 0000000..69c6e80 --- /dev/null +++ b/app/views/articles/rss.rxml @@ -0,0 +1,23 @@ +xml.instruct! + +xml.rss "version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/" do + xml.channel do + + xml.title "News Feed " + xml.link articles_url + xml.pubDate CGI.rfc1123_date @articles.first.published_at if @articles.any? + xml.description "News about the RailsCoders" + + @articles.each do |article| + xml.item do + xml.title article.title + xml.link article_url(article) + xml.description article.body + xml.pubDate CGI.rfc1123_date article.published_at + xml.guid article_url(article) + xml.author "#{article.user.email} (#{article.user.username})" + end + end + + end +end diff --git a/app/views/articles/show.rhtml b/app/views/articles/show.rhtml new file mode 100644 index 0000000..9803dd0 --- /dev/null +++ b/app/views/articles/show.rhtml @@ -0,0 +1,14 @@ +

<%= @article.title %>

+ +<% if @article.category %> +

+ Category: '<%= link_to @article.category.name, + category_articles_path(@article.category) %>' +

+<% end %> + +

+ <%= @article.created_at.to_s(:short) %>
+ <%= textilize(@article.body) %>
+

+

<%= link_to 'Back to article list', articles_url %>

diff --git a/app/views/articles/update.rhtml b/app/views/articles/update.rhtml new file mode 100644 index 0000000..d4ed6be --- /dev/null +++ b/app/views/articles/update.rhtml @@ -0,0 +1,2 @@ +

Articles#update

+

Find me in app/views/articles/update.rhtml

diff --git a/app/views/blogs/index.rhtml b/app/views/blogs/index.rhtml new file mode 100644 index 0000000..1a9af6f --- /dev/null +++ b/app/views/blogs/index.rhtml @@ -0,0 +1,14 @@ +

Recently updated blogs

+ +<% @entries.each do |entry| %> +

+ <%= link_to entry.user.username, entries_url(:user_id => entry.user) %>
+ '<%= entry.title %>' was posted <%= time_ago_in_words(entry.created_at) %> ago +

+<% end %> + +<% if @entry_pages.page_count > 1 %> +

Pages: + <%= pagination_links @entry_pages, :params => params %> +

+<% end %> diff --git a/app/views/categories/admin.rhtml b/app/views/categories/admin.rhtml new file mode 100644 index 0000000..351d0b8 --- /dev/null +++ b/app/views/categories/admin.rhtml @@ -0,0 +1,14 @@ +

Edit Categories

+ +

<%= link_to 'Create New Category', new_category_path %>

+ +
    +<% @categories.each do |category| %> +
  • + <%= link_to category.name, category_articles_url(:category_id => category) %> + [<%= link_to 'Edit', edit_category_path(category) %>] + [<%= link_to 'Delete', category_path(category), :method => :delete, + :confirm => 'Are you sure you wish to delete this category?' %>] +
  • +<% end %> +
diff --git a/app/views/categories/create.rhtml b/app/views/categories/create.rhtml new file mode 100644 index 0000000..a4e9172 --- /dev/null +++ b/app/views/categories/create.rhtml @@ -0,0 +1,2 @@ +

Categories#create

+

Find me in app/views/categories/create.rhtml

diff --git a/app/views/categories/destroy.rhtml b/app/views/categories/destroy.rhtml new file mode 100644 index 0000000..2c836d8 --- /dev/null +++ b/app/views/categories/destroy.rhtml @@ -0,0 +1,2 @@ +

Categories#destroy

+

Find me in app/views/categories/destroy.rhtml

diff --git a/app/views/categories/edit.rhtml b/app/views/categories/edit.rhtml new file mode 100644 index 0000000..7fbd77c --- /dev/null +++ b/app/views/categories/edit.rhtml @@ -0,0 +1,8 @@ +

Edit a Category

+<%= error_messages_for :category %> +<% form_for(:category, + :url => category_path(@category), + :html => {:method => :put}) do |f| -%> +

Name:
<%= f.text_field :name, :size => 60 %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', admin_categories_url %> +<% end %> diff --git a/app/views/categories/index.rhtml b/app/views/categories/index.rhtml new file mode 100644 index 0000000..a8ab8bc --- /dev/null +++ b/app/views/categories/index.rhtml @@ -0,0 +1,8 @@ +

Categories

+
    +<% @categories.each do |category| %> +
  • + <%= link_to category.name, category_articles_url(:category_id => category) %> +
  • +<% end %> +
diff --git a/app/views/categories/new.rhtml b/app/views/categories/new.rhtml new file mode 100644 index 0000000..5cc4974 --- /dev/null +++ b/app/views/categories/new.rhtml @@ -0,0 +1,6 @@ +

Create a New Category

+<%= error_messages_for :category %> +<% form_for(:category, :url => categories_path) do |f| -%> +

Name:
<%= f.text_field :name, :size => 60 %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', admin_categories_url %> +<% end %> diff --git a/app/views/categories/show.rhtml b/app/views/categories/show.rhtml new file mode 100644 index 0000000..63e70bb --- /dev/null +++ b/app/views/categories/show.rhtml @@ -0,0 +1,2 @@ +

Categories#show

+

Find me in app/views/categories/show.rhtml

diff --git a/app/views/categories/update.rhtml b/app/views/categories/update.rhtml new file mode 100644 index 0000000..58e93f6 --- /dev/null +++ b/app/views/categories/update.rhtml @@ -0,0 +1,2 @@ +

Categories#update

+

Find me in app/views/categories/update.rhtml

diff --git a/app/views/comments/edit.rhtml b/app/views/comments/edit.rhtml new file mode 100644 index 0000000..388fe3d --- /dev/null +++ b/app/views/comments/edit.rhtml @@ -0,0 +1,12 @@ +

Editing comment

+ +<%= error_messages_for :comment %> + +<% form_for(:comment, :url => comment_path(@comment), :html => { :method => :put }) do |f| %> +

+ <%= submit_tag "Update" %> +

+<% end %> + +<%= link_to 'Show', comment_path(@comment) %> | +<%= link_to 'Back', comments_path %> \ No newline at end of file diff --git a/app/views/comments/index.rhtml b/app/views/comments/index.rhtml new file mode 100644 index 0000000..388915b --- /dev/null +++ b/app/views/comments/index.rhtml @@ -0,0 +1,18 @@ +

Listing comments

+ + + + + +<% for comment in @comments %> + + + + + +<% end %> +
<%= link_to 'Show', comment_path(comment) %><%= link_to 'Edit', edit_comment_path(comment) %><%= link_to 'Destroy', comment_path(comment), :confirm => 'Are you sure?', :method => :delete %>
+ +
+ +<%= link_to 'New comment', new_comment_path %> \ No newline at end of file diff --git a/app/views/comments/new.rhtml b/app/views/comments/new.rhtml new file mode 100644 index 0000000..817ac64 --- /dev/null +++ b/app/views/comments/new.rhtml @@ -0,0 +1,11 @@ +

New comment

+ +<%= error_messages_for :comment %> + +<% form_for(:comment, :url => comments_path) do |f| %> +

+ <%= submit_tag "Create" %> +

+<% end %> + +<%= link_to 'Back', comments_path %> \ No newline at end of file diff --git a/app/views/comments/show.rhtml b/app/views/comments/show.rhtml new file mode 100644 index 0000000..a4bdfd9 --- /dev/null +++ b/app/views/comments/show.rhtml @@ -0,0 +1,3 @@ + +<%= link_to 'Edit', edit_comment_path(@comment) %> | +<%= link_to 'Back', comments_path %> \ No newline at end of file diff --git a/app/views/entries/edit.rhtml b/app/views/entries/edit.rhtml new file mode 100644 index 0000000..9f14d04 --- /dev/null +++ b/app/views/entries/edit.rhtml @@ -0,0 +1,11 @@ +

Editing entry

+ +<%= error_messages_for :entry %> + +<% form_for(:entry, + :url => entry_path(:user_id => logged_in_user.id, :id => @entry), + :html => { :method => :put }) do |f| %> +

Title:
<%= f.text_field :title, :size => 40 -%>

+

Blog Entry:
<%= f.text_area :body, :rows => 10, :cols => 60 -%>

+

<%= submit_tag "Save" %> or <%= link_to 'cancel', entries_path %>

+<% end %> diff --git a/app/views/entries/index.rhtml b/app/views/entries/index.rhtml new file mode 100644 index 0000000..e6dd649 --- /dev/null +++ b/app/views/entries/index.rhtml @@ -0,0 +1,31 @@ +

<%= link_to blog_title(@user), entries_path(:user_id => @user.id) %>

+ +<% @entries.each do |entry| -%> +
+

<%= link_to entry.title, + entry_path(:user_id => entry.user, :id => entry) %>

+ + <% if is_logged_in? and logged_in_user.id == @user.id -%> +
+ <%= link_to 'Edit', edit_entry_path(:user_id => entry.user, :id => entry) %> + <%= link_to 'Destroy', entry_path(:user_id => entry.user, :id => entry), + :confirm => 'Are you sure?', :method => :delete %> +
+ <% end -%> + +
+ <%= textilize(entry.body) %> +
+ +
+

<%= link_to pluralize(entry.comments_count, 'comment'), + entry_path(:user_id => entry.user, :id => entry) -%>

+
+
+<% end -%> + +<% if @entry_pages.page_count > 1 %> +

Pages: + <%= pagination_links @entry_pages, :params => params %> +

+<% end %> diff --git a/app/views/entries/new.rhtml b/app/views/entries/new.rhtml new file mode 100644 index 0000000..43c94af --- /dev/null +++ b/app/views/entries/new.rhtml @@ -0,0 +1,11 @@ +

New Blog Entry

+ +<%= error_messages_for :entry %> + +<% form_for(:entry, :url => entries_path) do |f| %> +

Title:
<%= f.text_field :title, :size => 40 -%>

+

Blog Entry:
<%= f.text_area :body, :rows => 10, :cols => 60 -%>

+

<%= submit_tag "Create" %>

+<% end %> + +<%= link_to 'Back', entries_path %> diff --git a/app/views/entries/show.rhtml b/app/views/entries/show.rhtml new file mode 100644 index 0000000..98857d1 --- /dev/null +++ b/app/views/entries/show.rhtml @@ -0,0 +1,35 @@ +

+ <%= link_to blog_title(@entry.user), entries_path(:user_id => @entry.user) %> +

+ +

<%= @entry.title %>

+ +

<%= textilize(@entry.body) %>

+ +

Comments

+<% @entry.comments.each do |comment| -%> +
+

At <%= comment.created_at.to_s(:short) %>, + <%= comment.user.username %> said:

+ <% if is_logged_in? and logged_in_user.id == @entry.user.id -%> +

+ <%= link_to 'Delete this comment', + comment_path( + :user_id => @entry.user, + :entry_id => @entry.id, + :id => comment.id), + :confirm => 'Are you sure?', + :method => :delete -%> +

+ <% end -%> +

<%=h comment.body %>

+
+<% end -%> + +

Leave a comment

+<%= error_messages_for :comment %> +<% form_for(:comment, :url => comments_path(:user_id => @entry.user, + :entry_id => @entry)) do |f| -%> +

<%= f.text_area :body, :rows => 4, :cols => 40 %>

+

<%= submit_tag 'Save Comment' -%>

+<% end -%> diff --git a/app/views/forums/_form.rhtml b/app/views/forums/_form.rhtml new file mode 100644 index 0000000..5d2f15c --- /dev/null +++ b/app/views/forums/_form.rhtml @@ -0,0 +1,2 @@ +

Forum Name:
<%= f.text_field :name, :size => 40 -%>

+

Description:
<%= f.text_area :description, :rows => 4, :cols => 60 -%>

diff --git a/app/views/forums/edit.rhtml b/app/views/forums/edit.rhtml new file mode 100644 index 0000000..577805f --- /dev/null +++ b/app/views/forums/edit.rhtml @@ -0,0 +1,10 @@ +

Edit forum

+<%= error_messages_for :forum %> +<% form_for(:forum, + :url => forum_path(@forum), + :html => { :method => :put }) do |f| %> + <%= render :partial => 'form', :locals => {:f => f} -%> + <%= submit_tag "Update" %> +<% end %> +<%= link_to 'Show', forum_path(@forum) %> | +<%= link_to 'Back', forums_path %> diff --git a/app/views/forums/index.rhtml b/app/views/forums/index.rhtml new file mode 100644 index 0000000..ee91fb4 --- /dev/null +++ b/app/views/forums/index.rhtml @@ -0,0 +1,35 @@ +

Forums

+ +<% if is_logged_in? and logged_in_user.has_role?('Moderator') %> +

<%= link_to 'Create New Forum', new_forum_path -%>

+<% end %> + + + + + + + <% @forums.each do |forum| -%> + + + + + <% end -%> +
Forum nameTopics
+
+ <%= link_to forum.name, topics_path(:forum_id => forum) -%> +
+ +
+ <%= forum.description -%> +
+ + <% if is_logged_in? and logged_in_user.has_role?('Moderator') -%> +
+ + <%= link_to 'edit', edit_forum_path(forum) %> + <%= link_to 'delete', forum_path(forum), :method => :delete, + :confirm => 'Are you sure? This will delete this entire forum.' -%> + + <% end -%> +
<%= forum.topics_count %>
\ No newline at end of file diff --git a/app/views/forums/new.rhtml b/app/views/forums/new.rhtml new file mode 100644 index 0000000..adbce6a --- /dev/null +++ b/app/views/forums/new.rhtml @@ -0,0 +1,7 @@ +

New forum

+<%= error_messages_for :forum %> +<% form_for(:forum, :url => forums_path) do |f| %> + <%= render :partial => 'form', :locals => {:f => f} -%> + <%= submit_tag "Create" %> +<% end %> +<%= link_to 'Back', forums_path %> diff --git a/app/views/forums/show.rhtml b/app/views/forums/show.rhtml new file mode 100644 index 0000000..c6e0d10 --- /dev/null +++ b/app/views/forums/show.rhtml @@ -0,0 +1,3 @@ + +<%= link_to 'Edit', edit_forum_path(@forum) %> | +<%= link_to 'Back', forums_path %> \ No newline at end of file diff --git a/app/views/friends/_friendship.rhtml b/app/views/friends/_friendship.rhtml new file mode 100644 index 0000000..9808e5b --- /dev/null +++ b/app/views/friends/_friendship.rhtml @@ -0,0 +1,15 @@ + + + <%= link_to friendship.friend.username, + user_path(friendship.friend), + :class => 'xfnRelationship', + :rel => xfn_rel_tag(@user, friendship), + :id => "friend-#{friendship.friend.id}" %> + + + <% if friendship.friend.last_activity_at %> + <%= friendship.friend.last_activity %> + <%= time_ago_in_words(friendship.friend.last_activity_at) %> ago + <% end %> + + \ No newline at end of file diff --git a/app/views/friends/_friendship_with_edit.rhtml b/app/views/friends/_friendship_with_edit.rhtml new file mode 100644 index 0000000..92e25af --- /dev/null +++ b/app/views/friends/_friendship_with_edit.rhtml @@ -0,0 +1,20 @@ + + + <%= link_to friendship_with_edit.friend.username, + user_path(friendship_with_edit.friend), + :class => 'xfnRelationship', + :rel => xfn_rel_tag(@user, friendship_with_edit), + :id => "friend-#{friendship_with_edit.friend.id}" %> + + + <% if friendship_with_edit.friend.last_activity_at %> + <%= friendship_with_edit.friend.last_activity %> + <%= time_ago_in_words(friendship_with_edit.friend.last_activity_at) %> ago + <% end %> + + + [<%= link_to 'edit friendship', + edit_friend_url(:user_id => @user, + :id => friendship_with_edit.friend) %>] + + \ No newline at end of file diff --git a/app/views/friends/edit.rhtml b/app/views/friends/edit.rhtml new file mode 100644 index 0000000..b76cf69 --- /dev/null +++ b/app/views/friends/edit.rhtml @@ -0,0 +1,68 @@ +

Edit a friend

+ +<%= error_messages_for :friendship %> + +<% form_for(:friendship, + :url => friend_path(:user_id => @logged_in_user, + :friend_id => @friend), + :html => { :multipart => true, :method => :put }) do |f| %> + +

+ Define your relationship with <%= @friend.username %> +

+ +

+ Friendship
+ <%= f.radio_button :xfn_friendship, :xfn_contact %> Contact + <%= f.radio_button :xfn_friendship, :xfn_acquaintance %> Acquaintance + <%= f.radio_button :xfn_friendship, :xfn_friend %> Friend + <%= f.radio_button :xfn_friendship, false %> None +

+ +

+ Physical
+ <%= f.check_box :xfn_met %> Met +

+ +

+ Professional
+ <%= f.check_box :xfn_coworker %> Co-worker + <%= f.check_box :xfn_colleague %> Colleague +

+ +

+ Geographical
+ <%= f.radio_button :xfn_geographical, :xfn_coresident %> Co-resident + <%= f.radio_button :xfn_geographical, :xfn_neighbor %> Neighbor + <%= f.radio_button :xfn_geographical, false %> None +

+ +

+ Family
+ <%= f.radio_button :xfn_family, :xfn_child %> Child + <%= f.radio_button :xfn_family, :xfn_parent %> Parent + <%= f.radio_button :xfn_family, :xfn_sibling %> Sibling + <%= f.radio_button :xfn_family, :xfn_spouse %> Spouse + <%= f.radio_button :xfn_family, :xfn_kin %> Kin + <%= f.radio_button :xfn_family, false %> None +

+ +

+ Romantic
+ <%= f.check_box :xfn_muse %> Muse + <%= f.check_box :xfn_crush %> Crush + <%= f.check_box :xfn_date %> Date + <%= f.check_box :xfn_sweetheart %> Sweetheart +

+ +

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', user_path(@friend) %> +

+ +

+ <%= link_to 'Delete this friendship', + friend_path(:user_id => @logged_in_user, :id => @friend), + :method => :delete %> +

+ +<% end %> \ No newline at end of file diff --git a/app/views/friends/index.rhtml b/app/views/friends/index.rhtml new file mode 100644 index 0000000..f83c847 --- /dev/null +++ b/app/views/friends/index.rhtml @@ -0,0 +1,9 @@ +

<%= @user.username %>'s Friends

+ + + <% if is_logged_in? and logged_in_user == @user %> + <%= render :partial => 'friendship_with_edit', :collection=>@user.friendships %> + <% else %> + <%= render :partial => 'friendship', :collection => @user.friendships %> + <% end %> +
\ No newline at end of file diff --git a/app/views/friends/new.rhtml b/app/views/friends/new.rhtml new file mode 100644 index 0000000..054323e --- /dev/null +++ b/app/views/friends/new.rhtml @@ -0,0 +1,61 @@ +

Add a new friend

+ +<%= error_messages_for :friendship %> + +<% form_for(:friendship, + :url => friends_path(:user_id => @logged_in_user, + :friend_id => @friend), + :html => { :multipart => true }) do |f| %> +

+ Define your relationship with <%= @friend.username %> +

+ +

+ Friendship
+ <%= f.radio_button :xfn_friendship, :xfn_contact %> Contact + <%= f.radio_button :xfn_friendship, :xfn_acquaintance %> Acquaintance + <%= f.radio_button :xfn_friendship, :xfn_friend %> Friend + <%= f.radio_button :xfn_friendship, false %> None +

+ +

+ Physical
+ <%= f.check_box :xfn_met %> Met +

+ +

+ Professional
+ <%= f.check_box :xfn_coworker %> Co-worker + <%= f.check_box :xfn_colleague %> Colleague +

+ +

+ Geographical
+ <%= f.radio_button :xfn_geographical, :xfn_coresident %> Co-resident + <%= f.radio_button :xfn_geographical, :xfn_neighbor %> Neighbor + <%= f.radio_button :xfn_geographical, false %> None +

+ +

+ Family
+ <%= f.radio_button :xfn_family, :xfn_child %> Child + <%= f.radio_button :xfn_family, :xfn_parent %> Parent + <%= f.radio_button :xfn_family, :xfn_sibling %> Sibling + <%= f.radio_button :xfn_family, :xfn_spouse %> Spouse + <%= f.radio_button :xfn_family, :xfn_kin %> Kin + <%= f.radio_button :xfn_family, false %> None +

+ +

+ Romantic
+ <%= f.check_box :xfn_muse %> Muse + <%= f.check_box :xfn_crush %> Crush + <%= f.check_box :xfn_date %> Date + <%= f.check_box :xfn_sweetheart %> Sweetheart +

+ + <%= f.hidden_field :friend_id %> +

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', user_path(@friend) %> +

+<% end %> \ No newline at end of file diff --git a/app/views/layouts/_menu.rhtml b/app/views/layouts/_menu.rhtml new file mode 100644 index 0000000..b0a7b2b --- /dev/null +++ b/app/views/layouts/_menu.rhtml @@ -0,0 +1,38 @@ +
    +
  • <%= link_to 'Home', index_url %>
  • +
  • <%= link_to 'News', articles_path %>
  • +
  • <%= link_to 'Forums', forums_path %>
  • +
  • <%= link_to 'Blogs', blogs_path %>
  • +
  • <%= link_to 'Photos', photos_path %>
  • +
  • <%= link_to 'Photo Tags', tags_path %>
  • +

  • + + <% if is_logged_in? %> +
  • Logged in as: <%= logged_in_user.username %>
  • +
  • <%= link_to 'My Profile', edit_user_path(logged_in_user) %>
  • +
  • <%= link_to 'My Friends', friends_path(:user_id => logged_in_user) %>
  • +
  • <%= link_to 'My Photos', user_photos_path(:user_id => logged_in_user) -%>
  • +
  • <%= link_to 'New Blog Post', new_entry_path(:user_id => logged_in_user) -%>
  • +
  • <%= link_to 'Blog Templates', usertemplates_path -%>
  • +
  • <%= link_to 'Upload Photo', user_new_photo_path(:user_id => logged_in_user) -%>
  • +
  • <%= link_to 'Logout', {:controller => 'account', :action => 'logout'}, :method => :post %>
  • + <% else %> +
  • <%= link_to 'Signup', :controller => 'users', :action => 'new' %>
  • +
  • <%= link_to 'Login', :controller => 'account', :action => 'login' %>
  • + <% end %> + + <% if logged_in_user and logged_in_user.has_role?('administrator') %> +

  • +
  • Admin Options
  • +
  • <%= link_to 'Administer Users', users_path %>
  • +
  • <%= link_to 'Edit Pages', pages_path %>
  • +
  • <%= link_to 'Newsletters', newsletters_path %>
  • + <% end %> + + <% if is_logged_in? && logged_in_user.has_role?('editor') %> +

  • +
  • Editor Options
  • +
  • <%= link_to 'News Articles', admin_articles_path %>
  • +
  • <%= link_to 'News Categories', admin_categories_path %>
  • + <% end %> +
diff --git a/app/views/layouts/_mobilemenu.rhtml b/app/views/layouts/_mobilemenu.rhtml new file mode 100644 index 0000000..7af45bc --- /dev/null +++ b/app/views/layouts/_mobilemenu.rhtml @@ -0,0 +1,14 @@ +
    +
  • 0 <%= link_to 'Home', mobile_index_url, :accesskey => '0' %>
  • +
  • 1 <%= link_to 'News', mobile_articles_path, :accesskey => '1' %>
  • +
  • 2 <%= link_to 'Forums', mobile_forums_path, :accesskey => '2' %>
  • +
  • 3 <%= link_to 'Blogs', mobile_blogs_path, :accesskey => '3' %>
  • +
  • 4 <%= link_to 'Photos', mobile_photos_path, :accesskey => '4' %>
  • + + <% if is_logged_in? %> +
  • <%= link_to 'Logout', mobile_logout_path %>
  • + <% else %> +
  • <%= link_to 'Login', mobile_login_path %>
  • + <% end %> + +
\ No newline at end of file diff --git a/app/views/layouts/application.rhtml b/app/views/layouts/application.rhtml new file mode 100644 index 0000000..d2ff8c2 --- /dev/null +++ b/app/views/layouts/application.rhtml @@ -0,0 +1,29 @@ + + + + RailsCoders + <%= stylesheet_link_tag 'main' %> + <%= javascript_include_tag :defaults %> + <%= yield :head %> + + +
+ +
+ <%= render :partial => 'layouts/menu' %> +
+
+ <% if flash[:notice] -%> +
<%= flash[:notice] %>
+ <% end -%> + <% if flash[:error] -%> +
<%= flash[:error] %>
+ <% end -%> + <%= yield %> +
+
+ + diff --git a/app/views/layouts/mobile.rhtml b/app/views/layouts/mobile.rhtml new file mode 100644 index 0000000..c3c25b8 --- /dev/null +++ b/app/views/layouts/mobile.rhtml @@ -0,0 +1,28 @@ + + + + RailsCoders + <%= stylesheet_link_tag 'mobile' %> + <%= yield :head %> + + +
+ +
+ <% if flash[:notice] -%> +
<%= flash[:notice] %>
+ <% end -%> + <% if flash[:error] -%> +
<%= flash[:error] %>
+ <% end -%> + <%= yield %> +
+ +
+ + \ No newline at end of file diff --git a/app/views/mobile/account/login.rhtml b/app/views/mobile/account/login.rhtml new file mode 100644 index 0000000..c312b95 --- /dev/null +++ b/app/views/mobile/account/login.rhtml @@ -0,0 +1,7 @@ +

Login

+ +<% form_for :user, :url => {:action => 'authenticate'} do |f| -%> +

Username:<%= f.text_field :username %>

+

Password:<%= f.password_field :password %>

+ <%= submit_tag 'Login' %> +<% end %> diff --git a/app/views/mobile/articles/index.rhtml b/app/views/mobile/articles/index.rhtml new file mode 100644 index 0000000..7a649d1 --- /dev/null +++ b/app/views/mobile/articles/index.rhtml @@ -0,0 +1,30 @@ +

News Articles

+ +<% if @articles_pages.page_count > 1 %> +

Pages: + <%= pagination_links @articles_pages, :params => params %> +

+<% end %> + +<% @articles.each do |article| %> +
+

<%= article.title %>

+ <% if article.category %> +

+ Category: '<%= link_to article.category.name, + mobile_category_articles_path(article.category) %>' +

+ <% end %> + +

+ <%= article.created_at.to_s(:short) %> by <%= article.user.username %>
+ <%= link_to 'Read the full article', mobile_article_url(article) %> +

+
+<% end %> + +<% if @articles_pages.page_count > 1 %> +

Pages: + <%= pagination_links @articles_pages, :params => params %> +

+<% end %> \ No newline at end of file diff --git a/app/views/mobile/articles/show.rhtml b/app/views/mobile/articles/show.rhtml new file mode 100644 index 0000000..fac327b --- /dev/null +++ b/app/views/mobile/articles/show.rhtml @@ -0,0 +1,6 @@ +

<%= @article.title %>

+

+ <%= @article.created_at.to_s(:short) %>
+ <%= textilize(@article.body) %>
+

+

<%= link_to 'Back to article list', mobile_articles_url %>

diff --git a/app/views/mobile/blogs/index.rhtml b/app/views/mobile/blogs/index.rhtml new file mode 100644 index 0000000..fec4484 --- /dev/null +++ b/app/views/mobile/blogs/index.rhtml @@ -0,0 +1,14 @@ +

Recently updated blogs

+ +<% @entries.each do |entry| %> +

+ <%= link_to entry.user.username, mobile_entries_url(:user_id => entry.user) %>
+ '<%= entry.title %>' was posted <%= time_ago_in_words(entry.created_at) %> ago +

+<% end %> + +<% if @entry_pages.page_count > 1 %> +

Pages: + <%= pagination_links @entry_pages, :params => params %> +

+<% end %> diff --git a/app/views/mobile/entries/index.rhtml b/app/views/mobile/entries/index.rhtml new file mode 100644 index 0000000..c3cc9c9 --- /dev/null +++ b/app/views/mobile/entries/index.rhtml @@ -0,0 +1,32 @@ +

<%= @user.username %>'s blog

+ +<% if @entry_pages.page_count > 1 %> +

+ <% if @entry_pages.current.previous %> + <%= link_to '« Previous', :page => @entry_pages.current.previous %> + <% end %> + <% if @entry_pages.current.next %> + <%= link_to 'Next »', :page => @entry_pages.current.next %> + <% end %> +

+<% end %> + +
    +<% @entries.each do |entry| -%> +
  • + <%= link_to entry.title, mobile_entry_path(:user_id => @user, :id => entry) -%> + (<%= entry.created_at.to_s(:short) %>) +
  • +<% end -%> +
+ +<% if @entry_pages.page_count > 1 %> +

+ <% if @entry_pages.current.previous %> + <%= link_to '« Previous', :page => @entry_pages.current.previous %> + <% end %> + <% if @entry_pages.current.next %> + <%= link_to 'Next »', :page => @entry_pages.current.next %> + <% end %> +

+<% end %> diff --git a/app/views/mobile/entries/show.rhtml b/app/views/mobile/entries/show.rhtml new file mode 100644 index 0000000..f11ad9e --- /dev/null +++ b/app/views/mobile/entries/show.rhtml @@ -0,0 +1,3 @@ +

<%= link_to "#{@user.username}'s blog", mobile_entries_path(:user_id => @user.id) %>

+

<%=h @entry.title %>

+

<%= textilize @entry.body %>

\ No newline at end of file diff --git a/app/views/mobile/forums/index.rhtml b/app/views/mobile/forums/index.rhtml new file mode 100644 index 0000000..1748274 --- /dev/null +++ b/app/views/mobile/forums/index.rhtml @@ -0,0 +1,10 @@ +

Forums

+ +<% @forums.each do |forum| -%> +
+ <%= link_to forum.name, mobile_topics_path(:forum_id => forum) -%> +
+
+ <%= forum.description -%> +
+<% end -%> diff --git a/app/views/mobile/pages/show.rhtml b/app/views/mobile/pages/show.rhtml new file mode 100644 index 0000000..a653249 --- /dev/null +++ b/app/views/mobile/pages/show.rhtml @@ -0,0 +1,2 @@ +

<%= @page.title %>

+

<%= @page.body %>

\ No newline at end of file diff --git a/app/views/mobile/photos/index.rhtml b/app/views/mobile/photos/index.rhtml new file mode 100644 index 0000000..2a7e2b9 --- /dev/null +++ b/app/views/mobile/photos/index.rhtml @@ -0,0 +1,21 @@ +

All Photos

+ +<% if @photo_pages.page_count > 1 %> +

+ <% if @photo_pages.current.previous %> + <%= link_to '« Previous', :page => @photo_pages.current.previous %> + <% end %> + <% if @photo_pages.current.next %> + <%= link_to 'Next »', :page => @photo_pages.current.next %> + <% end %> +

+<% end %> + +
    + <% @photos.each do |photo| -%> +
  • + <%= link_to image_tag(photo.public_filename('tiny')), + mobile_user_photo_path(:user_id => photo.user, :id => photo) %> +
  • + <% end %> +
diff --git a/app/views/mobile/posts/index.rhtml b/app/views/mobile/posts/index.rhtml new file mode 100644 index 0000000..3b396ec --- /dev/null +++ b/app/views/mobile/posts/index.rhtml @@ -0,0 +1,34 @@ +

<%= @topic.name -%>

+ +

+ <%= link_to 'Forums', mobile_forums_path -%> > + <%= link_to @topic.forum.name, mobile_topics_path(:forum_id => @topic.forum) -%> > + <%= @topic.name -%> +

+ +

+<% if is_logged_in? -%> + <%= link_to 'Post Reply', mobile_new_post_path(:forum_id => @topic.forum, :topic_id => @topic) -%> +<% else -%> + <%= link_to 'Login to post a new topic', mobile_login_url -%> +<% end -%> +

+ +<% if @posts_pages.page_count > 1 %> +

Pages: + <%= pagination_links @posts_pages, :params => params %> +

+<% end %> + +<% @posts.each do |post| -%> +

+ <%= link_to post.user.username, mobile_user_path(post.user) -%> said:
+ <%=h post.body -%> +

+<% end -%> + +<% if @posts_pages.page_count > 1 %> +

Pages: + <%= pagination_links @posts_pages, :params => params %> +

+<% end %> diff --git a/app/views/mobile/posts/new.rhtml b/app/views/mobile/posts/new.rhtml new file mode 100644 index 0000000..0013931 --- /dev/null +++ b/app/views/mobile/posts/new.rhtml @@ -0,0 +1,11 @@ +

New Post

+ +<%= error_messages_for :post -%> + +

Topic: <%= @topic.name %>

+<% form_for :post, + :url => mobile_posts_path(:topic_id => @topic, :forum_id => @topic.forum) do |f| -%> +

Message:
<%= f.text_area :body, :rows => 8, :cols => 60 -%>

+ <%= submit_tag 'Save' -%> or + <%= link_to 'Cancel', mobile_topics_path(:id => @topic, :forum_id => @topic.forum) -%> +<% end -%> \ No newline at end of file diff --git a/app/views/mobile/topics/index.rhtml b/app/views/mobile/topics/index.rhtml new file mode 100644 index 0000000..c170cfd --- /dev/null +++ b/app/views/mobile/topics/index.rhtml @@ -0,0 +1,30 @@ +

Forum : <%= @forum.name -%>

+ +

Topics

+ +

+<% if is_logged_in? -%> + <%= link_to 'Post New Topic', mobile_new_topic_path(:forum_id => @forum) -%> +<% else -%> + <%= link_to 'Login to post a new topic', login_url -%> +<% end -%> +

+ +<% if @topics_pages.page_count > 1 %> +

Pages: + <%= pagination_links @topics_pages, :params => params %> +

+<% end %> + +
    +<% @topics.each do |topic| -%> +
  • <%= link_to topic.name, mobile_posts_path(:forum_id => @forum, :topic_id => topic) -%> + (<%= pluralize(topic.posts_count, 'post') -%>)
  • +<% end -%> +
+ +<% if @topics_pages.page_count > 1 %> +

Pages: + <%= pagination_links @topics_pages, :params => params %> +

+<% end %> diff --git a/app/views/mobile/topics/new.rhtml b/app/views/mobile/topics/new.rhtml new file mode 100644 index 0000000..1ef47d0 --- /dev/null +++ b/app/views/mobile/topics/new.rhtml @@ -0,0 +1,11 @@ +

New Topic

+ +<%= error_messages_for :topic -%> +<%= error_messages_for :post -%> + +<% form_for :topic, :url => mobile_topics_path do |f| -%> +

Subject:<%= f.text_field :name -%>

+

Message:<%= text_area :post, :body -%>

+ <%= submit_tag 'Save' %> or + <%= link_to 'cancel', mobile_topics_path(:forum_id => params[:forum_id]) %> +<% end -%> \ No newline at end of file diff --git a/app/views/mobile/user_photos/index.rhtml b/app/views/mobile/user_photos/index.rhtml new file mode 100644 index 0000000..f0f86e9 --- /dev/null +++ b/app/views/mobile/user_photos/index.rhtml @@ -0,0 +1,21 @@ +

<%= @user.username %>'s Photos

+ +<% if @photo_pages.page_count > 1 %> +

+ <% if @photo_pages.current.previous %> + <%= link_to '« Previous', :page => @photo_pages.current.previous %> + <% end %> + <% if @photo_pages.current.next %> + <%= link_to 'Next »', :page => @photo_pages.current.next %> + <% end %> +

+<% end %> + +
    +<% @photos.each do |photo| -%> +
  • + <%= link_to image_tag(photo.public_filename('tiny')), + mobile_user_photo_path(:user_id => photo.user, :id => photo) %> +
  • +<% end %> +
diff --git a/app/views/mobile/user_photos/show.rhtml b/app/views/mobile/user_photos/show.rhtml new file mode 100644 index 0000000..e550684 --- /dev/null +++ b/app/views/mobile/user_photos/show.rhtml @@ -0,0 +1,6 @@ +

<%= link_to "#{@photo.user.username}'s Photos", mobile_user_photos_path(:user_id => @photo.user) %>

+

<%=h @photo.title %>

+ +<%= image_tag @photo.public_filename('thumb'), :id => 'photo' %> + +

<%=h @photo.body %>

diff --git a/app/views/mobile/users/show.rhtml b/app/views/mobile/users/show.rhtml new file mode 100644 index 0000000..dc11dbe --- /dev/null +++ b/app/views/mobile/users/show.rhtml @@ -0,0 +1,22 @@ +

<%= @user.username %>

+

<%=h @user.profile %>

+ +

Blog Entries

+
    +<% @entries.each do |entry| %> +
  • <%= link_to entry.title, mobile_entry_path(:user_id => @user, :id => entry) %> +<% end %> +
+

<%= link_to "See all of #{@user.username}'s blog", mobile_entries_path(:user_id => @user) %>

+ +

Photos

+
    +<% @photos.each do |photo| -%> +
  • + <%= link_to image_tag(photo.public_filename('tiny')), + mobile_user_photo_path(:user_id => photo.user, :id => photo) %> +
  • +<% end %> +
+ +

<%= link_to "See all of #{@user.username}'s photos", mobile_user_photos_path(:user_id => @user) %>

\ No newline at end of file diff --git a/app/views/newsletters/edit.rhtml b/app/views/newsletters/edit.rhtml new file mode 100644 index 0000000..89ced4d --- /dev/null +++ b/app/views/newsletters/edit.rhtml @@ -0,0 +1,24 @@ +

Edit Newsletter

+ +<%= error_messages_for :newsletter %> + +<% form_for(:newsletter, + :url => newsletter_path(@newsletter), + :html => { :method => :put }) do |f| %> +

+ Subject
+ <%= f.text_field :subject, :size => 70 %> +

+ +

+ Body
+ <%= f.text_area :body, :cols => 70, :rows => 25 %> +
+ You can access the user model with @user.
+ e.g. <%= @user.username %> or <%= @user.email %> +

+ +

+ <%= submit_tag "Save draft" %> or <%= link_to 'Cancel', newsletters_path %> +

+<% end %> diff --git a/app/views/newsletters/index.rhtml b/app/views/newsletters/index.rhtml new file mode 100644 index 0000000..857ce01 --- /dev/null +++ b/app/views/newsletters/index.rhtml @@ -0,0 +1,27 @@ +

Listing newsletters

+ +<%= link_to 'Create new newsletter', new_newsletter_path %> + + + + + + + + + +<% for newsletter in @newsletters %> + + + + + + <% if !newsletter.sent %> + + + + <% end %> + +<% end %> +
SubjectSentCreated atUpdated at
<%=h newsletter.subject %><%= yes_no(newsletter.sent) %><%=h newsletter.created_at %><%=h newsletter.updated_at %><%= link_to 'Show', newsletter_path(newsletter) %><%= link_to 'Edit', edit_newsletter_path(newsletter) %><%= link_to 'Destroy', newsletter_path(newsletter), + :confirm => 'Are you sure?', :method => :delete %>
\ No newline at end of file diff --git a/app/views/newsletters/new.rhtml b/app/views/newsletters/new.rhtml new file mode 100644 index 0000000..5a34239 --- /dev/null +++ b/app/views/newsletters/new.rhtml @@ -0,0 +1,22 @@ +

Create New Newsletter

+ +<%= error_messages_for :newsletter %> + +<% form_for(:newsletter, :url => newsletters_path) do |f| %> +

+ Subject
+ <%= f.text_field :subject, :size => 70 %> +

+ +

+ Body
+ <%= f.text_area :body, :cols => 70, :rows => 25 %> +
+ You can access the user model with @user.
+ e.g. <%= @user.username %> or <%= @user.email %> +

+ +

+ <%= submit_tag "Save draft" %> or <%= link_to 'Cancel', newsletters_path %> +

+<% end %> diff --git a/app/views/newsletters/newsletter.text.plain.rhtml b/app/views/newsletters/newsletter.text.plain.rhtml new file mode 100644 index 0000000..bc1c6eb --- /dev/null +++ b/app/views/newsletters/newsletter.text.plain.rhtml @@ -0,0 +1,3 @@ +RailsCoders Newsletter + +<%= render :inline => @body %> diff --git a/app/views/newsletters/show.rhtml b/app/views/newsletters/show.rhtml new file mode 100644 index 0000000..24b6edb --- /dev/null +++ b/app/views/newsletters/show.rhtml @@ -0,0 +1,31 @@ +

Newsletter

+ +

+ Subject: <%=h @newsletter.subject %> +

+ +

+ Created at: <%=h @newsletter.created_at %> +

+ +

+ Updated at: <%=h @newsletter.updated_at %> +

+ +

+ Sent: <%= yes_no(@newsletter.sent) %> +

+ +

+ Body: +
+ <%=h @newsletter.body %> +

+ +<% if !@newsletter.sent %> + <%= link_to 'Edit', edit_newsletter_path(@newsletter) %> | + <%= link_to 'Send', sendmails_newsletter_path(@newsletter), + :method => :put, + :confirm => 'Are you sure you wish to send this newsletter?' %> | +<% end %> +<%= link_to 'Back', newsletters_path %> diff --git a/app/views/notifier/new_comment_notification.text.html.rhtml b/app/views/notifier/new_comment_notification.text.html.rhtml new file mode 100644 index 0000000..bd1366c --- /dev/null +++ b/app/views/notifier/new_comment_notification.text.html.rhtml @@ -0,0 +1,13 @@ +<%= image_tag "http://railscoders.net/images/logo.png", :alt => "RailsCoders" %> + +

Hi <%= @blog_owner.username %>,

+ +

A new comment has been left on your blog at RailsCoders.

+ +

The comment was left by <%= link_to @comment.user.username, @blog_owner_url %> on <%= @comment.created_at.to_s(:short) %>

+ + + +

To read the comment, go to <%= link_to @blog_entry_url, @blog_entry_url %>

+

Cheers,
+The RailsCoders Team

diff --git a/app/views/notifier/new_comment_notification.text.plan.rhtml b/app/views/notifier/new_comment_notification.text.plan.rhtml new file mode 100644 index 0000000..5174670 --- /dev/null +++ b/app/views/notifier/new_comment_notification.text.plan.rhtml @@ -0,0 +1,10 @@ +Hi <%= @blog_owner.username %>, + +A new comment has been left on your blog at RailsCoders.net. + +The comment was left by '<%= @comment.user.username %>' at <%= @comment.created_at.to_s(:short) %>. + +To read the comment, go to <%= @blog_entry_url %>. + +Cheers, +The RailsCoders Team \ No newline at end of file diff --git a/app/views/pages/create.rhtml b/app/views/pages/create.rhtml new file mode 100644 index 0000000..d9aaf48 --- /dev/null +++ b/app/views/pages/create.rhtml @@ -0,0 +1,2 @@ +

Pages#create

+

Find me in app/views/pages/create.rhtml

diff --git a/app/views/pages/destroy.rhtml b/app/views/pages/destroy.rhtml new file mode 100644 index 0000000..381e3ad --- /dev/null +++ b/app/views/pages/destroy.rhtml @@ -0,0 +1,2 @@ +

Pages#destroy

+

Find me in app/views/pages/destroy.rhtml

diff --git a/app/views/pages/edit.rhtml b/app/views/pages/edit.rhtml new file mode 100644 index 0000000..156394e --- /dev/null +++ b/app/views/pages/edit.rhtml @@ -0,0 +1,9 @@ +

Edit Page

+ +<%= error_messages_for :page %> + +<% form_for :page, :url => page_url(@page), :html => {:method=>:put} do |f| -%> +

Title:
<%= f.text_field :title, :size => 60 %>

+

Body:
<%= f.text_area :body, :rows => 20, :cols => 60 %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', pages_url %> +<% end -%> diff --git a/app/views/pages/index.rhtml b/app/views/pages/index.rhtml new file mode 100644 index 0000000..7c1f350 --- /dev/null +++ b/app/views/pages/index.rhtml @@ -0,0 +1,13 @@ +

All Pages

+

<%= link_to 'Create new page', new_page_url %>

+
    + <% @pages.each do |page| %> +
  • + <%= link_to page.permalink, page_path(page) %> + [<%= link_to 'edit', edit_page_path(page) %>] + | <%= link_to 'delete', page_url(page), :method => :delete, + :confirm => 'Are you sure you wish to delete this page?' %> ] + : <%= page.title %> +
  • + <% end %> +
diff --git a/app/views/pages/new.rhtml b/app/views/pages/new.rhtml new file mode 100644 index 0000000..8df58b8 --- /dev/null +++ b/app/views/pages/new.rhtml @@ -0,0 +1,9 @@ +

Create New Page

+ +<%= error_messages_for :page %> + +<% form_for :page, :url => pages_url, :html => { :method => :post } do |f| -%> +

Title:
<%= f.text_field :title, :size => 60 %>

+

Body:
<%= f.text_area :body, :rows => 20, :cols => 60 %>

+ <%= submit_tag 'Save' %> or <%= link_to 'cancel', pages_url %> +<% end -%> diff --git a/app/views/pages/show.rhtml b/app/views/pages/show.rhtml new file mode 100644 index 0000000..864a71a --- /dev/null +++ b/app/views/pages/show.rhtml @@ -0,0 +1,2 @@ +

<%= @page.title %>

+

<%= @page.body %>

diff --git a/app/views/pages/update.rhtml b/app/views/pages/update.rhtml new file mode 100644 index 0000000..b916d88 --- /dev/null +++ b/app/views/pages/update.rhtml @@ -0,0 +1,2 @@ +

Pages#update

+

Find me in app/views/pages/update.rhtml

diff --git a/app/views/photos/_page_links.rhtml b/app/views/photos/_page_links.rhtml new file mode 100644 index 0000000..e84fc86 --- /dev/null +++ b/app/views/photos/_page_links.rhtml @@ -0,0 +1,13 @@ +<% if @photo_pages.page_count > 1 %> + + <% if @photo_pages.current.previous %> + <%= link_to '« Previous', :page => @photo_pages.current.previous %> + <% end %> + + <%= pagination_links @photo_pages, :params => params %> + + <% if @photo_pages.current.next %> + <%= link_to 'Next »', { :page => @photo_pages.current.next } %> + <% end %> + + <% end %> diff --git a/app/views/photos/_photo.rhtml b/app/views/photos/_photo.rhtml new file mode 100644 index 0000000..ba6a7e0 --- /dev/null +++ b/app/views/photos/_photo.rhtml @@ -0,0 +1,4 @@ +
  • + <%= link_to image_tag(photo.public_filename('thumb')), + user_photo_path(:user_id => photo.user, :id => photo) %> +
  • diff --git a/app/views/photos/index.rhtml b/app/views/photos/index.rhtml new file mode 100644 index 0000000..d83e05a --- /dev/null +++ b/app/views/photos/index.rhtml @@ -0,0 +1,9 @@ +

    All Photos

    + +<%= render :partial => 'page_links' %> + +
      + <%= render :partial => 'photo', :collection => @photos %> +
    + +<%= render :partial => 'page_links' %> diff --git a/app/views/posts/_textile_help.rhtml b/app/views/posts/_textile_help.rhtml new file mode 100644 index 0000000..0665d7b --- /dev/null +++ b/app/views/posts/_textile_help.rhtml @@ -0,0 +1,3 @@ +

    You can format your post using Textile.

    + +

    A Textile cheat-sheet can be found here.

    diff --git a/app/views/posts/edit.rhtml b/app/views/posts/edit.rhtml new file mode 100644 index 0000000..91d8705 --- /dev/null +++ b/app/views/posts/edit.rhtml @@ -0,0 +1,14 @@ +

    Edit Post

    +<%= error_messages_for :post -%> +

    Topic: <%= @post.topic.name %>

    +<% form_for :post, :url => post_path(:id => @post, :topic_id => @post.topic, + :forum_id => @post.topic.forum), + :html => {:method => :put} do |f| -%> +

    Message:
    <%= f.text_area :body, :rows => 8, :cols => 60 -%>

    + <%= submit_tag 'Save' -%> or + <%= link_to 'Cancel', topics_path(:id => @post.topic, + :forum_id => @post.topic.forum) -%> +<% end -%> + +
    +<%= render :partial => 'textile_help' %> \ No newline at end of file diff --git a/app/views/posts/index.rhtml b/app/views/posts/index.rhtml new file mode 100644 index 0000000..d6c4cc4 --- /dev/null +++ b/app/views/posts/index.rhtml @@ -0,0 +1,64 @@ +

    <%= @topic.name -%>

    + +

    + <%= link_to 'Forums', forums_path -%> > + <%= link_to @topic.forum.name, forum_path(@topic.forum) -%> > + <%= @topic.name -%> +

    + +

    +<% if is_logged_in? -%> + <%= link_to 'Post Reply', new_post_path(:forum_id => @topic.forum, + :topic_id => @topic) -%> +<% else -%> + <%= link_to 'Login to post a new topic', :controller => 'account', :action => 'login' -%> +<% end -%> +

    + +<% if @posts_pages.page_count > 1 %> +

    Pages: + <%= pagination_links @posts_pages, :params => params %> +

    +<% end %> + + + + + + + <% @posts.each do |post| -%> + + + + + <% end -%> +
    AuthorMessage
    + <%= link_to post.user.username, user_path(post.user) -%>
    + + Member since <%= post.user.created_at.to_s(:short) %>
    + <%= pluralize(post.user.posts_count, 'post') -%> +
    + <% if is_logged_in? and logged_in_user.has_role?('Moderator') -%> +
    + + <%= link_to 'Edit', edit_post_path(:id => post, + :topic_id => @topic, :forum_id => @topic.forum) -%> +
    + <%= link_to 'Delete', post_path(:id => post, :topic_id => @topic, + :forum_id => @topic.forum), :method => :delete, + :confirm => 'Are you sure you wish to delete this post?' -%> +
    + <% end -%> + <% if is_logged_in? and post.user == logged_in_user %> +
    + + <%= link_to 'Edit this post', edit_post_path(:id => post, :topic_id => @topic, :forum_id => @topic.forum) %> + + <% end %> +
    <%= textilize(post.body) -%>
    + +<% if @posts_pages.page_count > 1 %> +

    Pages: + <%= pagination_links @posts_pages, :params => params %> +

    +<% end %> diff --git a/app/views/posts/new.rhtml b/app/views/posts/new.rhtml new file mode 100644 index 0000000..b9fe253 --- /dev/null +++ b/app/views/posts/new.rhtml @@ -0,0 +1,14 @@ +

    New Post

    +<%= error_messages_for :post -%> + +

    Topic: <%= @topic.name %>

    + +<% form_for :post, :url => posts_path(:forum_id => @topic.forum, + :topic_id => @topic) do |f| -%> +

    Message:
    <%= f.text_area :body, :rows => 8, :cols => 60 -%>

    + <%= submit_tag 'Save' -%> or + <%= link_to 'Cancel', topics_path(:id => @topic, :forum_id => @topic.forum) -%> +<% end -%> + +
    +<%= render :partial => 'textile_help' %> \ No newline at end of file diff --git a/app/views/posts/show.rhtml b/app/views/posts/show.rhtml new file mode 100644 index 0000000..f1b35c2 --- /dev/null +++ b/app/views/posts/show.rhtml @@ -0,0 +1,3 @@ + +<%= link_to 'Edit', edit_post_path(@post) %> | +<%= link_to 'Back', posts_path %> \ No newline at end of file diff --git a/app/views/roles/_role.rhtml b/app/views/roles/_role.rhtml new file mode 100644 index 0000000..7a612d0 --- /dev/null +++ b/app/views/roles/_role.rhtml @@ -0,0 +1,12 @@ +
  • + <%= role.name %> + [<% if @user.has_role?(role.name) %> + <%= link_to 'remove role', + role_url(:id => role.id, :user_id => @user.id), + :method => :delete %> + <% else %> + <%= link_to 'assign role', + role_url(:id => role.id, :user_id => @user.id), + :method => :put %> + <% end %>] +
  • diff --git a/app/views/roles/index.rhtml b/app/views/roles/index.rhtml new file mode 100644 index 0000000..770298e --- /dev/null +++ b/app/views/roles/index.rhtml @@ -0,0 +1,7 @@ +

    Roles for <%= @user.username %>

    + +

    Roles assigned:

    +
      <%= render :partial => 'role', :collection => @user.roles %>
    + +

    Roles available:

    +
      <%= render :partial => 'role', :collection => (@all_roles - @user.roles) %>
    diff --git a/app/views/tags/index.rhtml b/app/views/tags/index.rhtml new file mode 100644 index 0000000..eafe3a8 --- /dev/null +++ b/app/views/tags/index.rhtml @@ -0,0 +1,5 @@ +

    Most Popular Tags

    + +<% tag_cloud @tags, %w(tag1 tag2 tag3 tag4 tag5) do |name, css_class| %> + <%= link_to name, :controller => 'tags', :action => 'show', :id => name, :class => css_class %> +<% end %> \ No newline at end of file diff --git a/app/views/tags/show.rhtml b/app/views/tags/show.rhtml new file mode 100644 index 0000000..be5918f --- /dev/null +++ b/app/views/tags/show.rhtml @@ -0,0 +1,5 @@ +

    Photos Tagged: <%=h params[:id] %>

    + +
      + <%= render :partial => 'photos/photo', :collection => @photos %> +
    \ No newline at end of file diff --git a/app/views/topics/edit.rhtml b/app/views/topics/edit.rhtml new file mode 100644 index 0000000..dfb7127 --- /dev/null +++ b/app/views/topics/edit.rhtml @@ -0,0 +1,11 @@ +

    Editing topic

    + +<%= error_messages_for :topic %> + + <% form_for(:topic, :url => topic_path(:id => @topic, :forum_id => @topic.forum), + :html => { :method => :put }) do |f| %> +

    Subject:
    <%= f.text_field :name, :size => 40 -%>

    +

    + <%= submit_tag "Update" %> +

    +<% end %> \ No newline at end of file diff --git a/app/views/topics/index.rhtml b/app/views/topics/index.rhtml new file mode 100644 index 0000000..53a9ff2 --- /dev/null +++ b/app/views/topics/index.rhtml @@ -0,0 +1,51 @@ +

    Forum : <%= @forum.name -%>

    + +

    Topics

    + +

    +<% if is_logged_in? -%> + <%= link_to 'Post New Topic', new_topic_path(:forum_id => @forum) -%> +<% else -%> + <%= link_to 'Login to post a new topic', :controller => 'account', :action => 'login' -%> +<% end -%> +

    + +<% if @topics_pages.page_count > 1 %> +

    Pages: + <%= pagination_links @topics_pages, :params => params %> +

    +<% end %> + + + + + + + + <% @topics.each do |topic| -%> + + + + + + <% end -%> +
    TopicsPostsAuthor
    + <%= link_to topic.name, posts_path(:forum_id => @forum, + :topic_id => topic) -%> + <% if is_logged_in? and logged_in_user.has_role?('moderator') -%> +
    + + <%= link_to 'delete', topic_path(:forum_id => @forum, :id => topic), + :method => :delete, + :confirm => 'Are you sure? This will delete this entire topic.' -%> + <%= link_to 'edit', edit_topic_path(:forum_id => @forum, :id => topic) -%> + + <% end -%> +
    <%= topic.posts_count %><%= link_to topic.user.username, + user_path(:id => topic.user) %>
    + +<% if @topics_pages.page_count > 1 %> +

    Pages: + <%= pagination_links @topics_pages, :params => params %> +

    +<% end %> diff --git a/app/views/topics/new.rhtml b/app/views/topics/new.rhtml new file mode 100644 index 0000000..9be4d88 --- /dev/null +++ b/app/views/topics/new.rhtml @@ -0,0 +1,6 @@ +

    New Topic

    +<% form_for :topic, :url => topics_path do |f| -%> +

    Subject:
    <%= f.text_field :name, :size => 40 -%>

    +

    First Post:
    <%= text_area :post, :body, :rows => 8, :cols => 60 -%>

    + <%= submit_tag 'Save' %> +<% end -%> diff --git a/app/views/topics/show.rhtml b/app/views/topics/show.rhtml new file mode 100644 index 0000000..98e37c2 --- /dev/null +++ b/app/views/topics/show.rhtml @@ -0,0 +1,3 @@ + +<%= link_to 'Edit', edit_topic_path(@topic) %> | +<%= link_to 'Back', topics_path %> \ No newline at end of file diff --git a/app/views/user_photos/_edit_tag.rhtml b/app/views/user_photos/_edit_tag.rhtml new file mode 100644 index 0000000..6c6f790 --- /dev/null +++ b/app/views/user_photos/_edit_tag.rhtml @@ -0,0 +1,9 @@ +
  • + <%= edit_tag.name %> + + [<%= link_to_remote 'delete', + :url => user_remove_tag_photo_path(:id => @photo.id, + :tag_id => edit_tag.id), + :method => :delete %>] + +
  • \ No newline at end of file diff --git a/app/views/user_photos/add_tag.rjs b/app/views/user_photos/add_tag.rjs new file mode 100644 index 0000000..e86b394 --- /dev/null +++ b/app/views/user_photos/add_tag.rjs @@ -0,0 +1,3 @@ +page.insert_html :bottom, 'taglist', { :partial => 'edit_tag', + :locals => {:edit_tag => @new_tag} } +page.visual_effect :highlight, "tag-#{@new_tag.id}", :duration => 2 \ No newline at end of file diff --git a/app/views/user_photos/edit.rhtml b/app/views/user_photos/edit.rhtml new file mode 100644 index 0000000..fff26b6 --- /dev/null +++ b/app/views/user_photos/edit.rhtml @@ -0,0 +1,40 @@ +<% content_for :head do %> + <%= GMap.header %> +<% end %> + +

    Edit photo details

    + +<%= link_to image_tag(@photo.public_filename('thumb')), + user_photo_path(:user_id => @photo.user, :id => @photo) %> + +<%= error_messages_for :photo %> + +

    Tags

    +
      + <%= render :partial => 'edit_tag', :collection => @photo.tags %> +
    + +<% remote_form_for(:tag, + :url => user_add_tag_photo_path(:id => @photo), + :method => :put, + :complete => "Field.clear('tag-name')") do |f| %> + <%= f.text_field :name, :id => 'tag-name' %> + <%= submit_tag 'Add Tag' %> +<% end %> + +<% form_for(:photo, + :url => user_photo_path(:user_id => @photo.user, :id => @photo), + :html => { :method => :put }) do |f| %> +

    Title:
    <%= f.text_field 'title' %>

    +

    Description:
    <%= f.text_area 'body', :rows => 6, :cols => 40 %>

    +

    Latitude: <%= f.text_field 'geo_lat', :size => '8' %>

    +

    Longitude: <%= f.text_field 'geo_long', :size => '8' %>

    +

    Display Location Data? <%= f.check_box 'show_geo' %>

    + +
    + <%= @map.to_html %> + <%= @map.div(:width => 650, :height => 300) %> +
    + +

    <%= submit_tag "Save" %> or <%= link_to 'cancel', user_photos_path %>

    +<% end %> diff --git a/app/views/user_photos/index.rhtml b/app/views/user_photos/index.rhtml new file mode 100644 index 0000000..b8cdc77 --- /dev/null +++ b/app/views/user_photos/index.rhtml @@ -0,0 +1,13 @@ +

    <%= @user.username %>'s Photos

    + +<% if is_logged_in? && logged_in_user == @user %> +

    <%= link_to 'Upload a new photo', user_new_photo_path() %>

    +<% end %> + +<%= render :partial => 'photos/page_links' %> + +
      + <%= render :partial => 'photos/photo', :collection => @photos %> +
    + +<%= render :partial => 'photos/page_links' %> diff --git a/app/views/user_photos/new.rhtml b/app/views/user_photos/new.rhtml new file mode 100644 index 0000000..a8ef860 --- /dev/null +++ b/app/views/user_photos/new.rhtml @@ -0,0 +1,29 @@ +<% content_for :head do %> + <%= GMap.header %> +<% end %> + +

    Upload a new photo

    + +<%= error_messages_for :photo %> + +<% form_for(:photo, + :url => user_photos_path(:user_id => @logged_in_user), + :html => { :multipart => true }) do |f| %> +

    Select a photo to upload

    +

    Title:
    <%= f.text_field 'title' %>

    +

    Description:
    <%= f.text_area 'body', :rows => 6, :cols => 40 %>

    +

    Latitude: <%= f.text_field 'geo_lat', :size => '8' %>

    +

    Longitude: <%= f.text_field 'geo_long', :size => '8' %>

    +

    Display Location Data? <%= f.check_box 'show_geo' %>

    +

    Photo:
    <%= f.file_field 'uploaded_data' %>

    + +
    + <%= @map.to_html %> + <%= @map.div(:width => 650, :height => 300) %> +
    + +

    + <%= submit_tag 'Upload Photo' %> or + <%= link_to 'cancel', user_photos_path(@logged_in_user) %> +

    +<% end %> diff --git a/app/views/user_photos/remove_tag.rjs b/app/views/user_photos/remove_tag.rjs new file mode 100644 index 0000000..723a5d5 --- /dev/null +++ b/app/views/user_photos/remove_tag.rjs @@ -0,0 +1,2 @@ +page.remove "tag-#{@tag_to_delete.id}" +page.visual_effect :highlight, 'taglist', :duration => 2 \ No newline at end of file diff --git a/app/views/user_photos/show.rhtml b/app/views/user_photos/show.rhtml new file mode 100644 index 0000000..20da32f --- /dev/null +++ b/app/views/user_photos/show.rhtml @@ -0,0 +1,41 @@ +<% content_for :head do %> + <%= GMap.header %> +<% end %> + +

    + <%= link_to "#{@photo.user.username}'s Photos", + user_photos_path(:user_id => @photo.user) %> +

    +

    <%=h @photo.title %>

    +

    <%=h @photo.body %> + +

    Tags: + <% @photo.tags.each do |tag| %> + <%= link_to tag.name, user_tag_path(@photo.user, tag.name)%> + <% end %> +

    + +<% if is_logged_in? && @photo.user_id == logged_in_user.id %> +

    + <%= link_to 'Edit', user_edit_photo_path(:user_id=>@photo.user, :id=>@photo) %>, + <%= link_to 'Delete', user_photo_path(:user_id => @photo.user, :id => @photo), + :confirm => 'Are you sure?', + :method => :delete %> +

    +<% end %> + +<% if @map %> +
    + <%= link_to_function 'Hide Map', + "Element.hide('gmap'); Element.show('showmaplink')" %> + <%= @map.to_html %> + <%= @map.div(:width => 650, :height => 200) %> +
    +
    + <%= link_to_function 'Show Map', + "Element.show('gmap'); Element.hide('showmaplink')", + :id => 'showmaplink', + :style => 'display:none' %> +<% end %> + +<%= image_tag @photo.public_filename, :id => 'photo' %> diff --git a/app/views/user_tags/index.rhtml b/app/views/user_tags/index.rhtml new file mode 100644 index 0000000..b42f248 --- /dev/null +++ b/app/views/user_tags/index.rhtml @@ -0,0 +1,7 @@ +

    <%= @user.username %>'s Most Popular Tags

    + +

    <%= link_to "Show all user's tags", tags_path %>

    + +<% tag_cloud @tags, %w(tag1 tag2 tag3 tag4 tag5) do |name, css_class| %> + <%= link_to name, tag_path(name), :class => css_class %> +<% end %> \ No newline at end of file diff --git a/app/views/user_tags/show.rhtml b/app/views/user_tags/show.rhtml new file mode 100644 index 0000000..9171bfc --- /dev/null +++ b/app/views/user_tags/show.rhtml @@ -0,0 +1,9 @@ +

    <%= @user.username %>'s Photos Tagged: <%=h params[:id] %>

    + +

    + <%= link_to "Show all photos tagged with #{h params[:id]}", tag_path(h(params[:id])) %> +

    + +
      + <%= render :partial => 'photos/photo', :collection => @photos %> +
    \ No newline at end of file diff --git a/app/views/users/_user.rhtml b/app/views/users/_user.rhtml new file mode 100644 index 0000000..6aa721b --- /dev/null +++ b/app/views/users/_user.rhtml @@ -0,0 +1,14 @@ + + <%= user.username -%> + <%= user.email -%> + <%= user.enabled ? 'yes' : 'no' -%> + <% unless user == logged_in_user -%> + <% if user.enabled -%> + [<%= link_to('disable', user_url(user.id), :method => :delete) %>] + <% else -%> + [<%= link_to('enable', enable_user_url(user.id), :method => :put) %>] + <% end -%> + <% end -%> + + [<%= link_to 'edit permissions', roles_url(user) %>] + diff --git a/app/views/users/create.rhtml b/app/views/users/create.rhtml new file mode 100644 index 0000000..3c75fcc --- /dev/null +++ b/app/views/users/create.rhtml @@ -0,0 +1,2 @@ +

    Users#create

    +

    Find me in app/views/users/create.rhtml

    diff --git a/app/views/users/destroy.rhtml b/app/views/users/destroy.rhtml new file mode 100644 index 0000000..69d377b --- /dev/null +++ b/app/views/users/destroy.rhtml @@ -0,0 +1,2 @@ +

    Users#destroy

    +

    Find me in app/views/users/destroy.rhtml

    diff --git a/app/views/users/edit.rhtml b/app/views/users/edit.rhtml new file mode 100644 index 0000000..060a81c --- /dev/null +++ b/app/views/users/edit.rhtml @@ -0,0 +1,16 @@ +

    Edit your account

    + +<%= error_messages_for :user %> + +<% form_for :user, + :url => user_url(@user), + :html => { :method => :put } do |f| -%> +

    Email:
    <%= f.text_field :email, :size => 60 %>

    +

    Flickr Username:
    <%= f.text_field :flickr_username, :size => 60 %>

    +

    Password:
    <%= f.password_field :password, :size => 60 %>

    +

    Password Confirmation:
    + <%= f.password_field :password_confirmation, :size => 60 %>

    +

    Blog Title:
    <%= f.text_field :blog_title, :size => 60 %>

    +

    Profile:
    <%= f.text_area :profile, :rows => 6, :cols => 60 %>

    + <%= submit_tag 'Save' %> +<% end -%> diff --git a/app/views/users/enable.rhtml b/app/views/users/enable.rhtml new file mode 100644 index 0000000..c428d41 --- /dev/null +++ b/app/views/users/enable.rhtml @@ -0,0 +1,2 @@ +

    Users#enable

    +

    Find me in app/views/users/enable.rhtml

    diff --git a/app/views/users/index.rhtml b/app/views/users/index.rhtml new file mode 100644 index 0000000..1d4bbab --- /dev/null +++ b/app/views/users/index.rhtml @@ -0,0 +1,11 @@ +

    All users

    + + + + + + + + + <%= render :partial => 'user', :collection => @users %> +
    UsernameEmailEnabled?Roles
    diff --git a/app/views/users/new.rhtml b/app/views/users/new.rhtml new file mode 100644 index 0000000..6abc31b --- /dev/null +++ b/app/views/users/new.rhtml @@ -0,0 +1,13 @@ +

    Signup

    + +<%= error_messages_for :user %> + +<% form_for :user, :url => users_path do |f| -%> +

    Username:
    <%= f.text_field :username, :size => 40 %>

    +

    Email:
    <%= f.text_field :email, :size => 60 %>

    +

    Password:
    <%= f.password_field :password, :size => 60 %>

    +

    Password Confirmation:
    + <%= f.password_field :password_confirmation, :size => 60 %>

    +

    Profile:
    <%= f.text_area :profile, :rows => 6, :cols => 60 %>

    + <%= submit_tag 'Sign Up' %> +<% end -%> diff --git a/app/views/users/show.rhtml b/app/views/users/show.rhtml new file mode 100644 index 0000000..b0076bc --- /dev/null +++ b/app/views/users/show.rhtml @@ -0,0 +1,58 @@ +

    User: <%= @user.username %>

    + +

    Member since <%= @user.created_at.to_s(:long) %>

    +

    <%= @user.profile %>

    + +

    Blog Entries

    +
      + <% for entry in @entries %> +
    • + <%= link_to entry.title, + entry_path(:user_id => @user, :id => entry) %> +
    • + <% end %> +
    +

    + <%= link_to "See all of #{@user.username}'s blog", + entries_path(:user_id => @user) %> +

    + +

    Photos

    +
      + <%= render :partial => 'photos/photo', :collection => @photos %> +
    +

    + <% if is_logged_in? and @user.id != logged_in_user.id %> + <% if logged_in_user.friends.include?(@user) %> + <%= @user.username %> is your friend + <% else %> + <%= link_to "Add #{@user.username} as a friend", + new_friend_path(:user_id => logged_in_user, + :friend_id=>@user) %> + <% end %> +
    + <% end %> + <%= link_to "See all of #{@user.username}'s photos", + user_photos_path(:user_id => @user) %> +

    + +

    + <%= link_to "#{@user.username}'s Tags", + user_tags_path(:user_id => @user) %> +

    + +<% if @flickr_feed %> +

    Flickr Photos

    +

    +

      + <% @flickr_feed.each do |item| %> + <% if !item.nil? %> +
    • + <%= image_tag(item) %> +
    • + <% end %> + <% end %> +
    + <%= link_to "See more", "http://flickr.com/photos/#{@user.flickr_id}" %> +

    +<% end %> \ No newline at end of file diff --git a/app/views/users/update.rhtml b/app/views/users/update.rhtml new file mode 100644 index 0000000..a58d1d5 --- /dev/null +++ b/app/views/users/update.rhtml @@ -0,0 +1,2 @@ +

    Users#update

    +

    Find me in app/views/users/update.rhtml

    diff --git a/app/views/usertemplates/edit.rhtml b/app/views/usertemplates/edit.rhtml new file mode 100644 index 0000000..4e6c243 --- /dev/null +++ b/app/views/usertemplates/edit.rhtml @@ -0,0 +1,12 @@ +

    Editing <%= @usertemplate.name %>

    + +<%= error_messages_for :usertemplate %> + +<% form_for(:usertemplate, + :url => usertemplate_path(@usertemplate), + :html => { :method => :put }) do |f| %> +

    <%= f.text_area :body, :rows => 25, :cols => 80 %> +

    + <%= submit_tag "Save" %> or <%= link_to 'cancel', usertemplates_path %> +

    +<% end %> \ No newline at end of file diff --git a/app/views/usertemplates/index.rhtml b/app/views/usertemplates/index.rhtml new file mode 100644 index 0000000..1a7ee43 --- /dev/null +++ b/app/views/usertemplates/index.rhtml @@ -0,0 +1,17 @@ +

    Your Blog Templates

    + + + + + + + + + + + + + +
    Template NameDescription
    <%= link_to 'blog_index', edit_usertemplate_path( + @usertemplates.find {|ut| ut.name == 'blog_index'} ) -%>The main template for your blog
    <%= link_to 'blog_entry', edit_usertemplate_path( + @usertemplates.find {|ut| ut.name == 'blog_entry'} ) -%>The template for viewing one entry
    \ No newline at end of file diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..b7af0c3 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,45 @@ +# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb + +unless defined?(RAILS_ROOT) + root_path = File.join(File.dirname(__FILE__), '..') + + unless RUBY_PLATFORM =~ /(:?mswin|mingw)/ + require 'pathname' + root_path = Pathname.new(root_path).cleanpath(true).to_s + end + + RAILS_ROOT = root_path +end + +unless defined?(Rails::Initializer) + if File.directory?("#{RAILS_ROOT}/vendor/rails") + require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" + else + require 'rubygems' + + environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join + environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/ + rails_gem_version = $1 + + if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version + # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems + rails_gem = Gem.cache.search('rails', "~>#{version}.0").sort_by { |g| g.version.version }.last + + if rails_gem + gem "rails", "=#{rails_gem.version.version}" + require rails_gem.full_gem_path + '/lib/initializer' + else + STDERR.puts %(Cannot find gem for Rails ~>#{version}.0: + Install the missing gem with 'gem install -v=#{version} rails', or + change environment.rb to define RAILS_GEM_VERSION with your desired version. + ) + exit 1 + end + else + gem "rails" + require 'initializer' + end + end + + Rails::Initializer.run(:set_load_path) +end diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..dce2064 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,33 @@ +# MySQL (default setup). Versions 4.1 and 5.0 are recommended. +# +# Install the MySQL driver: +# gem install mysql +# On MacOS X: +# gem install mysql -- --include=/usr/local/lib +# On Windows: +# gem install mysql +# Choose the win32 build. +# Install MySQL and put its /bin directory on your path. +# +# And be sure to use new-style password hashing: +# http://dev.mysql.com/doc/refman/5.0/en/old-client.html +development: + adapter: mysql + database: railscoders_development + username: root + password: + +# Warning: The database defined as 'test' will be erased and +# re-generated from your development database when you run 'rake'. +# Do not set this db to the same as development or production. +test: + adapter: mysql + database: railscoders_test + username: root + password: + +production: + adapter: mysql + database: railscoders_production + username: root + password: diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..3204f5b --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,62 @@ +# Be sure to restart your web server when you modify this file. + +# Uncomment below to force Rails into production mode when +# you don't control web/app server and can't set it the proper way +# ENV['RAILS_ENV'] ||= 'production' + +# Specifies gem version of Rails to use when vendor/rails is not present +RAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +Rails::Initializer.run do |config| + # Settings in config/environments/* take precedence over those specified here + + # Skip frameworks you're not going to use (only works if using vendor/rails) + # config.frameworks -= [ :action_web_service, :action_mailer ] + + # Only load the plugins named here, by default all plugins in vendor/plugins are loaded + # config.plugins = %W( exception_notification ssl_requirement ) + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/extras ) + config.load_paths += %W( #{RAILS_ROOT}/app/drops #{RAILS_ROOT}/app/filters ) + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Use the database for sessions instead of the file system + # (create the session table with 'rake db:sessions:create') + config.action_controller.session_store = :active_record_store + + # Use SQL instead of Active Record's schema dumper when creating the test database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector + + # Make Active Record use UTC-base instead of local time + # config.active_record.default_timezone = :utc + + # See Rails::Configuration for more options +end + +# Add new inflection rules using the following format +# (all these examples are active by default): +# Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register "application/x-mobile", :mobile + +# Include your application configuration below +require 'action_mailer/ar_mailer' \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..a9b7dc7 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,22 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# In the development environment your application's code is reloaded on +# every request. This slows down response time but is perfect for development +# since you don't have to restart the webserver when you make code changes. +config.cache_classes = false + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Enable the breakpoint server that script/breakpointer connects to +config.breakpoint_server = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false +config.action_view.cache_template_extensions = false +config.action_view.debug_rjs = true + +# Don't care if the mailer can't send +config.action_mailer.raise_delivery_errors = false +config.action_mailer.delivery_method = :activerecord \ No newline at end of file diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..cb295b8 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,18 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests +config.cache_classes = true + +# Use a different logger for distributed setups +# config.logger = SyslogLogger.new + +# Full error reports are disabled and caching is turned on +config.action_controller.consider_all_requests_local = false +config.action_controller.perform_caching = true + +# Enable serving of images, stylesheets, and javascripts from an asset server +# config.action_controller.asset_host = "http://assets.example.com" + +# Disable delivery errors, bad email addresses will be ignored +# config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..f0689b9 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,19 @@ +# Settings specified here will take precedence over those in config/environment.rb + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! +config.cache_classes = true + +# Log error messages when you accidentally call methods on nil. +config.whiny_nils = true + +# Show full error reports and disable caching +config.action_controller.consider_all_requests_local = true +config.action_controller.perform_caching = false + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +config.action_mailer.delivery_method = :test \ No newline at end of file diff --git a/config/gmaps_api_key.yml b/config/gmaps_api_key.yml new file mode 100644 index 0000000..0f04b8d --- /dev/null +++ b/config/gmaps_api_key.yml @@ -0,0 +1,13 @@ +#Fill here the Google Maps API keys for your application +#In this sample: +#For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode. +#In production, the app can be accessed through 2 different hosts: thepochisuperstarmegashow.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test). + +development: + ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ + +test: + ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ + +production: + ABQIAAAAEItd7Z_b-RngMPi6_lNGahQRn07cXBjXICK-jcvMUuW5kxxwuxSClyd89qWFv6jC7K-r3JJfnjvKnQ \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..a31819a --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,134 @@ +ActionController::Routing::Routes.draw do |map| + # The priority is based upon order of creation: first created -> highest priority. + + # Sample of regular route: + # map.connect 'products/:id', :controller => 'catalog', :action => 'view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' + # This route can be invoked with purchase_url(:id => product.id) + + # You can have the root of your site routed by hooking up '' + # -- just remember to delete public/index.html. + # map.connect '', :controller => "welcome" + + # Allow downloading Web Service WSDL as a file with an extension + # instead of a file named 'wsdl' + map.connect ':controller/service.wsdl', :action => 'wsdl' + + + # RailsCoders routes + map.index '/', :controller => 'pages', + :action => 'show', + :id => '1-welcome-page' + + map.resources :pages + map.resources :blogs + map.resources :photos + map.resources :tags + map.resources :usertemplates + + map.resources :users, :member => { :enable => :put } do |users| + users.resources :roles + users.resources :entries do |entry| + entry.resources :comments + end + users.resources :friends + users.resources :tags, :name_prefix => 'user_', + :controller => 'user_tags' + users.resources :photos, :name_prefix => 'user_', + :controller => 'user_photos', + :member => { :add_tag => :put, :remove_tag => :delete } + end + + map.resources :articles, :collection => {:admin => :get} + + map.resources :categories, :collection => {:admin => :get} do |categories| + categories.resources :articles, :name_prefix => 'category_' + end + + map.resources :forums do |forum| + forum.resources :topics do |topic| + topic.resources :posts + end + end + + map.resources :newsletters, :member => { :sendmails => :put } + + map.show_user '/user/:username', + :controller => 'users', + :action => 'show_by_username' + + + # Mobile Routes + + map.resources :pages, + :controller => 'mobile/pages', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' + + map.resources :articles, + :controller => 'mobile/articles', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' + + map.resources :blogs, + :controller => 'mobile/blogs', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' + + map.resources :photos, + :controller => 'mobile/photos', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' + + map.resources :categories, + :controller => 'mobile/categories', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' do |categories| + categories.resources :articles, + :controller => 'mobile/articles', + :name_prefix => 'mobile_category_' + end + + map.resources :users, + :controller => 'mobile/users', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' do |users| + users.resources :photos, + :controller => 'mobile/user_photos', + :name_prefix => 'mobile_user_' + users.resources :entries, + :controller => 'mobile/entries', + :name_prefix => 'mobile_' + end + + map.resources :forums, + :controller => 'mobile/forums', + :path_prefix => '/mobile', + :name_prefix => 'mobile_' do |forums| + forums.resources :topics, + :controller => 'mobile/topics', + :name_prefix => 'mobile_' do |topics| + topics.resources :posts, + :controller => 'mobile/posts', + :name_prefix => 'mobile_' + end + end + + map.mobile_index '/mobile', :controller => 'mobile/pages', + :action => 'show', + :id => "1" + + map.mobile_show_user '/mobile/user/:username', :controller => 'mobile/users', :action => 'show_by_username' + map.mobile_all_blogs '/mobile/blogs', :controller => 'mobile/blogs', :action => 'index' + map.mobile_all_photos '/mobile/photos', :controller => 'mobile/photos', :action => 'index' + + map.mobile_login '/mobile/login', :controller => 'mobile/account', :action => 'login' + map.mobile_logout '/mobile/logout', :controller => 'mobile/account', :action => 'logout' + + # Install the default route as the lowest priority. + map.connect ':controller/:action/:id.:format' + map.connect ':controller/:action/:id' +end diff --git a/db/migrate/001_create_pages.rb b/db/migrate/001_create_pages.rb new file mode 100644 index 0000000..565d5c8 --- /dev/null +++ b/db/migrate/001_create_pages.rb @@ -0,0 +1,19 @@ +class CreatePages < ActiveRecord::Migration + def self.up + create_table :pages do |t| + t.column :title, :string + t.column :permalink, :string + t.column :body, :text + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + + Page.create(:title => "RailsCoders Home", + :permalink => "welcome-page", + :body => "Welcome to RailsCoders") + end + + def self.down + drop_table :pages + end +end diff --git a/db/migrate/002_create_users.rb b/db/migrate/002_create_users.rb new file mode 100644 index 0000000..4ac0498 --- /dev/null +++ b/db/migrate/002_create_users.rb @@ -0,0 +1,19 @@ +class CreateUsers < ActiveRecord::Migration + def self.up + create_table :users do |t| + t.column :username, :string, :limit => 64, :null => false + t.column :email, :string, :limit => 128, :null => false + t.column :hashed_password, :string, :limit => 64 + t.column :enabled, :boolean, :default => true, :null => false + t.column :profile, :text + t.column :created_at, :datetime + t.column :updated_at, :datetime + t.column :last_login_at, :datetime + end + add_index :users, :username + end + + def self.down + drop_table :users + end +end \ No newline at end of file diff --git a/db/migrate/003_create_roles.rb b/db/migrate/003_create_roles.rb new file mode 100644 index 0000000..13e81ae --- /dev/null +++ b/db/migrate/003_create_roles.rb @@ -0,0 +1,13 @@ +class CreateRoles < ActiveRecord::Migration + def self.up + create_table :roles do |t| + t.column :name, :string + end + + Role.create(:name => 'Administrator') + end + + def self.down + drop_table :roles + end +end diff --git a/db/migrate/004_create_roles_users_join.rb b/db/migrate/004_create_roles_users_join.rb new file mode 100644 index 0000000..d9b5835 --- /dev/null +++ b/db/migrate/004_create_roles_users_join.rb @@ -0,0 +1,21 @@ +class CreateRolesUsersJoin < ActiveRecord::Migration + def self.up + create_table :roles_users, :id => false do |t| + t.column :role_id, :integer, :null => false + t.column :user_id, :integer, :null => false + end + + admin_user = User.create(:username => 'Admin', + :email => 'admin@railscoders.net', + :profile => 'Site Administrator', + :password => 'admin', + :password_confirmation => 'admin') + admin_role = Role.find_by_name('Administrator') + admin_user.roles << admin_role + end + + def self.down + drop_table :roles_users + User.find_by_username('Admin').destroy + end +end diff --git a/db/migrate/005_create_articles.rb b/db/migrate/005_create_articles.rb new file mode 100644 index 0000000..88f2d01 --- /dev/null +++ b/db/migrate/005_create_articles.rb @@ -0,0 +1,19 @@ +class CreateArticles < ActiveRecord::Migration + def self.up + create_table :articles do |t| + t.column :user_id, :integer + t.column :title, :string + t.column :synopsis, :text, :limit => 1000 + t.column :body, :text, :limit => 20000 + t.column :published, :boolean, :default => false + t.column :created_at, :datetime + t.column :updated_at, :datetime + t.column :published_at, :datetime + t.column :category_id, :integer + end + end + + def self.down + drop_table :articles + end +end diff --git a/db/migrate/006_create_categories.rb b/db/migrate/006_create_categories.rb new file mode 100644 index 0000000..b0dd93b --- /dev/null +++ b/db/migrate/006_create_categories.rb @@ -0,0 +1,14 @@ +class CreateCategories < ActiveRecord::Migration + def self.up + create_table :categories do |t| + t.column :name, :string + end + news_category = Category.create(:name => 'Site News') + change_column :articles, :category_id, :integer, :default => news_category + end + + def self.down + change_column :articles, :category_id, :integer, :default => 0 + drop_table :categories + end +end diff --git a/db/migrate/007_add_editor_role.rb b/db/migrate/007_add_editor_role.rb new file mode 100644 index 0000000..4055107 --- /dev/null +++ b/db/migrate/007_add_editor_role.rb @@ -0,0 +1,14 @@ +class AddEditorRole < ActiveRecord::Migration + def self.up + editor_role = Role.create(:name => 'Editor') + admin_user = User.find_by_username('Admin') + admin_user.roles << editor_role + end + + def self.down + editor_role = Role.find_by_name('Editor') + admin_user = User.find_by_username('Admin') + admin_user.roles.delete(editor_role) + editor_role.destroy + end +end diff --git a/db/migrate/008_create_forums.rb b/db/migrate/008_create_forums.rb new file mode 100644 index 0000000..37bcc2c --- /dev/null +++ b/db/migrate/008_create_forums.rb @@ -0,0 +1,15 @@ +class CreateForums < ActiveRecord::Migration + def self.up + create_table :forums do |t| + t.column :name, :string + t.column :description, :text + t.column :created_at, :datetime + t.column :updated_at, :datetime + t.column :topics_count, :integer, :null => false, :default => 0 + end + end + + def self.down + drop_table :forums + end +end diff --git a/db/migrate/009_create_topics.rb b/db/migrate/009_create_topics.rb new file mode 100644 index 0000000..e74daff --- /dev/null +++ b/db/migrate/009_create_topics.rb @@ -0,0 +1,17 @@ +class CreateTopics < ActiveRecord::Migration + def self.up + create_table :topics do |t| + t.column :forum_id, :integer + t.column :user_id, :integer + t.column :name, :string + t.column :created_at, :datetime + t.column :updated_at, :datetime + t.column :posts_count, :integer, :null => false, :default => 0 + end + add_index :topics, :forum_id + end + + def self.down + drop_table :topics + end +end diff --git a/db/migrate/010_create_posts.rb b/db/migrate/010_create_posts.rb new file mode 100644 index 0000000..de917de --- /dev/null +++ b/db/migrate/010_create_posts.rb @@ -0,0 +1,16 @@ +class CreatePosts < ActiveRecord::Migration + def self.up + create_table :posts do |t| + t.column :topic_id, :integer + t.column :user_id, :integer + t.column :body, :text + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + add_index :posts, :topic_id + end + + def self.down + drop_table :posts + end +end diff --git a/db/migrate/011_add_user_posts_count.rb b/db/migrate/011_add_user_posts_count.rb new file mode 100644 index 0000000..6a59439 --- /dev/null +++ b/db/migrate/011_add_user_posts_count.rb @@ -0,0 +1,9 @@ +class AddUserPostsCount < ActiveRecord::Migration + def self.up + add_column :users, :posts_count, :integer, :null => false, :default => 0 + end + + def self.down + remove_column :users, :posts_count + end +end diff --git a/db/migrate/012_add_moderator_role.rb b/db/migrate/012_add_moderator_role.rb new file mode 100644 index 0000000..6b40965 --- /dev/null +++ b/db/migrate/012_add_moderator_role.rb @@ -0,0 +1,14 @@ +class AddModeratorRole < ActiveRecord::Migration + def self.up + moderator_role = Role.create(:name => 'Moderator') + admin_user = User.find_by_username('Admin') + admin_user.roles << moderator_role + end + + def self.down + moderator_role = Role.find_by_name('Moderator') + admin_user = User.find_by_username('Admin') + admin_user.roles.delete(moderator_role) + moderator_role.destroy + end +end diff --git a/db/migrate/013_create_entries.rb b/db/migrate/013_create_entries.rb new file mode 100644 index 0000000..e176550 --- /dev/null +++ b/db/migrate/013_create_entries.rb @@ -0,0 +1,17 @@ +class CreateEntries < ActiveRecord::Migration + def self.up + create_table :entries do |t| + t.column :user_id, :integer + t.column :title, :string + t.column :body, :text + t.column :comments_count, :integer, :null => false, :default => 0 + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + add_index :entries, :user_id + end + + def self.down + drop_table :entries + end +end diff --git a/db/migrate/014_create_comments.rb b/db/migrate/014_create_comments.rb new file mode 100644 index 0000000..08289b0 --- /dev/null +++ b/db/migrate/014_create_comments.rb @@ -0,0 +1,18 @@ +class CreateComments < ActiveRecord::Migration + def self.up + create_table :comments do |t| + t.column :entry_id, :integer + t.column :user_id, :integer + t.column :guest_name, :string + t.column :guest_email, :string + t.column :guest_url, :string + t.column :body, :text + t.column :created_at, :datetime + end + add_index :comments, :entry_id + end + + def self.down + drop_table :comments + end +end diff --git a/db/migrate/015_add_blog_settings_to_user.rb b/db/migrate/015_add_blog_settings_to_user.rb new file mode 100644 index 0000000..ecf3dc3 --- /dev/null +++ b/db/migrate/015_add_blog_settings_to_user.rb @@ -0,0 +1,13 @@ +class AddBlogSettingsToUser < ActiveRecord::Migration + def self.up + add_column :users, :entries_count, :integer, :null => false, :default => 0 + add_column :users, :blog_title, :string + add_column :users, :enable_comments, :boolean, :default => true + end + + def self.down + remove_column :users, :entries_count + remove_column :users, :blog_title + remove_column :users, :enable_comments + end +end diff --git a/db/migrate/016_create_photos.rb b/db/migrate/016_create_photos.rb new file mode 100644 index 0000000..54d18bf --- /dev/null +++ b/db/migrate/016_create_photos.rb @@ -0,0 +1,26 @@ +class CreatePhotos < ActiveRecord::Migration + def self.up + create_table :photos do |t| + t.column :user_id, :integer + t.column :title, :string + t.column :body, :text + t.column :created_at, :datetime + + # the following columns are required for attachment_fu + t.column :content_type, :string, :limit => 100 + t.column :filename, :string, :limit => 255 + t.column :path, :string, :limit => 255 + t.column :parent_id, :integer + t.column :thumbnail, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + end + add_column :users, :photos_count, :integer + end + + def self.down + drop_table :photos + remove_column :users, :photos_count + end +end diff --git a/db/migrate/017_create_emails.rb b/db/migrate/017_create_emails.rb new file mode 100644 index 0000000..593c2a0 --- /dev/null +++ b/db/migrate/017_create_emails.rb @@ -0,0 +1,14 @@ +class CreateEmails < ActiveRecord::Migration + def self.up + create_table :emails do |t| + t.column :from, :string + t.column :to, :string + t.column :last_send_attempt, :integer, :default => 0 + t.column :mail, :text + end + end + + def self.down + drop_table :emails + end +end diff --git a/db/migrate/018_create_newsletters.rb b/db/migrate/018_create_newsletters.rb new file mode 100644 index 0000000..5be6d0f --- /dev/null +++ b/db/migrate/018_create_newsletters.rb @@ -0,0 +1,15 @@ +class CreateNewsletters < ActiveRecord::Migration + def self.up + create_table :newsletters do |t| + t.column :subject, :string + t.column :body, :text + t.column :sent, :boolean, :null => false, :default => false + t.column :created_at, :datetime + t.column :updated_at, :datetime + end + end + + def self.down + drop_table :newsletters + end +end diff --git a/db/migrate/019_create_friendships.rb b/db/migrate/019_create_friendships.rb new file mode 100644 index 0000000..408926a --- /dev/null +++ b/db/migrate/019_create_friendships.rb @@ -0,0 +1,31 @@ +class CreateFriendships < ActiveRecord::Migration + def self.up + create_table :friendships do |t| + t.column :user_id, :integer, :null => false + t.column :friend_id, :integer, :null => false + t.column :xfn_friend, :boolean, :default => false, :null => false + t.column :xfn_acquaintance, :boolean, :default => false, :null => false + t.column :xfn_contact, :boolean, :default => false, :null => false + t.column :xfn_met, :boolean, :default => false, :null => false + t.column :xfn_coworker, :boolean, :default => false, :null => false + t.column :xfn_colleague, :boolean, :default => false, :null => false + t.column :xfn_coresident, :boolean, :default => false, :null => false + t.column :xfn_neighbor, :boolean, :default => false, :null => false + t.column :xfn_child, :boolean, :default => false, :null => false + t.column :xfn_parent, :boolean, :default => false, :null => false + t.column :xfn_sibling, :boolean, :default => false, :null => false + t.column :xfn_spouse, :boolean, :default => false, :null => false + t.column :xfn_kin, :boolean, :default => false, :null => false + t.column :xfn_muse, :boolean, :default => false, :null => false + t.column :xfn_crush, :boolean, :default => false, :null => false + t.column :xfn_date, :boolean, :default => false, :null => false + t.column :xfn_sweetheart, :boolean, :default => false, :null => false + end + + add_index :friendships, [:user_id, :friend_id] + end + + def self.down + drop_table :friendships + end +end \ No newline at end of file diff --git a/db/migrate/020_add_users_latest_activity.rb b/db/migrate/020_add_users_latest_activity.rb new file mode 100644 index 0000000..ce858a9 --- /dev/null +++ b/db/migrate/020_add_users_latest_activity.rb @@ -0,0 +1,11 @@ +class AddUsersLatestActivity < ActiveRecord::Migration + def self.up + add_column :users, :last_activity, :string + add_column :users, :last_activity_at, :datetime + end + + def self.down + remove_column :users, :last_activity + remove_column :users, :last_activity_at + end +end \ No newline at end of file diff --git a/db/migrate/021_add_tagging_support.rb b/db/migrate/021_add_tagging_support.rb new file mode 100644 index 0000000..4da5f91 --- /dev/null +++ b/db/migrate/021_add_tagging_support.rb @@ -0,0 +1,22 @@ +class AddTaggingSupport < ActiveRecord::Migration + def self.up + create_table :tags, :force => true do |t| + t.column :name, :string + end + + create_table :taggings, :force => true do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + t.column :taggable_type, :string + t.column :created_at, :datetime + end + + add_index :tags, :name + add_index :taggings, [:tag_id, :taggable_id, :taggable_type] + end + + def self.down + drop_table :tags + drop_table :taggings + end +end diff --git a/db/migrate/022_add_geo_to_photos.rb b/db/migrate/022_add_geo_to_photos.rb new file mode 100644 index 0000000..9679822 --- /dev/null +++ b/db/migrate/022_add_geo_to_photos.rb @@ -0,0 +1,13 @@ +class AddGeoToPhotos < ActiveRecord::Migration + def self.up + add_column :photos, :geo_lat, :float + add_column :photos, :geo_long, :float + add_column :photos, :show_geo, :boolean, :default => true, :null => false + end + + def self.down + remove_column :photos, :geo_lat + remove_column :photos, :geo_long + remove_column :photos, :show_geo + end +end diff --git a/db/migrate/023_add_flickr_user_fields.rb b/db/migrate/023_add_flickr_user_fields.rb new file mode 100644 index 0000000..f3ec686 --- /dev/null +++ b/db/migrate/023_add_flickr_user_fields.rb @@ -0,0 +1,11 @@ +class AddFlickrUserFields < ActiveRecord::Migration + def self.up + add_column :users, :flickr_username, :string + add_column :users, :flickr_id, :string + end + + def self.down + remove_column :users, :flickr_username + remove_column :users, :flickr_id + end +end diff --git a/db/migrate/024_create_usertemplates.rb b/db/migrate/024_create_usertemplates.rb new file mode 100644 index 0000000..97f65f1 --- /dev/null +++ b/db/migrate/024_create_usertemplates.rb @@ -0,0 +1,13 @@ +class CreateUsertemplates < ActiveRecord::Migration + def self.up + create_table :usertemplates do |t| + t.column :user_id, :integer + t.column :name, :string + t.column :body, :text + end + add_index :usertemplates, [:user_id, :name] end + + def self.down + drop_table :usertemplates + end +end diff --git a/db/migrate/025_add_sessions.rb b/db/migrate/025_add_sessions.rb new file mode 100644 index 0000000..22b4a00 --- /dev/null +++ b/db/migrate/025_add_sessions.rb @@ -0,0 +1,16 @@ +class AddSessions < ActiveRecord::Migration + def self.up + create_table :sessions do |t| + t.column :session_id, :string + t.column :data, :text + t.column :updated_at, :datetime + end + + add_index :sessions, :session_id + add_index :sessions, :updated_at + end + + def self.down + drop_table :sessions + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..af82275 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,203 @@ +# This file is autogenerated. Instead of editing this file, please use the +# migrations feature of ActiveRecord to incrementally modify your database, and +# then regenerate this schema definition. + +ActiveRecord::Schema.define(:version => 25) do + + create_table "articles", :force => true do |t| + t.column "user_id", :integer + t.column "title", :string + t.column "synopsis", :text + t.column "body", :text + t.column "published", :boolean, :default => false + t.column "created_at", :datetime + t.column "updated_at", :datetime + t.column "published_at", :datetime + t.column "category_id", :integer, :default => 1 + end + + create_table "categories", :force => true do |t| + t.column "name", :string + end + + create_table "comments", :force => true do |t| + t.column "entry_id", :integer + t.column "user_id", :integer + t.column "guest_name", :string + t.column "guest_email", :string + t.column "guest_url", :string + t.column "body", :text + t.column "created_at", :datetime + end + + add_index "comments", ["entry_id"], :name => "index_comments_on_entry_id" + + create_table "emails", :force => true do |t| + t.column "from", :string + t.column "to", :string + t.column "last_send_attempt", :integer, :default => 0 + t.column "mail", :text + end + + create_table "entries", :force => true do |t| + t.column "user_id", :integer + t.column "title", :string + t.column "body", :text + t.column "comments_count", :integer, :default => 0, :null => false + t.column "created_at", :datetime + t.column "updated_at", :datetime + end + + add_index "entries", ["user_id"], :name => "index_entries_on_user_id" + + create_table "forums", :force => true do |t| + t.column "name", :string + t.column "description", :text + t.column "created_at", :datetime + t.column "updated_at", :datetime + t.column "topics_count", :integer, :default => 0, :null => false + end + + create_table "friendships", :force => true do |t| + t.column "user_id", :integer, :null => false + t.column "friend_id", :integer, :null => false + t.column "xfn_friend", :boolean, :default => false, :null => false + t.column "xfn_acquaintance", :boolean, :default => false, :null => false + t.column "xfn_contact", :boolean, :default => false, :null => false + t.column "xfn_met", :boolean, :default => false, :null => false + t.column "xfn_coworker", :boolean, :default => false, :null => false + t.column "xfn_colleague", :boolean, :default => false, :null => false + t.column "xfn_coresident", :boolean, :default => false, :null => false + t.column "xfn_neighbor", :boolean, :default => false, :null => false + t.column "xfn_child", :boolean, :default => false, :null => false + t.column "xfn_parent", :boolean, :default => false, :null => false + t.column "xfn_sibling", :boolean, :default => false, :null => false + t.column "xfn_spouse", :boolean, :default => false, :null => false + t.column "xfn_kin", :boolean, :default => false, :null => false + t.column "xfn_muse", :boolean, :default => false, :null => false + t.column "xfn_crush", :boolean, :default => false, :null => false + t.column "xfn_date", :boolean, :default => false, :null => false + t.column "xfn_sweetheart", :boolean, :default => false, :null => false + end + + add_index "friendships", ["user_id", "friend_id"], :name => "index_friendships_on_user_id_and_friend_id" + + create_table "newsletters", :force => true do |t| + t.column "subject", :string + t.column "body", :text + t.column "sent", :boolean, :default => false, :null => false + t.column "created_at", :datetime + t.column "updated_at", :datetime + end + + create_table "pages", :force => true do |t| + t.column "title", :string + t.column "permalink", :string + t.column "body", :text + t.column "created_at", :datetime + t.column "updated_at", :datetime + end + + create_table "photos", :force => true do |t| + t.column "user_id", :integer + t.column "title", :string + t.column "body", :text + t.column "created_at", :datetime + t.column "content_type", :string, :limit => 100 + t.column "filename", :string + t.column "path", :string + t.column "parent_id", :integer + t.column "thumbnail", :string + t.column "size", :integer + t.column "width", :integer + t.column "height", :integer + t.column "geo_lat", :float + t.column "geo_long", :float + t.column "show_geo", :boolean, :default => true, :null => false + end + + create_table "posts", :force => true do |t| + t.column "topic_id", :integer + t.column "user_id", :integer + t.column "body", :text + t.column "created_at", :datetime + t.column "updated_at", :datetime + end + + add_index "posts", ["topic_id"], :name => "index_posts_on_topic_id" + + create_table "roles", :force => true do |t| + t.column "name", :string + end + + create_table "roles_users", :id => false, :force => true do |t| + t.column "role_id", :integer, :null => false + t.column "user_id", :integer, :null => false + end + + create_table "sessions", :force => true do |t| + t.column "session_id", :string + t.column "data", :text + t.column "updated_at", :datetime + end + + add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" + add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at" + + create_table "taggings", :force => true do |t| + t.column "tag_id", :integer + t.column "taggable_id", :integer + t.column "taggable_type", :string + t.column "created_at", :datetime + end + + add_index "taggings", ["tag_id", "taggable_id", "taggable_type"], :name => "index_taggings_on_tag_id_and_taggable_id_and_taggable_type" + + create_table "tags", :force => true do |t| + t.column "name", :string + end + + add_index "tags", ["name"], :name => "index_tags_on_name" + + create_table "topics", :force => true do |t| + t.column "forum_id", :integer + t.column "user_id", :integer + t.column "name", :string + t.column "created_at", :datetime + t.column "updated_at", :datetime + t.column "posts_count", :integer, :default => 0, :null => false + end + + add_index "topics", ["forum_id"], :name => "index_topics_on_forum_id" + + create_table "users", :force => true do |t| + t.column "username", :string, :limit => 64, :default => "", :null => false + t.column "email", :string, :limit => 128, :default => "", :null => false + t.column "hashed_password", :string, :limit => 64 + t.column "enabled", :boolean, :default => true, :null => false + t.column "profile", :text + t.column "created_at", :datetime + t.column "updated_at", :datetime + t.column "last_login_at", :datetime + t.column "posts_count", :integer, :default => 0, :null => false + t.column "entries_count", :integer, :default => 0, :null => false + t.column "blog_title", :string + t.column "enable_comments", :boolean, :default => true + t.column "photos_count", :integer + t.column "last_activity", :string + t.column "last_activity_at", :datetime + t.column "flickr_username", :string + t.column "flickr_id", :string + end + + add_index "users", ["username"], :name => "index_users_on_username" + + create_table "usertemplates", :force => true do |t| + t.column "user_id", :integer + t.column "name", :string + t.column "body", :text + end + + add_index "usertemplates", ["user_id", "name"], :name => "index_usertemplates_on_user_id_and_name" + +end diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP new file mode 100644 index 0000000..ac6c149 --- /dev/null +++ b/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake appdoc" to generate API documentation for your models and controllers. \ No newline at end of file diff --git a/lib/login_system.rb b/lib/login_system.rb new file mode 100644 index 0000000..f9d8d97 --- /dev/null +++ b/lib/login_system.rb @@ -0,0 +1,105 @@ +module LoginSystem + protected + + def is_logged_in? + username, password = get_http_auth_data + @logged_in_user = User.find(session[:user]) if session[:user] + @logged_in_user = User.authenticate(username, password) if username && password + @logged_in_user ? @logged_in_user : false + end + + def logged_in_user + return @logged_in_user if is_logged_in? + end + + def logged_in_user=(user) + if !user.nil? + session[:user] = user.id + @logged_in_user = user + end + end + + def self.included(base) + base.send :helper_method, :is_logged_in?, :logged_in_user + end + + def check_role(role) + unless is_logged_in? && @logged_in_user.has_role?(role) + respond_to do |wants| + wants.html do + flash[:error] = "You do not have the permission to do that." + redirect_to :controller => 'account', :action => 'login' + end + wants.xml do + headers['Status'] = 'Unauthorized' + headers['WWW-Authenticate'] = %(Basic realm="Password") + render :text => "Insuffient permission", + :status => '401 Unauthorized', + :layout => false + end + end + end + end + + def login_required + unless is_logged_in? + respond_to do |wants| + wants.html do + flash[:error] = "You must be logged in to do that." + redirect_to :controller => 'account', :action => 'login' + end + wants.xml do + headers["Status"] = "Unauthorized" + headers["WWW-Authenticate"] = %(Basic realm="Web Password") + render :text => "Could't authenticate you", + :status => '401 Unauthorized', + :layout => false + end + end + end + end + + def check_administrator_role + check_role('Administrator') + end + + def check_editor_role + check_role('Editor') + end + + def check_moderator_role + check_role('Moderator') + end + + def login_required + unless is_logged_in? + respond_to do |wants| + wants.html do + flash[:error] = "You must be logged in to do that." + redirect_to :controller => 'account', :action => 'login' + end + wants.xml do + headers["Status"] = "Unauthorized" + headers["WWW-Authenticate"] = %(Basic realm="Web Password") + render :text => "Could't authenticate you", + :status => '401 Unauthorized', + :layout => false + end + end + end + end + +private + def get_http_auth_data + username, password = nil, nil + auth_headers = ['X-HTTP_AUTHORIZATION', 'Authorization', 'HTTP_AUTHORIZATION', + 'REDIRECT_REDIRECT_X_http_AUTHORIZATION'] + auth_header = auth_headers.detect { |key| request.env[key] } + auth_data = request.env[auth_header].to_s.split + + if auth_data && auth_data[0] == 'Basic' + username, password = Base64.decode64(auth_data[1]).split(':')[0..1] + end + return [username, password] + end +end diff --git a/log/.gitignore b/log/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..d3c9983 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,40 @@ +# General Apache options +AddHandler fastcgi-script .fcgi +AddHandler cgi-script .cgi +Options +FollowSymLinks +ExecCGI + +# If you don't want Rails to look in certain directories, +# use the following rewrite rules so that Apache won't rewrite certain requests +# +# Example: +# RewriteCond %{REQUEST_URI} ^/notrails.* +# RewriteRule .* - [L] + +# Redirect all requests not available on the filesystem to Rails +# By default the cgi dispatcher is used which is very slow +# +# For better performance replace the dispatcher with the fastcgi one +# +# Example: +# RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] +RewriteEngine On + +# If your Rails application is accessed via an Alias directive, +# then you MUST also set the RewriteBase in this htaccess file. +# +# Example: +# Alias /myrailsapp /path/to/myrailsapp/public +# RewriteBase /myrailsapp + +RewriteRule ^$ index.html [QSA] +RewriteRule ^([^.]+)$ $1.html [QSA] +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + +# In case Rails experiences terminal errors +# Instead of displaying this message you can supply a file here which will be rendered instead +# +# Example: +# ErrorDocument 500 /500.html + +ErrorDocument 500 "

    Application error

    Rails application failed to start properly" \ No newline at end of file diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..eff660b --- /dev/null +++ b/public/404.html @@ -0,0 +1,30 @@ + + + + + + + The page you were looking for doesn't exist (404) + + + + + +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    + + \ No newline at end of file diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..f0aee0e --- /dev/null +++ b/public/500.html @@ -0,0 +1,30 @@ + + + + + + + We're sorry, but something went wrong + + + + + +
    +

    We're sorry, but something went wrong.

    +

    We've been notified about this issue and we'll take a look at it shortly.

    +
    + + \ No newline at end of file diff --git a/public/dispatch.cgi b/public/dispatch.cgi new file mode 100755 index 0000000..045b0ef --- /dev/null +++ b/public/dispatch.cgi @@ -0,0 +1,10 @@ +#!/Applications/Locomotive2/Bundles/rmagickRailsJan2007_x86.locobundle/framework/bin/ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/dispatch.fcgi b/public/dispatch.fcgi new file mode 100755 index 0000000..1a89c64 --- /dev/null +++ b/public/dispatch.fcgi @@ -0,0 +1,24 @@ +#!/Applications/Locomotive2/Bundles/rmagickRailsJan2007_x86.locobundle/framework/bin/ruby +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# +require File.dirname(__FILE__) + "/../config/environment" +require 'fcgi_handler' + +RailsFCGIHandler.process! diff --git a/public/dispatch.rb b/public/dispatch.rb new file mode 100755 index 0000000..045b0ef --- /dev/null +++ b/public/dispatch.rb @@ -0,0 +1,10 @@ +#!/Applications/Locomotive2/Bundles/rmagickRailsJan2007_x86.locobundle/framework/bin/ruby + +require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) + +# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: +# "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired +require "dispatcher" + +ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) +Dispatcher.dispatch \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/images/h-grad.png b/public/images/h-grad.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e39ec4168fb442f7516948f92b8cb9829ea9d6 GIT binary patch literal 2817 zcmV+c3;y(pP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0000iNkl=%2^8` TZLUNC00000NkvXXu0mjf7!f+H literal 0 HcmV?d00001 diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f62dde997f99e83db006340b857c3aecbe1d5a17 GIT binary patch literal 3738 zcmc&$2UC-c(tVJoDC%ngk(VNhG$|rA6afV(N|7o>R6weNfFMc>@z|a!_ zkT}o%yXTZ4Bn$wwfbL5J)es(m@myH7gnY?8fnLw=wc4Nxp(mTe{C|vx7Kj5+zN4# zk(6}GKz);y+1~PU&o2vdz8A(npjK9?+W% z%fW>do7flbx6I@xx9b}ZD*C zP;hW?NJvO%XlPhiSa^7NL_`Dx0*Q=_jEahij*gCriHVJkjf;zmkB?7CNJvafOiD^h zPEJlqNl8sjO-oBlPfyRt$jHph%*x8j&d$!s$$>(lxw*M{d3i7x?Dy~A`T6;9IJ}^s zps=v8sHmv8xEO&zl$4Z|mX?;4m6ey5S5#C~R#sM3RaIA4*VNS1*4Eb5)gh5c6be;e zU*FKsfJUPm8yhhgOjA=+b8~Y`OG|5OYg=0z7K?3fZ|~^n=gww5?*8-VPft%z zZ*MOShr{FXeSLlX{rv<2VPIfjaBy&FXoyH8l1QZC;bAhFJTfveIyyQwHa0#!J~1&d zIXO8sH8njwJu@>iJ3Bi!H#a{&zp${dxVX5qw6wguyt1-Fp-@&=SJ&3o*4NiJHa0dl zH@CL7{{H<-rBb)Iw|91Sc6WF8_V)Jo_YV#Z4i68Hj*gCxk55icPESwI6=+A5pPcUv z*!{J({(lwupLsxrUdt8$m_jt3D;s zDc(2s7QvBBd6lZKDN%^J9*bG%tBB<2lpY4%U6ey{a(eDh@xqvp1%wdt zL-dFDXKh~x=dX;coF z_75%LaDFNSCSGu~nz4bT6L=KJR`5~ZIEh{Sc@0!?Hth)YCNPKXCg9P&k+?uI2DOAp z9V}aZ_zL9_7wl--w&25((-M<>NJozAl>KM>&t7CQ)KN;m+&jK^ycmI%f|U}vTDZDH z?A;;8_m+oxQFD@~*H8OeJMx8ac^tlMZdPnc_Txt%`o}z0MjW<+gL*p4na%qL*RJ(C zJu+m8l(*XU-t>|_)m_aH0k7O`#f<1rG4U=O{Iw$Ji5vNz!Q(UIvJxc|{W*=q(;Vj& z=0SEQinWC3w{?g?splGDE5ntSSbXgPVT?Xyj8#w!f_BmH?Z`(v8<%c66U4g(y$@Jv zEcP0kBLl?gxEWpHH`av;5Awu^i$~^^aH+kfZ|>-A|IO6YKitSQqV0p-$+8Q~W%I$q z7VL~E4~-oluNae3WcPTn^2Cp{ofSVVfiYMTV8wND2UkJE88+86sera*0cjaUhuf%?lom~xWUiH=_dH=?RsRr> z@bvbYIfOtT#yH15_ZZAtKdYnVCa$zuK;mF1P2%cDk~OlzORK|^Sf7chExoW9XqmF| zNcfh}CXwZ#srb+zcggQvgU&~rWT9KvDm{>qY&6M)n^+8;%eY;5+(VA| zz68D7CO)}#ziDjw)SYuFzVTus&C72rnA##EZ&+*<7d12t-zLhJR&?GDWBlyay18Bn z_@-3loC#QeJyLMJOIsBgmpMw)lx7p}I_E_D?+?I)+q~pUr5n0pq7E#4KhTLWfZXkx z?(B0n5vd6=4sMZkQm^jK!9qCJb>&@(*n=*Br*a80dKY1is_WyjO{zIMAM!oNoH?cW z#9l?guB5wH#_`$?7-t5`iXOi;66}NV49Kb8LY(me!PccL2Y$lfmY^+hZ!usjoM@uh zILHyk2r}nh?WMnXz2xWWo+bc#F2YtuahK6pEYGn5>npzxmaV2&nJjCF*wS2-a4}ta zY;+)|G@^wCdaCb`EuO#e)0N-7n42k#;+&(>@B-71S&#)PI$!&q1f~zYA=f9vuS*SK zNe`CI4kW-Eykx`n7D$WVeSpqLkxmIEa2B**scvfK6I~MF4P)^5Ipd$mPq(HAD-CjS zDD2Fj8Rnx7$q=8@__p{xd~<#PXyqsR;eh}MPA|Sb)ay9M5qJSR{7HIP&?JEquDlz)I^!r8)B@I(k1mKDQ~GHt6A?>F6(yhe z)6!Ip@q^c2y1`8CHy6|dZB3!NHutIoFLT{4DCM=BS*N)pXfZm}@+2tipOq(DG6MiS zf}EvyXGyUVMENzNmm=zZK?^jb+Sc#KN}CI#EL?iZxDuC6uDHzn?ee8a9Ki+>qZAcu zcU3dqpq0RYa6L7uy;6x^mN_;IlrgJsU3s#g8@Ek8fK zZ76grN>4|JPUg!?yuKI^Dw#ON;&6WEh*SDdyKqQC>o(lKTMDhv{&8TlU(5!|$RyR> zQ(}7Ix!Tauj&c5qXfr@J>v-N(JB_qK0!LRcf6Qs0e;tLGScs-@__41wd3B51-U6eL- z(XKbQ^I=Uy88;}aF>+Oq>Ik^njG;+AA5%usUi&wFB&(z6vGWKc1RIa#tGn};ECzvB zlGuJ*CTV`ojGZsn!8160O>al3(Va(q0x@sjV^SEV>J>&v zK3HIJf&F1sd8@q=bjDVa%_jFJuza8S;L&GkMil?wtd$KSa~Pwb*wg#NXE}||0i%NE z6<^7I2pjV!wFYz5q8I9EeEAq-s|soLXuE=v(LHaRkY%^HPlosd<$wmpJ=8UW_mh~97{+q{M4!`bmm8kNP>1p2%%==)Ehm|`^oc)QIXcD z4KCBwhcDJ*ia!Lt>VW7vY%}E*JZg*Ko|sJ)Mk#OIxo~qRr>wsFrI%U~pB=dk%Cy`m z$@r>Gnbm-+X6X3FOIE?gij2J2h+qAlE#K`@N=4_prIwPVYc>&rmzz}TAyn7l5x$pH zP!E0Biv*5$Y1&zS2#QCvg{RTbSrL;)Y+fFL-w@8+skw;2OLt7NqqH{4Rrmgu>>b~NA)m>CR$j*A?BO?Ujq zuopAygn|SfozJuNIh$a0mZ`Ivg$?5~7XfO0eOe#A!`|fcF{b5R=s3H7%AIv~6~G)M WmFvK7Up)Vh0S#5{=Orrd0{#Q0UiK&e literal 0 HcmV?d00001 diff --git a/public/images/rails.png b/public/images/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..b8441f182e06974083cf08f0acaf0e2fd612bd40 GIT binary patch literal 1787 zcmVCLdthj)A!BBmWB&y|X`RY;f`BJ<_ju%@N||NoLFD~mQl$aHGjq>;5dG_D{h(5s}0 z6&=HANU$m__3PuddU(lvR_xWj`}Oho@9EyQt-n!E*P(KhM@X_VFV2l&>deNZJT%y8iwA zoG>u1B`p2=_u9k4v1Mud`1+qvOZoHg#bITJ9U`qBAek?40RR96!AV3xRCwBy*IQ$v zN(=yC9IhRft9V64L`77pqF_Cx@c;kSNoGK)`?Ps*cP(EtGlYZ{D5cxspMQvjKH)Oh6X(pa|J{ zGy1J$Ej7=Z{uvmMfRRsE;v`p;45B~6*ep#hM^ji zl$+7qoWq~}ewG=61uFw0He{tJurMU&4Iv?=B^eR(wAHk!miA)O7p_+YR>lbmU3rmn ze?+ze(+sEd6foB&*l9+?zkr_a-5*v&p*?c}HOGtyHg6r{WFYpQ=#z0Hc7VWLx$>M3|b0|Gn z+5t#z6*ffSVc6DjpmB2?AAR@@vB!wCK?9Yl;33;Q7^%(401QW|k=R8b!OwtLJPjjm zO9Ia;qCq)rOq!1Ia*6#A%#xb}yDx1P*pWla>9j$bnMn3CBqe4`TRll_Iy29kmG?4fbKuF=XqU|?3b@B zA`&a?KIgZ|KJx5eND_c3Em=WZn@xW8hRJ^G&sY^b(FW?WC9W_sb;+lAPdLTdBaKIK;-f}*h4|1aTjw7qX_k~e{TWO7jqcekERN;Jyh%67)q4rKpL*CEYL;|#GY{B@5 zi52XoC?xsoorJKxsliugF#z38MJqrYCWV(t<=G&f;^Me13&AiI9{3jUZ$ zFM`*L(9qc^VMxkz1oaDH!1pcD^IXp>Z0Jb=_qs?Vsrs{mp<^{$N!EC9o+`CO-(o}E zJ`y{*;9s|wr22-QoJ87y^~;)Q@b%P4UgSSsx>2$o@Vd{%Pk0@4qZ^fhB(vt$c1TG> z*{Ad;foraENbld`=MCNm4?9kvlgK~&J>ialpJ7nua zx0oRzwG5;}Qne)Fg(N3kf?JVmB;}y&5(0+~r*aL$0Zof8fe!AtHWH>A^1Y)@G@GsA zup`R{Qg?{+MaxTq#2n{6w|)c&yaJ7{U4ngAH5v6I)*;@rEBE*ehIPBwKBQU)YKE8F0lR!Sm?sE4Xk-sj&E$|A-9n dP56HS1^^A-61FoN)nxzx002ovPDHLkV1kw_Sd9Px literal 0 HcmV?d00001 diff --git a/public/images/xfn-child.png b/public/images/xfn-child.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ac882e8e70d8b6b5ad3e81546711b2de01d3ba GIT binary patch literal 588 zcmV-S0<-;zP)5EPYy1nt1WLg)Mg z1_l;>0wWuOPV8)*SQx`n@lhd$4g?Sql}Z^%T1u0&N=o9`#=e(dk)8^`bqcnK6W5A!{4Cxl4{I_KSA*LLMxBW%<(%^VbVJygN zAH_PM*Ef6Y_Tb$bY_*dx%akak-P8rlUd$!*<+E8<*5TVWEU!Wo5BH>kZcICjpBx$J zTh|I0`;}n?I;AHS)a-v`_7+$k<4(9aZq%65l<^Mp1t`&7^y-2-jmPV zyjsF>@;Fm@Vg;E)77QsxqaP@@>j&I<^pSegADY(bMZ8$419kl0SLlaDrcT4$Ip_xp zDMiE3T32cf@AmhmZ+d~EkQ+@v!U)uXrEF}snK@r%brUX}f?C51wAPg)FZ-xCeA(i- a{@Nc%aL}HNs8S#R0000y{q`QY`6?zK#qG*KS<#k1zuAB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xE{UoZr{1r_nv>%NIY=% z_Qy;2K1H{kfB*THTKs;Ktiua;J)W@s-tRyEZa)6<>FaN&lH;#F{D|$i5Y~L||NsB{ z&cE4v?)AoF&jaetJb(LR*V$KVk365d?a{p#Un}R_xcTJkv17+hrfvNVv`e8R$S)YE z1s`CDiIIK_RJ+2{#WAGfR?dV-zGed+m$@^JzVSYGLG)eyX)6V;pxON!zZ!MTUAQ&y zM&Yt^j9kJ$rcMk0VS7(Fsn+1Lt?U2244FF39O*l~_3q@}`4ibUdH3JBj_YcKKIi|* z{dekc^z63I!v}Bs?V7h@+cagqUFUUoCV83k?|oA&cx%&R!=6b~J*!rqx8+WonQ5k* zbzx^e<1_W{zlsd)`y>B7-(Y5Fy5i}wT`#BIc5h&0xOrc=vXg`VB+$(ap00i_>zopr E07Bv8X#fBK literal 0 HcmV?d00001 diff --git a/public/images/xfn-colleague.png b/public/images/xfn-colleague.png new file mode 100644 index 0000000000000000000000000000000000000000..d79e5256cc43a27c615a06bd95a05cfa1edf70ae GIT binary patch literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7DVB6cUq=RpYd5a=M;HP5k|nMY zCBgY=CFO}lsSJ)O`AMk?p1FzXsX?iUDV2pMQ*D5X5(0ceT<<>rdhYheV^`m)CG5ZX z_{+P`KPRrguaS7*)rX%ZS%+iV&M(;c=+oEV7w>)wZ9ez^|NnjG-|Rd8dhfZ{`_8@o z{pVk7$NAWf3r;1+4_$h@<;2Um+aEoD`=fHs&49Wy{&lC19Xpo&@0KLcE`^dHzhIzt ze1IV)M*1yK?Q~BU$B>F!IRVjp%?3Q#o{D>3FEO~W`TPE-QVV^3X4T6?mOVN0bb)}V zM#GW|6JiTKvb#2XoI0a)(zP@T!(9f&@vFB-#u!D-%v}4BUz|_8HnxgI-sOr#;qutW zZqd8LPkT!3cz9~@Kaa;7F3P08Vz<9LeUo=`xYV9i>cXYnZT{dG2%YT_E k%siX|n*0tY@)_9}o^0e*b>G1_3+O}!Pgg&ebxsLQ05S^AJOBUy literal 0 HcmV?d00001 diff --git a/public/images/xfn-friend-met.png b/public/images/xfn-friend-met.png new file mode 100644 index 0000000000000000000000000000000000000000..477f75178a3baaefa6ce105cfd47a8727129db68 GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^;y}#D!3-on>y{q`QY`6?zK#qG*KS<#k1zuAB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xW=@f*GN3@;@!_9SKgUq z9$LBo+0Dmae*ONtaMxq?`2APze{w20{{R2~edk~AJ@&gSq_Fs6t z@!0eG&%b^8`g`4x=TkS{-*V#Rt~0OJ9)7mz_>2ALUsuk#aqQSJN2Sjo2Pu>U`2_>b z!3P*(Vx->!)von)aSW-rl{2B2uUUboC0b;wSLVf)zrX*zUlckgdXuAvzWkFak!e~+ z$4?}+N8XrnVu!=jL+`zmKSZTF?Gf8+t;dq0>)`R=*KzUdbCoU5v-evc@8y@AccCKY zsxasIwfVl=9`?1LzmoVoZ~85jo38~dJ|Fuf)_ng{z_;lJrgLu!I4*WwWpC+ilF>Wy z)UNr)>A$agZR-`;Q~&&reLIWy(lt6+$=z#K9nvz?)n{a5XmaFpW7k}M4CrtMPgg&e IbxsLQ06e+iK>z>% literal 0 HcmV?d00001 diff --git a/public/images/xfn-friend.png b/public/images/xfn-friend.png new file mode 100644 index 0000000000000000000000000000000000000000..60563f678e6bcd8378de50605cdfeb747ce7feec GIT binary patch literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^LO{&N!3-otS@yI7DVB6cUq=RpYd5a=M;HP5k|nMY zCBgY=CFO}lsSJ)O`AMk?p1FzXsX?iUDV2pMQ*D5X5(0ceT<<>n8r^pO#rvNci3g5c zez))Z8{>>aHy?jdi{HO;|Fd7e|9<-Vd;X3`SMGoM|NsBK^RM@wd%f@6>y-zd?Z5EG zspR_niG+{9|rbuIIQ=X9>(SK3kbb7`fCXDaie$!F8gZro|c;8^GWvYF$7Lr%m~rFFO8 zJl8kT+i>Hn<(6;N4_R1Z&wO)uyoF=pKjE^}3G$X-xKg+l)ty+q!=Rz5toi$+qG$OF mU!^>3IdMiqD7Kc{fMHi7kLCmOt<7uaS6Q-}yKD&c9Yq z*uVGO>zj|i>^t{bBjG@7$N5iRf5&xRxcmHzdcuKw&%ZjAo>;i+@ux4pO|y?o*>Hc) z+1Ih17fxP#ck{{DirF`uN{+8P^4uir$lAltjvYJpkgZ%4Xp3A)kY6y+7Tkd0jf6}b zP-U~Hi(^Q|t(g8uJ{Cm|7tMgEx24~%{*EsWc-+qHsi@!T85ky*_D$zecZOPQ#)`L& zG4lBg*N@FuaxO!o>`F{=!|eYPcINT=K6zck_TbV437x5Bd;A_hOl<$&cV=pz?CQf( z`&aHh$5(K0W}Vfhr6m)8?afPG;Hs(Jz4YnH>Wxpteybl~IC_-1pFhsZ4(J>PPgg&e IbxsLQ09&QMMgRZ+ literal 0 HcmV?d00001 diff --git a/public/images/xfn-parent.png b/public/images/xfn-parent.png new file mode 100644 index 0000000000000000000000000000000000000000..a7137af51eaf4391024d79b23cc45c576aa66eb9 GIT binary patch literal 605 zcmV-j0;2tiP)aiF_KW>R<*_ZhR1TFPXe&%!`tQ`<%v+t*_MdRB*IYh>695$ zaq-G??mcZI0vFCq<2vpj=KYK-PNz)5Q1b>5>xn8neD!tUR~J`fz@2uSXbWo$_e2CH z_oWB-yqk>0_GgC?j3xp&IUSX9e99U4rU9uP|3G9VqvucGBSpOcUas_qF>DFw{|kV6 ztIO3JAGm%$8qKwvOB_8kLFvFa&MuqB@8H=-D2~I4gArElF0@ulg-rg37jX9S0^c@z zBc-RT1m|auF_w;MD^G%x>hgMJqmfY6x!Rg{uHNoirNS7hN@o*-0CY=j{}A*%oJ>UY rtEbe~JYQAkwzVR|HflxWe=Y5Ayj9d`&_!(500000NkvXXu0mjfi~6qxv++4$e0TL0U{7UI-%DPYA*f_w=-|wy!~N!XWnkOp@EK)!WjhsfD#siHkR^f zDU_iQY2Li<(Ih2#0>;7>07cbrkO8UbaA{H2UPE64fbwYHeWL`azF>ftjfUCN{;h2-Aj z{lem?W`!c2dCibd=D-halW9?HisU9}5?3jANVR?O9}*o}Kl58+$63+dN^0M^NTyun z#3j}wC2#o0*cOHGW(C1=V|=?JmHTJah*Qa+V-fFUMxRPvuRgU+nZlBf>s0Dm(n#;Q z7EljO%yk^wkt?)L)zzhuVQ#*xT| zao@=mZd@y@rKJUhLTPAdXliQSxpN1NMr&zlX=`ii=;&ZD7%Uch_wHR?U0pprJ$-$B z0|SG5_wE@Q8X6fH85}0Re%5 zfk8n*!NI{qA~7T+ghV2RhK7cPg^|hR@bK`6h=|C@ND74#6%`d79UT)B6B`>F7Z(>F zAD@trkeHa5l$7-5&70)pWGa=Kl9G~|nwplDmY$xTk&%&^nMtG3va+(Wv$Jz@a^Aju zo12@PmzVeM-Mjq!{DOjl!otF$qN4Zj-xn7bfB5jBq@<*@w6v_Oth~IuqN1X*vhw4{ zk5yGw)z#HCH8r)hwRLrMpFVx6udi=tXlQI~q|@mP2IKSR&rMBDOeT}XVl_86v)OE^ zDXp!o91f?gt*yPiy`!Up%jI@P z%a@^{q2b}-k&zLBKrlKwIyN>oK0f~S>(_~iiOI>ysi~>y>FJr7nc3Odxw*Od`FWvG zxUjIWxVX5qw6wguyt1;gy1Kfywzj^$zOk{fxw*NuwY9yyEfR@#c6P*K@$T;K-rnB+ z{{F$i!QtWI(b19g%&0%F^Q3=+ax*qCk;?xQfaz0?20+FHi`Fpp9JZ;j$J=SjaVa7o z)6C)Ew?ZarPVl}D)4N1&=Z~LXJU*VrV>ZajwepV;zyV#PMDY@*@9e@Atp%+eRht|N( ztHH$#BnE--l{FqOxyeD60a+EqPnYZ9wd#=Te=E8KBUxuHL!CV&(m{}D0tAWx?bqzx T3td{aq*ei}mI1m%(<=BsHBQwB literal 0 HcmV?d00001 diff --git a/public/images/xfn-spouse.png b/public/images/xfn-spouse.png new file mode 100644 index 0000000000000000000000000000000000000000..484bb59db8dcdb20c7670328f2e7213cc04f4143 GIT binary patch literal 853 zcmV-b1FHOqP)hLgzyG_PmY#<)|ak2;=hDz~J5OydGkqyzCARb0x+DTLA1{1c$h(b%7w3>FdH7O?A zG|w4}DF&5NhdocW@IR)t`d37TML2t)D^gJLM0SH# z>ak<#NK@3Y9LtA7{U<~aO0FTZSaM6Qm-9s0wNeb)mX7qa)LF5xO>HQEmNJ={CrEXf z^&vm&Lw+ztGBZ!mQld5#AQraiX{obpOGlJqP~A)B7Irj-qMJ9`q|=l9_|s{&G|thu z)n+zR;{I=nSX9&czDGlY&CK))&z?4L;`sOM+}XzbQkluQ;)3=(i3V*S>GTvgZyn*& z&k8IqI3yF>Nu~U>?ke&1x9hm`OOa$^JFj1@GTvq+lS{#jCdj|h2>Q_ zcmA;aI{L9(x^zyAF(M-J!{vkWcXGcBTx^jOC-;hoh%rWnh6d!1Kle$0|L0OzUX^$v zD;mHULpUQVLV`69|B*nl&8&0)ZNGFE-HCZ5STD z1;D0FQBqTWEbBRKyIzr+@)3=02H^JXpXu%u@-ORgg26R-Jdu?>J8Bt!{D9f#XDAk{ zOpd=tEY``y#63RvD2v|*BX`2|_IyGvmt=R>G)uWvI@*V5ZU2zOWC6d{YH_7(qJ4e) zSczX?V*ES@56x0vzernqg}WmzFS2Eh9&-qXN0^^?85^x~VE+JpeS4W-Dxaw`rIn&mae4G8!Sr;BrlxIlc6I}hO#VYA^OWZ1cj@TZgJs!xBK$RxcT29h f{?-yvZ!6k25EN)n8Q*?p00000NkvXXu0mjfO4o@+ literal 0 HcmV?d00001 diff --git a/public/images/xfn-sweetheart-met.png b/public/images/xfn-sweetheart-met.png new file mode 100644 index 0000000000000000000000000000000000000000..6982fa467d883e5f70f0599014a0540aa740d6b0 GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^;y}#D!3-on>y{q`QY`6?zK#qG*KS<#k1zuAB}-f* zN`mv#O3D+9QW+dm@{>{(JaZG%Q-e|yQz{EjrrH1%B?S0{xc;g-y7$~`r;_8UvHSiW zc^1`r?!mLK8VLt}IIa8MaPn8-p&O6B{Qvap)7Re%cRl{$xB377|NG9r*?8>v-wUsQ zMs7FBI`U`7qfN(OY&-d~V$O{}3$J&sxV3r4<+X>O{hojI*s){R%i>o6?NTTS@(Tv) z#RnK-Vx->!)%JM0IEGZ*>Y4DG@2~=g+eN>GtJ^lctNZ--{-zy%e=q9X^yGQ+!k zol<!)i!v#IEGZ*D(Sz;*KEMU!fR+YLAYxE&;R@1NQG>j+9jO5_~Mhqm04br z*AtDMv@^x)mj!!GD^I)k-RYa3c2?sp&o1{<29@j**iQU>^`M0;#gzfx@Mbbd4zRTJQEMYjn&*r#{#~&0^P#k M>FVdQ&MBb@01eN@qW}N^ literal 0 HcmV?d00001 diff --git a/public/javascripts/application.js b/public/javascripts/application.js new file mode 100644 index 0000000..145b683 --- /dev/null +++ b/public/javascripts/application.js @@ -0,0 +1,10 @@ +// Place your application-specific JavaScript functions and classes here +// This file is automatically included by javascript_include_tag :defaults + + +function updateLocation(point) { + document.getElementById('photo_geo_lat').value = point.y; + document.getElementById('photo_geo_long').value = point.x; + map.clearOverlays(); + map.addOverlay(new GMarker(new GLatLng(point.y, point.x))); +} \ No newline at end of file diff --git a/public/javascripts/clusterer.js b/public/javascripts/clusterer.js new file mode 100644 index 0000000..eeb6dd9 --- /dev/null +++ b/public/javascripts/clusterer.js @@ -0,0 +1,444 @@ +// Clusterer.js - marker clustering routines for Google Maps apps +// +// The original version of this code is available at: +// http://www.acme.com/javascript/ +// +// Copyright © 2005,2006 by Jef Poskanzer . +// All rights reserved. +// +// Modified for inclusion into the YM4R library in accordance with the +// following license: +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// For commentary on this license please see http://www.acme.com/license.html + + +// Constructor. +Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) { + this.markers = []; + if(markers){ + for(var i =0 ; i< markers.length ; i++){ + this.addMarker(markers[i]); + } + } + this.clusters = []; + this.timeout = null; + + this.maxVisibleMarkers = maxVisibleMarkers || 150; + this.gridSize = gridSize || 5; + this.minMarkersPerCluster = minMarkersPerCluster || 5; + this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10; + + this.icon = icon || G_DEFAULT_ICON; +} + +Clusterer.prototype = new GOverlay(); + +Clusterer.prototype.initialize = function ( map ){ + this.map = map; + this.currentZoomLevel = map.getZoom(); + + GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) ); + //Set map for each marker + for(var i = 0,len = this.markers.length ; i < len ; i++){ + this.markers[i].setMap( map ); + } + this.displayLater(); +} + +Clusterer.prototype.remove = function(){ + for ( var i = 0; i < this.markers.length; ++i ){ + this.removeMarker(this.markers[i]); + } +} + +Clusterer.prototype.copy = function(){ + return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox); +} + +Clusterer.prototype.redraw = function(force){ + this.displayLater(); +} + +// Call this to change the cluster icon. +Clusterer.prototype.setIcon = function ( icon ){ + this.icon = icon; +} + +// Call this to add a marker. +Clusterer.prototype.addMarker = function ( marker, description){ + marker.onMap = false; + this.markers.push( marker ); + marker.description = marker.description || description; + if(this.map != null){ + marker.setMap(this.map); + this.displayLater(); + } +}; + + +// Call this to remove a marker. +Clusterer.prototype.removeMarker = function ( marker ){ + for ( var i = 0; i < this.markers.length; ++i ) + if ( this.markers[i] == marker ){ + if ( marker.onMap ) + this.map.removeOverlay( marker ); + for ( var j = 0; j < this.clusters.length; ++j ){ + var cluster = this.clusters[j]; + if ( cluster != null ){ + for ( var k = 0; k < cluster.markers.length; ++k ) + if ( cluster.markers[k] == marker ){ + cluster.markers[k] = null; + --cluster.markerCount; + break; + } + if ( cluster.markerCount == 0 ){ + this.clearCluster( cluster ); + this.clusters[j] = null; + } + else if ( cluster == this.poppedUpCluster ) + Clusterer.rePop( this ); + } + } + this.markers[i] = null; + break; + } + this.displayLater(); +}; + +Clusterer.prototype.displayLater = function (){ + if ( this.timeout != null ) + clearTimeout( this.timeout ); + this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 ); +}; + +Clusterer.display = function ( clusterer ){ + var i, j, marker, cluster, len, len2; + + clearTimeout( clusterer.timeout ); + + var newZoomLevel = clusterer.map.getZoom(); + if ( newZoomLevel != clusterer.currentZoomLevel ){ + // When the zoom level changes, we have to remove all the clusters. + for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){ + if ( clusterer.clusters[i] != null ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + } + clusterer.clusters.length = 0; + clusterer.currentZoomLevel = newZoomLevel; + } + + // Get the current bounds of the visible area. + var bounds = clusterer.map.getBounds(); + + // Expand the bounds a little, so things look smoother when scrolling + // by small amounts. + var sw = bounds.getSouthWest(); + var ne = bounds.getNorthEast(); + var dx = ne.lng() - sw.lng(); + var dy = ne.lat() - sw.lat(); + dx *= 0.10; + dy *= 0.10; + bounds = new GLatLngBounds( + new GLatLng( sw.lat() - dy, sw.lng() - dx ), + new GLatLng( ne.lat() + dy, ne.lng() + dx ) + ); + + // Partition the markers into visible and non-visible lists. + var visibleMarkers = []; + var nonvisibleMarkers = []; + for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){ + marker = clusterer.markers[i]; + if ( marker != null ) + if ( bounds.contains( marker.getPoint() ) ) + visibleMarkers.push( marker ); + else + nonvisibleMarkers.push( marker ); + } + + // Take down the non-visible markers. + for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){ + marker = nonvisibleMarkers[i]; + if ( marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + + // Take down the non-visible clusters. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){ + clusterer.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } + } + + // Clustering! This is some complicated stuff. We have three goals + // here. One, limit the number of markers & clusters displayed, so the + // maps code doesn't slow to a crawl. Two, when possible keep existing + // clusters instead of replacing them with new ones, so that the app pans + // better. And three, of course, be CPU and memory efficient. + if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){ + // Add to the list of clusters by splitting up the current bounds + // into a grid. + var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat(); + var latInc = latRange / clusterer.gridSize; + var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 ); + for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc ) + for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){ + cluster = new Object(); + cluster.clusterer = clusterer; + cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) ); + cluster.markers = []; + cluster.markerCount = 0; + cluster.onMap = false; + cluster.marker = null; + clusterer.clusters.push( cluster ); + } + + // Put all the unclustered visible markers into a cluster - the first + // one it fits in, which favors pre-existing clusters. + for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.inCluster ){ + for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){ + cluster = clusterer.clusters[j]; + if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){ + cluster.markers.push( marker ); + ++cluster.markerCount; + marker.inCluster = true; + } + } + } + } + + // Get rid of any clusters containing only a few markers. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ) + if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + + // Shrink the clusters list. + for ( i = clusterer.clusters.length - 1; i >= 0; --i ) + if ( clusterer.clusters[i] != null ) + break; + else + --clusterer.clusters.length; + + // Ok, we have our clusters. Go through the markers in each + // cluster and remove them from the map if they are currently up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null ){ + for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){ + marker = cluster.markers[j]; + if ( marker != null && marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + } + } + + // Now make cluster-markers for any clusters that need one. + for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && cluster.marker == null ){ + // Figure out the average coordinates of the markers in this + // cluster. + var xTotal = 0.0, yTotal = 0.0; + for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){ + marker = cluster.markers[j]; + if ( marker != null ){ + xTotal += ( + marker.getPoint().lng() ); + yTotal += ( + marker.getPoint().lat() ); + } + } + var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount ); + marker = new GMarker( location, { icon: clusterer.icon } ); + cluster.marker = marker; + GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) ); + } + } + } + + // Display the visible markers not already up and not in clusters. + for ( i = 0, len = visibleMarkers.length; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.onMap && ! marker.inCluster ) + { + clusterer.map.addOverlay( marker ); + marker.addedToMap(); + marker.onMap = true; + } + } + + // Display the visible clusters not already up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){ + clusterer.map.addOverlay( cluster.marker ); + cluster.onMap = true; + } + } + + // In case a cluster is currently popped-up, re-pop to get any new + // markers into the infobox. + Clusterer.rePop( clusterer ); +}; + + +Clusterer.popUp = function ( cluster ){ + var clusterer = cluster.clusterer; + var html = ''; + var n = 0; + for ( var i = 0 , len = cluster.markers.length; i < len; ++i ) + { + var marker = cluster.markers[i]; + if ( marker != null ) + { + ++n; + html += ''; + if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox ) + { + html += ''; + break; + } + } + } + html += '
    '; + if ( marker.getIcon().smallImage != null ) + html += ''; + else + html += ''; + html += '' + marker.description + '
    ...and ' + ( cluster.markerCount - n ) + ' more
    '; + clusterer.map.closeInfoWindow(); + cluster.marker.openInfoWindowHtml( html ); + clusterer.poppedUpCluster = cluster; +}; + +Clusterer.rePop = function ( clusterer ){ + if ( clusterer.poppedUpCluster != null ) + Clusterer.popUp( clusterer.poppedUpCluster ); +}; + +Clusterer.popDown = function ( clusterer ){ + clusterer.poppedUpCluster = null; +}; + +Clusterer.prototype.clearCluster = function ( cluster ){ + var i, marker; + + for ( i = 0; i < cluster.markers.length; ++i ){ + if ( cluster.markers[i] != null ){ + cluster.markers[i].inCluster = false; + cluster.markers[i] = null; + } + } + + cluster.markers.length = 0; + cluster.markerCount = 0; + + if ( cluster == this.poppedUpCluster ) + this.map.closeInfoWindow(); + + if ( cluster.onMap ) + { + this.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } +}; + +// This returns a function closure that calls the given routine with the +// specified arg. +Clusterer.makeCaller = function ( func, arg ){ + return function () { func( arg ); }; +}; + + +// Augment GMarker so it handles markers that have been created but +// not yet addOverlayed. +GMarker.prototype.setMap = function ( map ){ + this.map = map; +}; + +GMarker.prototype.getMap = function (){ + return this.map; +} + +GMarker.prototype.addedToMap = function (){ + this.map = null; +}; + + +GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow; +GMarker.prototype.openInfoWindow = function ( node, opts ){ + if ( this.map != null ) + return this.map.openInfoWindow( this.getPoint(), node, opts ); + else + return this.origOpenInfoWindow( node, opts ); +}; + +GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml; +GMarker.prototype.openInfoWindowHtml = function ( html, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowHtml( this.getPoint(), html, opts ); + else + return this.origOpenInfoWindowHtml( html, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs; +GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts ); + else + return this.origOpenInfoWindowTabs( tabNodes, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml; +GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts ); + else + return this.origOpenInfoWindowTabsHtml( tabHtmls, opts ); +}; + +GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup; +GMarker.prototype.showMapBlowup = function ( opts ){ + if ( this.map != null ) + return this.map.showMapBlowup( this.getPoint(), opts ); + else + return this.origShowMapBlowup( opts ); +}; + + +function addDescriptionToMarker(marker, description){ + marker.description = description; + return marker; +} diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js new file mode 100644 index 0000000..8c273f8 --- /dev/null +++ b/public/javascripts/controls.js @@ -0,0 +1,833 @@ +// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// Autocompleter.Base handles all the autocompletion functionality +// that's independent of the data source for autocompletion. This +// includes drawing the autocompletion menu, observing keyboard +// and mouse events, and similar. +// +// Specific autocompleters need to provide, at the very least, +// a getUpdatedChoices function that will be invoked every time +// the text inside the monitored textbox changes. This method +// should get the text for which to provide autocompletion by +// invoking this.getToken(), NOT by directly accessing +// this.element.value. This is to allow incremental tokenized +// autocompletion. Specific auto-completion logic (AJAX, etc) +// belongs in getUpdatedChoices. +// +// Tokenized incremental autocompletion is enabled automatically +// when an autocompleter is instantiated with the 'tokens' option +// in the options parameter, e.g.: +// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); +// will incrementally autocomplete with a comma as the token. +// Additionally, ',' in the above example can be replaced with +// a token array, e.g. { tokens: [',', '\n'] } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +if(typeof Effect == 'undefined') + throw("controls.js requires including script.aculo.us' effects.js library"); + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + baseInitialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.hasFocus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entryCount = 0; + + if(this.setOptions) + this.setOptions(options); + else + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; + this.options.frequency = this.options.frequency || 0.4; + this.options.minChars = this.options.minChars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + Position.clone(element, update, { + setHeight: false, + offsetTop: element.offsetHeight + }); + } + Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + this.element.setAttribute('autocomplete','off'); + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && + (navigator.appVersion.indexOf('MSIE')>0) && + (navigator.userAgent.indexOf('Opera')<0) && + (Element.getStyle(this.update, 'position')=='absolute')) { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50); + }, + + fixIEOverlapping: function() { + Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)}); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + }, + + hide: function() { + this.stopIndicator(); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.options.indicator) Element.show(this.options.indicator); + }, + + stopIndicator: function() { + if(this.options.indicator) Element.hide(this.options.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.selectEntry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + Event.stop(event); + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.markPrevious(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.markNext(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || + (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return; + + this.changed = true; + this.hasFocus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + activate: function() { + this.changed = false; + this.hasFocus = true; + this.getUpdatedChoices(); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.selectEntry(); + this.hide(); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.hasFocus = false; + this.active = false; + }, + + render: function() { + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) + this.index==i ? + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + + if(this.hasFocus) { + this.show(); + this.active = true; + } + } else { + this.active = false; + this.hide(); + } + }, + + markPrevious: function() { + if(this.index > 0) this.index-- + else this.index = this.entryCount-1; + this.getEntry(this.index).scrollIntoView(true); + }, + + markNext: function() { + if(this.index < this.entryCount-1) this.index++ + else this.index = 0; + this.getEntry(this.index).scrollIntoView(false); + }, + + getEntry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + getCurrentEntry: function() { + return this.getEntry(this.index); + }, + + selectEntry: function() { + this.active = false; + this.updateElement(this.getCurrentEntry()); + }, + + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + var value = ''; + if (this.options.select) { + var nodes = document.getElementsByClassName(this.options.select, selectedElement) || []; + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + } else + value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); + if (whitespace) + newValue += whitespace[0]; + this.element.value = newValue + value; + } else { + this.element.value = value; + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); + }, + + updateChoices: function(choices) { + if(!this.changed && this.hasFocus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.down()); + + if(this.update.firstChild && this.update.down().childNodes) { + this.entryCount = + this.update.down().childNodes.length; + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entryCount = 0; + } + + this.stopIndicator(); + this.index = 0; + + if(this.entryCount==1 && this.options.autoSelect) { + this.selectEntry(); + this.hide(); + } else { + this.render(); + } + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getToken().length>=this.options.minChars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var lastTokenPos = -1; + + for (var i=0; i lastTokenPos) + lastTokenPos = thisTokenPos; + } + return lastTokenPos; + } +} + +Ajax.Autocompleter = Class.create(); +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { + initialize: function(element, update, url, options) { + this.baseInitialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this); + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + + this.options.parameters = this.options.callback ? + this.options.callback(this.element, entry) : entry; + + if(this.options.defaultParams) + this.options.parameters += '&' + this.options.defaultParams; + + new Ajax.Request(this.url, this.options); + }, + + onComplete: function(request) { + this.updateChoices(request.responseText); + } + +}); + +// The local array autocompleter. Used when you'd prefer to +// inject an array of autocompletion options into the page, rather +// than sending out Ajax queries, which can be quite slow sometimes. +// +// The constructor takes four parameters. The first two are, as usual, +// the id of the monitored textbox, and id of the autocompletion menu. +// The third is the array you want to autocomplete from, and the fourth +// is the options block. +// +// Extra local autocompletion options: +// - choices - How many autocompletion choices to offer +// +// - partialSearch - If false, the autocompleter will match entered +// text only at the beginning of strings in the +// autocomplete array. Defaults to true, which will +// match text at the beginning of any *word* in the +// strings in the autocomplete array. If you want to +// search anywhere in the string, additionally set +// the option fullSearch to true (default: off). +// +// - fullSsearch - Search anywhere in autocomplete array strings. +// +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignoreCase - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.baseInitialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, + selector: function(instance) { + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); + var count = 0; + + for (var i = 0; i < instance.options.array.length && + ret.length < instance.options.choices ; i++) { + + var elem = instance.options.array[i]; + var foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("
  • " + elem.substr(0, foundPos) + "" + + elem.substr(foundPos, entry.length) + "" + elem.substr( + foundPos + entry.length) + "
  • "); + break; + } + } + + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); + +// AJAX in-place editor +// +// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor + +// Use this if you notice weird scrolling problems on some browsers, +// the DOM might be a bit confused when this gets called so do this +// waits 1 ms (with setTimeout) until it does the activation +Field.scrollFreeActivate = function(field) { + setTimeout(function() { + Field.activate(field); + }, 1); +} + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + paramName: "value", + okButton: true, + okText: "ok", + cancelLink: true, + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + loadingClassName: 'inplaceeditor-loading', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + submitOnBlur: false, + ajaxOptions: {}, + evalScripts: false + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function(evt) { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.createForm(); + this.element.parentNode.insertBefore(this.form, this.element); + if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField); + // stop the event to avoid a page refresh in Safari + if (evt) { + Event.stop(evt); + } + return false; + }, + createForm: function() { + this.form = document.createElement("form"); + this.form.id = this.options.formId; + Element.addClassName(this.form, this.options.formClassName) + this.form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(); + + if (this.options.textarea) { + var br = document.createElement("br"); + this.form.appendChild(br); + } + + if (this.options.okButton) { + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + okButton.className = 'editor_ok_button'; + this.form.appendChild(okButton); + } + + if (this.options.cancelLink) { + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + cancelLink.className = 'editor_cancel'; + this.form.appendChild(cancelLink); + } + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/
    /i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/
    /gi, "\n").replace(//gi, "\n").replace(/<\/p>/gi, "\n").replace(/

    /gi, ""); + }, + createEditField: function() { + var text; + if(this.options.loadTextURL) { + text = this.options.loadingText; + } else { + text = this.getText(); + } + + var obj = this; + + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.obj = this; + textField.type = "text"; + textField.name = this.options.paramName; + textField.value = text; + textField.style.backgroundColor = this.options.highlightcolor; + textField.className = 'editor_field'; + var size = this.options.size || this.options.cols || 0; + if (size != 0) textField.size = size; + if (this.options.submitOnBlur) + textField.onblur = this.onSubmit.bind(this); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.obj = this; + textArea.name = this.options.paramName; + textArea.value = this.convertHTMLLineBreaks(text); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + textArea.className = 'editor_field'; + if (this.options.submitOnBlur) + textArea.onblur = this.onSubmit.bind(this); + this.editField = textArea; + } + + if(this.options.loadTextURL) { + this.loadExternalText(); + } + this.form.appendChild(this.editField); + }, + getText: function() { + return this.element.innerHTML; + }, + loadExternalText: function() { + Element.addClassName(this.form, this.options.loadingClassName); + this.editField.disabled = true; + new Ajax.Request( + this.options.loadTextURL, + Object.extend({ + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + }, this.options.ajaxOptions) + ); + }, + onLoadedExternalText: function(transport) { + Element.removeClassName(this.form, this.options.loadingClassName); + this.editField.disabled = false; + this.editField.value = transport.responseText.stripTags(); + Field.scrollFreeActivate(this.editField); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + // onLoading resets these so we need to save them away for the Ajax call + var form = this.form; + var value = this.editField.value; + + // do this first, sometimes the ajax call returns before we get a chance to switch on Saving... + // which means this will actually switch on Saving... *after* we've left edit mode causing Saving... + // to be displayed indefinitely + this.onLoading(); + + if (this.options.evalScripts) { + new Ajax.Request( + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this), + asynchronous:true, + evalScripts:true + }, this.options.ajaxOptions)); + } else { + new Ajax.Updater( + { success: this.element, + // don't update on failure (this could be an option) + failure: null }, + this.url, Object.extend({ + parameters: this.options.callback(form, value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions)); + } + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + if (this.form.parentNode) Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +}; + +Ajax.InPlaceCollectionEditor = Class.create(); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype); +Object.extend(Ajax.InPlaceCollectionEditor.prototype, { + createEditField: function() { + if (!this.cached_selectTag) { + var selectTag = document.createElement("select"); + var collection = this.options.collection || []; + var optionTag; + collection.each(function(e,i) { + optionTag = document.createElement("option"); + optionTag.value = (e instanceof Array) ? e[0] : e; + if((typeof this.options.value == 'undefined') && + ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true; + if(this.options.value==optionTag.value) optionTag.selected = true; + optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e)); + selectTag.appendChild(optionTag); + }.bind(this)); + this.cached_selectTag = selectTag; + } + + this.editField = this.cached_selectTag; + if(this.options.loadTextURL) this.loadExternalText(); + this.form.appendChild(this.editField); + this.options.callback = function(form, value) { + return "value=" + encodeURIComponent(value); + } + } +}); + +// Delayed observer, like Form.Element.Observer, +// but waits for delay after last key input +// Ideal for live-search fields + +Form.Element.DelayedObserver = Class.create(); +Form.Element.DelayedObserver.prototype = { + initialize: function(element, delay, callback) { + this.delay = delay || 0.5; + this.element = $(element); + this.callback = callback; + this.timer = null; + this.lastValue = $F(this.element); + Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this)); + }, + delayedListener: function(event) { + if(this.lastValue == $F(this.element)) return; + if(this.timer) clearTimeout(this.timer); + this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000); + this.lastValue = $F(this.element); + }, + onTimerEvent: function() { + this.timer = null; + this.callback(this.element, $F(this.element)); + } +}; diff --git a/public/javascripts/dragdrop.js b/public/javascripts/dragdrop.js new file mode 100644 index 0000000..c71ddb8 --- /dev/null +++ b/public/javascripts/dragdrop.js @@ -0,0 +1,942 @@ +// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +if(typeof Effect == 'undefined') + throw("dragdrop.js requires including script.aculo.us' effects.js library"); + +var Droppables = { + drops: [], + + remove: function(element) { + this.drops = this.drops.reject(function(d) { return d.element==$(element) }); + }, + + add: function(element) { + element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null, + tree: false + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = []; + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + containment.each( function(c) { options._containers.push($(c)) }); + } else { + options._containers.push($(containment)); + } + } + + if(options.accept) options.accept = [options.accept].flatten(); + + Element.makePositioned(element); // fix IE + options.element = element; + + this.drops.push(options); + }, + + findDeepestChild: function(drops) { + deepest = drops[0]; + + for (i = 1; i < drops.length; ++i) + if (Element.isParent(drops[i].element, deepest.element)) + deepest = drops[i]; + + return deepest; + }, + + isContained: function(element, drop) { + var containmentNode; + if(drop.tree) { + containmentNode = element.treeNode; + } else { + containmentNode = element.parentNode; + } + return drop._containers.detect(function(c) { return containmentNode == c }); + }, + + isAffected: function(point, element, drop) { + return ( + (drop.element!=element) && + ((!drop._containers) || + this.isContained(element, drop)) && + ((!drop.accept) || + (Element.classNames(element).detect( + function(v) { return drop.accept.include(v) } ) )) && + Position.within(drop.element, point[0], point[1]) ); + }, + + deactivate: function(drop) { + if(drop.hoverclass) + Element.removeClassName(drop.element, drop.hoverclass); + this.last_active = null; + }, + + activate: function(drop) { + if(drop.hoverclass) + Element.addClassName(drop.element, drop.hoverclass); + this.last_active = drop; + }, + + show: function(point, element) { + if(!this.drops.length) return; + var affected = []; + + if(this.last_active) this.deactivate(this.last_active); + this.drops.each( function(drop) { + if(Droppables.isAffected(point, element, drop)) + affected.push(drop); + }); + + if(affected.length>0) { + drop = Droppables.findDeepestChild(affected); + Position.within(drop.element, point[0], point[1]); + if(drop.onHover) + drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); + + Droppables.activate(drop); + } + }, + + fire: function(event, element) { + if(!this.last_active) return; + Position.prepare(); + + if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) + if (this.last_active.onDrop) + this.last_active.onDrop(element, this.last_active.element, event); + }, + + reset: function() { + if(this.last_active) + this.deactivate(this.last_active); + } +} + +var Draggables = { + drags: [], + observers: [], + + register: function(draggable) { + if(this.drags.length == 0) { + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.updateDrag.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + } + this.drags.push(draggable); + }, + + unregister: function(draggable) { + this.drags = this.drags.reject(function(d) { return d==draggable }); + if(this.drags.length == 0) { + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + } + }, + + activate: function(draggable) { + if(draggable.options.delay) { + this._timeout = setTimeout(function() { + Draggables._timeout = null; + window.focus(); + Draggables.activeDraggable = draggable; + }.bind(this), draggable.options.delay); + } else { + window.focus(); // allows keypress events if window isn't currently focused, fails for Safari + this.activeDraggable = draggable; + } + }, + + deactivate: function() { + this.activeDraggable = null; + }, + + updateDrag: function(event) { + if(!this.activeDraggable) return; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + // Mozilla-based browsers fire successive mousemove events with + // the same coordinates, prevent needless redrawing (moz bug?) + if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; + this._lastPointer = pointer; + + this.activeDraggable.updateDrag(event, pointer); + }, + + endDrag: function(event) { + if(this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if(!this.activeDraggable) return; + this._lastPointer = null; + this.activeDraggable.endDrag(event); + this.activeDraggable = null; + }, + + keyPress: function(event) { + if(this.activeDraggable) + this.activeDraggable.keyPress(event); + }, + + addObserver: function(observer) { + this.observers.push(observer); + this._cacheObserverCallbacks(); + }, + + removeObserver: function(element) { // element instead of observer fixes mem leaks + this.observers = this.observers.reject( function(o) { return o.element==element }); + this._cacheObserverCallbacks(); + }, + + notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' + if(this[eventName+'Count'] > 0) + this.observers.each( function(o) { + if(o[eventName]) o[eventName](eventName, draggable, event); + }); + if(draggable.options[eventName]) draggable.options[eventName](draggable, event); + }, + + _cacheObserverCallbacks: function() { + ['onStart','onEnd','onDrag'].each( function(eventName) { + Draggables[eventName+'Count'] = Draggables.observers.select( + function(o) { return o[eventName]; } + ).length; + }); + } +} + +/*--------------------------------------------------------------------------*/ + +var Draggable = Class.create(); +Draggable._dragging = {}; + +Draggable.prototype = { + initialize: function(element) { + var defaults = { + handle: false, + reverteffect: function(element, top_offset, left_offset) { + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, + queue: {scope:'_draggable', position:'end'} + }); + }, + endeffect: function(element) { + var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0; + new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, + queue: {scope:'_draggable', position:'end'}, + afterFinish: function(){ + Draggable._dragging[element] = false + } + }); + }, + zindex: 1000, + revert: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } + delay: 0 + }; + + if(!arguments[1] || typeof arguments[1].endeffect == 'undefined') + Object.extend(defaults, { + starteffect: function(element) { + element._opacity = Element.getOpacity(element); + Draggable._dragging[element] = true; + new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); + } + }); + + var options = Object.extend(defaults, arguments[1] || {}); + + this.element = $(element); + + if(options.handle && (typeof options.handle == 'string')) + this.handle = this.element.down('.'+options.handle, 0); + + if(!this.handle) this.handle = $(options.handle); + if(!this.handle) this.handle = this.element; + + if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { + options.scroll = $(options.scroll); + this._isScrollChild = Element.childOf(this.element, options.scroll); + } + + Element.makePositioned(this.element); // fix IE + + this.delta = this.currentDelta(); + this.options = options; + this.dragging = false; + + this.eventMouseDown = this.initDrag.bindAsEventListener(this); + Event.observe(this.handle, "mousedown", this.eventMouseDown); + + Draggables.register(this); + }, + + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Draggables.unregister(this); + }, + + currentDelta: function() { + return([ + parseInt(Element.getStyle(this.element,'left') || '0'), + parseInt(Element.getStyle(this.element,'top') || '0')]); + }, + + initDrag: function(event) { + if(typeof Draggable._dragging[this.element] != 'undefined' && + Draggable._dragging[this.element]) return; + if(Event.isLeftClick(event)) { + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='OPTION' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var pos = Position.cumulativeOffset(this.element); + this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); + + Draggables.activate(this); + Event.stop(event); + } + }, + + startDrag: function(event) { + this.dragging = true; + + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + + if(this.options.scroll) { + if (this.options.scroll == window) { + var where = this._getWindowScroll(this.options.scroll); + this.originalScrollLeft = where.left; + this.originalScrollTop = where.top; + } else { + this.originalScrollLeft = this.options.scroll.scrollLeft; + this.originalScrollTop = this.options.scroll.scrollTop; + } + } + + Draggables.notify('onStart', this, event); + + if(this.options.starteffect) this.options.starteffect(this.element); + }, + + updateDrag: function(event, pointer) { + if(!this.dragging) this.startDrag(event); + Position.prepare(); + Droppables.show(pointer, this.element); + Draggables.notify('onDrag', this, event); + + this.draw(pointer); + if(this.options.change) this.options.change(this); + + if(this.options.scroll) { + this.stopScrolling(); + + var p; + if (this.options.scroll == window) { + with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } + } else { + p = Position.page(this.options.scroll); + p[0] += this.options.scroll.scrollLeft + Position.deltaX; + p[1] += this.options.scroll.scrollTop + Position.deltaY; + p.push(p[0]+this.options.scroll.offsetWidth); + p.push(p[1]+this.options.scroll.offsetHeight); + } + var speed = [0,0]; + if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); + if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); + if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); + if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); + this.startScrolling(speed); + } + + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + + Event.stop(event); + }, + + finishDrag: function(event, success) { + this.dragging = false; + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + + if(success) Droppables.fire(event, this.element); + Draggables.notify('onEnd', this, event); + + var revert = this.options.revert; + if(revert && typeof revert == 'function') revert = revert(this.element); + + var d = this.currentDelta(); + if(revert && this.options.reverteffect) { + this.options.reverteffect(this.element, + d[1]-this.delta[1], d[0]-this.delta[0]); + } else { + this.delta = d; + } + + if(this.options.zindex) + this.element.style.zIndex = this.originalZ; + + if(this.options.endeffect) + this.options.endeffect(this.element); + + Draggables.deactivate(this); + Droppables.reset(); + }, + + keyPress: function(event) { + if(event.keyCode!=Event.KEY_ESC) return; + this.finishDrag(event, false); + Event.stop(event); + }, + + endDrag: function(event) { + if(!this.dragging) return; + this.stopScrolling(); + this.finishDrag(event, true); + Event.stop(event); + }, + + draw: function(point) { + var pos = Position.cumulativeOffset(this.element); + if(this.options.ghosting) { + var r = Position.realOffset(this.element); + pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; + } + + var d = this.currentDelta(); + pos[0] -= d[0]; pos[1] -= d[1]; + + if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { + pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; + pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; + } + + var p = [0,1].map(function(i){ + return (point[i]-pos[i]-this.offset[i]) + }.bind(this)); + + if(this.options.snap) { + if(typeof this.options.snap == 'function') { + p = this.options.snap(p[0],p[1],this); + } else { + if(this.options.snap instanceof Array) { + p = p.map( function(v, i) { + return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this)) + } else { + p = p.map( function(v) { + return Math.round(v/this.options.snap)*this.options.snap }.bind(this)) + } + }} + + var style = this.element.style; + if((!this.options.constraint) || (this.options.constraint=='horizontal')) + style.left = p[0] + "px"; + if((!this.options.constraint) || (this.options.constraint=='vertical')) + style.top = p[1] + "px"; + + if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering + }, + + stopScrolling: function() { + if(this.scrollInterval) { + clearInterval(this.scrollInterval); + this.scrollInterval = null; + Draggables._lastScrollPointer = null; + } + }, + + startScrolling: function(speed) { + if(!(speed[0] || speed[1])) return; + this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; + this.lastScrolled = new Date(); + this.scrollInterval = setInterval(this.scroll.bind(this), 10); + }, + + scroll: function() { + var current = new Date(); + var delta = current - this.lastScrolled; + this.lastScrolled = current; + if(this.options.scroll == window) { + with (this._getWindowScroll(this.options.scroll)) { + if (this.scrollSpeed[0] || this.scrollSpeed[1]) { + var d = delta / 1000; + this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); + } + } + } else { + this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; + this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; + } + + Position.prepare(); + Droppables.show(Draggables._lastPointer, this.element); + Draggables.notify('onDrag', this); + if (this._isScrollChild) { + Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); + Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; + Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; + if (Draggables._lastScrollPointer[0] < 0) + Draggables._lastScrollPointer[0] = 0; + if (Draggables._lastScrollPointer[1] < 0) + Draggables._lastScrollPointer[1] = 0; + this.draw(Draggables._lastScrollPointer); + } + + if(this.options.change) this.options.change(this); + }, + + _getWindowScroll: function(w) { + var T, L, W, H; + with (w.document) { + if (w.document.documentElement && documentElement.scrollTop) { + T = documentElement.scrollTop; + L = documentElement.scrollLeft; + } else if (w.document.body) { + T = body.scrollTop; + L = body.scrollLeft; + } + if (w.innerWidth) { + W = w.innerWidth; + H = w.innerHeight; + } else if (w.document.documentElement && documentElement.clientWidth) { + W = documentElement.clientWidth; + H = documentElement.clientHeight; + } else { + W = body.offsetWidth; + H = body.offsetHeight + } + } + return { top: T, left: L, width: W, height: H }; + } +} + +/*--------------------------------------------------------------------------*/ + +var SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + + onEnd: function() { + Sortable.unmark(); + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +var Sortable = { + SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, + + sortables: {}, + + _findRootElement: function(element) { + while (element.tagName != "BODY") { + if(element.id && Sortable.sortables[element.id]) return element; + element = element.parentNode; + } + }, + + options: function(element) { + element = Sortable._findRootElement($(element)); + if(!element) return; + return Sortable.sortables[element.id]; + }, + + destroy: function(element){ + var s = Sortable.options(element); + + if(s) { + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + + delete Sortable.sortables[s.element.id]; + } + }, + + create: function(element) { + element = $(element); + var options = Object.extend({ + element: element, + tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, + treeTag: 'ul', + overlap: 'vertical', // one of 'vertical', 'horizontal' + constraint: 'vertical', // one of 'vertical', 'horizontal', false + containment: element, // also takes array of elements (or id's); or false + handle: false, // or a CSS class + only: false, + delay: 0, + hoverclass: null, + ghosting: false, + scroll: false, + scrollSensitivity: 20, + scrollSpeed: 15, + format: this.SERIALIZE_RULE, + onChange: Prototype.emptyFunction, + onUpdate: Prototype.emptyFunction + }, arguments[1] || {}); + + // clear any old sortable with same element + this.destroy(element); + + // build options for the draggables + var options_for_draggable = { + revert: true, + scroll: options.scroll, + scrollSpeed: options.scrollSpeed, + scrollSensitivity: options.scrollSensitivity, + delay: options.delay, + ghosting: options.ghosting, + constraint: options.constraint, + handle: options.handle }; + + if(options.starteffect) + options_for_draggable.starteffect = options.starteffect; + + if(options.reverteffect) + options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + + if(options.endeffect) + options_for_draggable.endeffect = options.endeffect; + + if(options.zindex) + options_for_draggable.zindex = options.zindex; + + // build options for the droppables + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // drop on empty handling + if(options.dropOnEmpty || options.tree) { + Droppables.add(element, options_for_tree); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + $(e).down('.'+options.handle,0) : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + if(options.tree) e.treeNode = element; + options.droppables.push(e); + }); + + if(options.tree) { + (Sortable.findTreeElements(element, options) || []).each( function(e) { + Droppables.add(e, options_for_tree); + e.treeNode = element; + options.droppables.push(e); + }); + } + + // keep reference + this.sortables[element.id] = options; + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.tag); + }, + + findTreeElements: function(element, options) { + return Element.findChildren( + element, options.only, options.tree ? true : false, options.treeTag); + }, + + onHover: function(element, dropon, overlap) { + if(Element.isParent(dropon, element)) return; + + if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { + return; + } else if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon, overlap) { + var oldParentNode = element.parentNode; + var droponOptions = Sortable.options(dropon); + + if(!Element.isParent(dropon, element)) { + var index; + + var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); + var child = null; + + if(children) { + var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); + + for (index = 0; index < children.length; index += 1) { + if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { + offset -= Element.offsetSize (children[index], droponOptions.overlap); + } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { + child = index + 1 < children.length ? children[index + 1] : null; + break; + } else { + child = children[index]; + break; + } + } + } + + dropon.insertBefore(element, child); + + Sortable.options(oldParentNode).onChange(element); + droponOptions.onChange(element); + } + }, + + unmark: function() { + if(Sortable._marker) Sortable._marker.hide(); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = + ($('dropmarker') || Element.extend(document.createElement('DIV'))). + hide().addClassName('dropmarker').setStyle({position:'absolute'}); + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); + + if(position=='after') + if(sortable.overlap == 'horizontal') + Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); + else + Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); + + Sortable._marker.show(); + }, + + _tree: function(element, options, parent) { + var children = Sortable.findElements(element, options) || []; + + for (var i = 0; i < children.length; ++i) { + var match = children[i].id.match(options.format); + + if (!match) continue; + + var child = { + id: encodeURIComponent(match ? match[1] : null), + element: element, + parent: parent, + children: [], + position: parent.children.length, + container: $(children[i]).down(options.treeTag) + } + + /* Get the element containing the children and recurse over it */ + if (child.container) + this._tree(child.container, options, child) + + parent.children.push (child); + } + + return parent; + }, + + tree: function(element) { + element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + treeTag: sortableOptions.treeTag, + only: sortableOptions.only, + name: element.id, + format: sortableOptions.format + }, arguments[1] || {}); + + var root = { + id: null, + parent: null, + children: [], + container: element, + position: 0 + } + + return Sortable._tree(element, options, root); + }, + + /* Construct a [i] index for a particular node */ + _constructIndex: function(node) { + var index = ''; + do { + if (node.id) index = '[' + node.position + ']' + index; + } while ((node = node.parent) != null); + return index; + }, + + sequence: function(element) { + element = $(element); + var options = Object.extend(this.options(element), arguments[1] || {}); + + return $(this.findElements(element, options) || []).map( function(item) { + return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; + }); + }, + + setSequence: function(element, new_sequence) { + element = $(element); + var options = Object.extend(this.options(element), arguments[2] || {}); + + var nodeMap = {}; + this.findElements(element, options).each( function(n) { + if (n.id.match(options.format)) + nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; + n.parentNode.removeChild(n); + }); + + new_sequence.each(function(ident) { + var n = nodeMap[ident]; + if (n) { + n[1].appendChild(n[0]); + delete nodeMap[ident]; + } + }); + }, + + serialize: function(element) { + element = $(element); + var options = Object.extend(Sortable.options(element), arguments[1] || {}); + var name = encodeURIComponent( + (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); + + if (options.tree) { + return Sortable.tree(element, arguments[1]).children.map( function (item) { + return [name + Sortable._constructIndex(item) + "[id]=" + + encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); + }).flatten().join('&'); + } else { + return Sortable.sequence(element, arguments[1]).map( function(item) { + return name + "[]=" + encodeURIComponent(item); + }).join('&'); + } + } +} + +// Returns true if child is contained within element +Element.isParent = function(child, element) { + if (!child.parentNode || child == element) return false; + if (child.parentNode == element) return true; + return Element.isParent(child.parentNode, element); +} + +Element.findChildren = function(element, only, recursive, tagName) { + if(!element.hasChildNodes()) return null; + tagName = tagName.toUpperCase(); + if(only) only = [only].flatten(); + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName.toUpperCase()==tagName && + (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) + elements.push(e); + if(recursive) { + var grandchildren = Element.findChildren(e, only, recursive, tagName); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : []); +} + +Element.offsetSize = function (element, type) { + return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; +} diff --git a/public/javascripts/effects.js b/public/javascripts/effects.js new file mode 100644 index 0000000..3b02eda --- /dev/null +++ b/public/javascripts/effects.js @@ -0,0 +1,1088 @@ +// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki +// +// script.aculo.us is freely distributable under the terms of an MIT-style license. +// For details, see the script.aculo.us web site: http://script.aculo.us/ + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + var color = '#'; + if(this.slice(0,4) == 'rgb(') { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +/*--------------------------------------------------------------------------*/ + +Element.collectTextNodes = function(element) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + (node.hasChildNodes() ? Element.collectTextNodes(node) : '')); + }).flatten().join(''); +} + +Element.collectTextNodesIgnoreClass = function(element, className) { + return $A($(element).childNodes).collect( function(node) { + return (node.nodeType==3 ? node.nodeValue : + ((node.hasChildNodes() && !Element.hasClassName(node,className)) ? + Element.collectTextNodesIgnoreClass(node, className) : '')); + }).flatten().join(''); +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.setStyle({fontSize: (percent/100) + 'em'}); + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + return element; +} + +Element.getOpacity = function(element){ + element = $(element); + var opacity; + if (opacity = element.getStyle('opacity')) + return parseFloat(opacity); + if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(opacity[1]) return parseFloat(opacity[1]) / 100; + return 1.0; +} + +Element.setOpacity = function(element, value){ + element= $(element); + if (value == 1){ + element.setStyle({ opacity: + (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? + 0.999999 : 1.0 }); + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')}); + } else { + if(value < 0.00001) value = 0; + element.setStyle({opacity: value}); + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.setStyle( + { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')' }); + } + return element; +} + +Element.getInlineOpacity = function(element){ + return $(element).style.opacity || ''; +} + +Element.forceRerendering = function(element) { + try { + element = $(element); + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch(e) { } +}; + +/*--------------------------------------------------------------------------*/ + +Array.prototype.call = function() { + var args = arguments; + this.each(function(f){ f.apply(this, args) }); +} + +/*--------------------------------------------------------------------------*/ + +var Effect = { + _elementDoesNotExistError: { + name: 'ElementDoesNotExistError', + message: 'The specified DOM element does not exist, but is required for this effect to operate' + }, + tagifyText: function(element) { + if(typeof Builder == 'undefined') + throw("Effect.tagifyText requires including script.aculo.us' builder.js library"); + + var tagifyStyle = 'position:relative'; + if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1'; + + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == ' ' ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var masterDelay = options.delay; + + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); + }); + }, + PAIRS: { + 'slide': ['SlideDown','SlideUp'], + 'blind': ['BlindDown','BlindUp'], + 'appear': ['Appear','Fade'] + }, + toggle: function(element, effect) { + element = $(element); + effect = (effect || 'appear').toLowerCase(); + var options = Object.extend({ + queue: { position:'end', scope:(element.id || 'global'), limit: 1 } + }, arguments[2] || {}); + Effect[element.visible() ? + Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); + } +}; + +var Effect2 = Effect; // deprecated + +/* ------------- transitions ------------- */ + +Effect.Transitions = { + linear: Prototype.K, + sinoidal: function(pos) { + return (-Math.cos(pos*Math.PI)/2) + 0.5; + }, + reverse: function(pos) { + return 1-pos; + }, + flicker: function(pos) { + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; + }, + wobble: function(pos) { + return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; + }, + pulse: function(pos, pulses) { + pulses = pulses || 5; + return ( + Math.round((pos % (1/pulses)) * pulses) == 0 ? + ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : + 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) + ); + }, + none: function(pos) { + return 0; + }, + full: function(pos) { + return 1; + } +}; + +/* ------------- core effects ------------- */ + +Effect.ScopedQueue = Class.create(); +Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), { + initialize: function() { + this.effects = []; + this.interval = null; + }, + _each: function(iterator) { + this.effects._each(iterator); + }, + add: function(effect) { + var timestamp = new Date().getTime(); + + var position = (typeof effect.options.queue == 'string') ? + effect.options.queue : effect.options.queue.position; + + switch(position) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'with-last': + timestamp = this.effects.pluck('startOn').max() || timestamp; + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + + if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) + this.effects.push(effect); + + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } +}); + +Effect.Queues = { + instances: $H(), + get: function(queueName) { + if(typeof queueName != 'string') return queueName; + + if(!this.instances[queueName]) + this.instances[queueName] = new Effect.ScopedQueue(); + + return this.instances[queueName]; + } +} +Effect.Queue = Effect.Queues.get('global'); + +Effect.DefaultOptions = { + transition: Effect.Transitions.sinoidal, + duration: 1.0, // seconds + fps: 25.0, // max. 25fps due to Effect.Queue implementation + sync: false, // true for combining + from: 0.0, + to: 1.0, + delay: 0.0, + queue: 'parallel' +} + +Effect.Base = function() {}; +Effect.Base.prototype = { + position: null, + start: function(options) { + this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {}); + this.currentFrame = 0; + this.state = 'idle'; + this.startOn = this.options.delay*1000; + this.finishOn = this.startOn + (this.options.duration*1000); + this.event('beforeStart'); + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).add(this); + }, + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + } + }, + render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } + if(this.state == 'running') { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + this.position = pos; + this.event('beforeUpdate'); + if(this.update) this.update(pos); + this.event('afterUpdate'); + } + }, + cancel: function() { + if(!this.options.sync) + Effect.Queues.get(typeof this.options.queue == 'string' ? + 'global' : this.options.queue.scope).remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); + }, + inspect: function() { + return '#'; + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + this.effects.invoke('render', position); + }, + finish: function(position) { + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); + } +}); + +Effect.Event = Class.create(); +Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), { + initialize: function() { + var options = Object.extend({ + duration: 0 + }, arguments[0] || {}); + this.start(options); + }, + update: Prototype.emptyFunction +}); + +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout)) + this.element.setStyle({zoom: 1}); + var options = Object.extend({ + from: this.element.getOpacity() || 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.element.setOpacity(position); + } +}); + +Effect.Move = Class.create(); +Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + x: 0, + y: 0, + mode: 'relative' + }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + this.element.makePositioned(); + this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); + this.originalTop = parseFloat(this.element.getStyle('top') || '0'); + if(this.options.mode == 'absolute') { + // absolute movement, so we need to calc deltaX and deltaY + this.options.x = this.options.x - this.originalLeft; + this.options.y = this.options.y - this.originalTop; + } + }, + update: function(position) { + this.element.setStyle({ + left: Math.round(this.options.x * position + this.originalLeft) + 'px', + top: Math.round(this.options.y * position + this.originalTop) + 'px' + }); + } +}); + +// for backwards compatibility +Effect.MoveBy = function(element, toTop, toLeft) { + return new Effect.Move(element, + Object.extend({ x: toLeft, y: toTop }, arguments[3] || {})); +}; + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0, + scaleTo: percent + }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = this.element.getStyle('position'); + + this.originalStyle = {}; + ['top','left','width','height','fontSize'].each( function(k) { + this.originalStyle[k] = this.element.style[k]; + }.bind(this)); + + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + + var fontSize = this.element.getStyle('font-size') || '100%'; + ['em','px','%','pt'].each( function(fontSizeType) { + if(fontSize.indexOf(fontSizeType)>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = fontSizeType; + } + }.bind(this)); + + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + + this.dims = null; + if(this.options.scaleMode=='box') + this.dims = [this.element.offsetHeight, this.element.offsetWidth]; + if(/^content/.test(this.options.scaleMode)) + this.dims = [this.element.scrollHeight, this.element.scrollWidth]; + if(!this.dims) + this.dims = [this.options.scaleMode.originalHeight, + this.options.scaleMode.originalWidth]; + }, + update: function(position) { + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); + this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); + }, + finish: function(position) { + if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle); + }, + setDimensions: function(height, width) { + var d = {}; + if(this.options.scaleX) d.width = Math.round(width) + 'px'; + if(this.options.scaleY) d.height = Math.round(height) + 'px'; + if(this.options.scaleFromCenter) { + var topd = (height - this.dims[0])/2; + var leftd = (width - this.dims[1])/2; + if(this.elementPositioning == 'absolute') { + if(this.options.scaleY) d.top = this.originalTop-topd + 'px'; + if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; + } else { + if(this.options.scaleY) d.top = -topd + 'px'; + if(this.options.scaleX) d.left = -leftd + 'px'; + } + } + this.element.setStyle(d); + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {}); + this.start(options); + }, + setup: function() { + // Prevent executing on elements not in the layout flow + if(this.element.getStyle('display')=='none') { this.cancel(); return; } + // Disable background image during the effect + this.oldStyle = { + backgroundImage: this.element.getStyle('background-image') }; + this.element.setStyle({backgroundImage: 'none'}); + if(!this.options.endcolor) + this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); + if(!this.options.restorecolor) + this.options.restorecolor = this.element.getStyle('background-color'); + // init color calculations + this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); + this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); + }, + update: function(position) { + this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) }); + }, + finish: function() { + this.element.setStyle(Object.extend(this.oldStyle, { + backgroundColor: this.options.restorecolor + })); + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + if(this.options.offset) offsets[1] += this.options.offset; + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- combination effects ------------- */ + +Effect.Fade = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + var options = Object.extend({ + from: element.getOpacity() || 1.0, + to: 0.0, + afterFinishInternal: function(effect) { + if(effect.options.to!=0) return; + effect.element.hide().setStyle({opacity: oldOpacity}); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + element = $(element); + var options = Object.extend({ + from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), + to: 1.0, + // force Safari to render floated elements properly + afterFinishInternal: function(effect) { + effect.element.forceRerendering(); + }, + beforeSetup: function(effect) { + effect.element.setOpacity(effect.options.from).show(); + }}, arguments[1] || {}); + return new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + element = $(element); + var oldStyle = { + opacity: element.getInlineOpacity(), + position: element.getStyle('position'), + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height + }; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { + Position.absolutize(effect.effects[0].element) + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().setStyle(oldStyle); } + }, arguments[1] || {}) + ); +} + +Effect.BlindUp = function(element) { + element = $(element); + element.makeClipping(); + return new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + restoreAfterFinish: true, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + element = $(element); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: 0, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping(); + } + }, arguments[1] || {})); +} + +Effect.SwitchOff = function(element) { + element = $(element); + var oldOpacity = element.getInlineOpacity(); + return new Effect.Appear(element, Object.extend({ + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); + } + }) + } + }, arguments[1] || {})); +} + +Effect.DropOut = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left'), + opacity: element.getInlineOpacity() }; + return new Effect.Parallel( + [ new Effect.Move(element, {x: 0, y: 100, sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + effect.effects[0].element.makePositioned(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); + } + }, arguments[1] || {})); +} + +Effect.Shake = function(element) { + element = $(element); + var oldStyle = { + top: element.getStyle('top'), + left: element.getStyle('left') }; + return new Effect.Move(element, + { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) { + new Effect.Move(effect.element, + { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) { + effect.element.undoPositioned().setStyle(oldStyle); + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element).cleanWhitespace(); + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.down().getStyle('bottom'); + var elementDimensions = element.getDimensions(); + return new Effect.Scale(element, 100, Object.extend({ + scaleContent: false, + scaleX: false, + scaleFrom: window.opera ? 0 : 1, + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().setStyle({height: '0px'}).show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.undoClipping().undoPositioned(); + effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element).cleanWhitespace(); + var oldInnerBottom = element.down().getStyle('bottom'); + return new Effect.Scale(element, window.opera ? 0 : 1, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + effect.element.makePositioned(); + effect.element.down().makePositioned(); + if(window.opera) effect.element.setStyle({top: ''}); + effect.element.makeClipping().show(); + }, + afterUpdateInternal: function(effect) { + effect.element.down().setStyle({bottom: + (effect.dims[0] - effect.element.clientHeight) + 'px' }); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom}); + effect.element.down().undoPositioned(); + } + }, arguments[1] || {}) + ); +} + +// Bug in opera makes the TD containing this element expand for a instance after finish +Effect.Squish = function(element) { + return new Effect.Scale(element, window.opera ? 1 : 0, { + restoreAfterFinish: true, + beforeSetup: function(effect) { + effect.element.makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping(); + } + }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.full + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = dims.width; + initialMoveY = moveY = 0; + moveX = -dims.width; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = dims.height; + moveY = -dims.height; + break; + case 'bottom-right': + initialMoveX = dims.width; + initialMoveY = dims.height; + moveX = -dims.width; + moveY = -dims.height; + break; + case 'center': + initialMoveX = dims.width / 2; + initialMoveY = dims.height / 2; + moveX = -dims.width / 2; + moveY = -dims.height / 2; + break; + } + + return new Effect.Move(element, { + x: initialMoveX, + y: initialMoveY, + duration: 0.01, + beforeSetup: function(effect) { + effect.element.hide().makeClipping().makePositioned(); + }, + afterFinishInternal: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), + new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), + new Effect.Scale(effect.element, 100, { + scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, + sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.setStyle({height: '0px'}).show(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); + } + }, options) + ) + } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = Object.extend({ + direction: 'center', + moveTransition: Effect.Transitions.sinoidal, + scaleTransition: Effect.Transitions.sinoidal, + opacityTransition: Effect.Transitions.none + }, arguments[1] || {}); + var oldStyle = { + top: element.style.top, + left: element.style.left, + height: element.style.height, + width: element.style.width, + opacity: element.getInlineOpacity() }; + + var dims = element.getDimensions(); + var moveX, moveY; + + switch (options.direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = dims.width; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = dims.height; + break; + case 'bottom-right': + moveX = dims.width; + moveY = dims.height; + break; + case 'center': + moveX = dims.width / 2; + moveY = dims.height / 2; + break; + } + + return new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), + new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + effect.effects[0].element.makePositioned().makeClipping(); + }, + afterFinishInternal: function(effect) { + effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } + }, options) + ); +} + +Effect.Pulsate = function(element) { + element = $(element); + var options = arguments[1] || {}; + var oldOpacity = element.getInlineOpacity(); + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) }; + reverser.bind(transition); + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 2.0, from: 0, + afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + element = $(element); + var oldStyle = { + top: element.style.top, + left: element.style.left, + width: element.style.width, + height: element.style.height }; + element.makeClipping(); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + effect.element.hide().undoClipping().setStyle(oldStyle); + } }); + }}, arguments[1] || {})); +}; + +Effect.Morph = Class.create(); +Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + if(!this.element) throw(Effect._elementDoesNotExistError); + var options = Object.extend({ + style: '' + }, arguments[1] || {}); + this.start(options); + }, + setup: function(){ + function parseColor(color){ + if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; + color = color.parseColor(); + return $R(0,2).map(function(i){ + return parseInt( color.slice(i*2+1,i*2+3), 16 ) + }); + } + this.transforms = this.options.style.parseStyle().map(function(property){ + var originalValue = this.element.getStyle(property[0]); + return $H({ + style: property[0], + originalValue: property[1].unit=='color' ? + parseColor(originalValue) : parseFloat(originalValue || 0), + targetValue: property[1].unit=='color' ? + parseColor(property[1].value) : property[1].value, + unit: property[1].unit + }); + }.bind(this)).reject(function(transform){ + return ( + (transform.originalValue == transform.targetValue) || + ( + transform.unit != 'color' && + (isNaN(transform.originalValue) || isNaN(transform.targetValue)) + ) + ) + }); + }, + update: function(position) { + var style = $H(), value = null; + this.transforms.each(function(transform){ + value = transform.unit=='color' ? + $R(0,2).inject('#',function(m,v,i){ + return m+(Math.round(transform.originalValue[i]+ + (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : + transform.originalValue + Math.round( + ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit; + style[transform.style] = value; + }); + this.element.setStyle(style); + } +}); + +Effect.Transform = Class.create(); +Object.extend(Effect.Transform.prototype, { + initialize: function(tracks){ + this.tracks = []; + this.options = arguments[1] || {}; + this.addTracks(tracks); + }, + addTracks: function(tracks){ + tracks.each(function(track){ + var data = $H(track).values().first(); + this.tracks.push($H({ + ids: $H(track).keys().first(), + effect: Effect.Morph, + options: { style: data } + })); + }.bind(this)); + return this; + }, + play: function(){ + return new Effect.Parallel( + this.tracks.map(function(track){ + var elements = [$(track.ids) || $$(track.ids)].flatten(); + return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) }); + }).flatten(), + this.options + ); + } +}); + +Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', + 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', + 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth', + 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor', + 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content', + 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction', + 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch', + 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight', + 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight', + 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity', + 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY', + 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore', + 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes', + 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress', + 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top', + 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows', + 'width', 'wordSpacing', 'zIndex']; + +Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; + +String.prototype.parseStyle = function(){ + var element = Element.extend(document.createElement('div')); + element.innerHTML = '

    '; + var style = element.down().style, styleRules = $H(); + + Element.CSS_PROPERTIES.each(function(property){ + if(style[property]) styleRules[property] = style[property]; + }); + + var result = $H(); + + styleRules.each(function(pair){ + var property = pair[0], value = pair[1], unit = null; + + if(value.parseColor('#zzzzzz') != '#zzzzzz') { + value = value.parseColor(); + unit = 'color'; + } else if(Element.CSS_LENGTH.test(value)) + var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/), + value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null; + + result[property.underscore().dasherize()] = $H({ value:value, unit:unit }); + }.bind(this)); + + return result; +}; + +Element.morph = function(element, style) { + new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {})); + return element; +}; + +['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom', + 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( + function(f) { Element.Methods[f] = Element[f]; } +); + +Element.Methods.visualEffect = function(element, effect, options) { + s = effect.gsub(/_/, '-').camelize(); + effect_class = s.charAt(0).toUpperCase() + s.substring(1); + new Effect[effect_class](element, options); + return $(element); +}; + +Element.addMethods(); \ No newline at end of file diff --git a/public/javascripts/geoRssOverlay.js b/public/javascripts/geoRssOverlay.js new file mode 100644 index 0000000..315c26d --- /dev/null +++ b/public/javascripts/geoRssOverlay.js @@ -0,0 +1,194 @@ +// GeoRssOverlay: GMaps API extension to display a group of markers from +// a RSS feed +// +// Copyright 2006 Mikel Maron (email: mikel_maron yahoo com) +// +// The original version of this code is called MGeoRSS and can be found +// at the following address: +// http://brainoff.com/gmaps/mgeorss.html +// +// Modified by Andrew Turner to add support for the GeoRss Simple vocabulary +// +// Modified and bundled with YM4R in accordance with the following +// license: +// +// This work is public domain + +function GeoRssOverlay(rssurl,icon,proxyurl,options){ + this.rssurl = rssurl; + this.icon = icon; + this.proxyurl = proxyurl; + if(options['visible'] == undefined) + this.visible = true; + else + this.visible = options['visible']; + this.listDiv = options['listDiv']; //ID of the item list DIV + this.contentDiv = options['contentDiv']; //ID of the content DIV + this.listItemClass = options['listItemClass']; //Class of the list item DIV + this.limitItems = options['limit']; //Maximum number of displayed entries + this.request = false; + this.markers = []; +} + +GeoRssOverlay.prototype = new GOverlay(); + +GeoRssOverlay.prototype.initialize=function(map) { + this.map = map; + this.load(); +} + +GeoRssOverlay.prototype.redraw = function(force){ + //nothing to do : the markers are already taken care of +} + +GeoRssOverlay.prototype.remove = function(){ + for(var i= 0, len = this.markers.length ; i< len; i++){ + this.map.removeOverlay(this.markers[i]); + } +} + +GeoRssOverlay.prototype.showHide=function() { + if (this.visible) { + for (var i=0;i" + title + "

    " + description; + + if(this.contentDiv == undefined){ + GEvent.addListener(marker, "click", function() { + marker.openInfoWindowHtml(html); + }); + }else{ + var contentDiv = this.contentDiv; + GEvent.addListener(marker, "click", function() { + document.getElementById(contentDiv).innerHTML = html; + }); + } + + if(this.listDiv != undefined){ + var a = document.createElement('a'); + a.innerHTML = title; + a.setAttribute("href","#"); + var georss = this; + a.onclick = function(){ + georss.showMarker(index); + return false; + }; + var div = document.createElement('div'); + if(this.listItemClass != undefined){ + div.setAttribute("class",this.listItemClass); + } + div.appendChild(a); + document.getElementById(this.listDiv).appendChild(div); + } + + return marker; +} diff --git a/public/javascripts/markerGroup.js b/public/javascripts/markerGroup.js new file mode 100644 index 0000000..02fe624 --- /dev/null +++ b/public/javascripts/markerGroup.js @@ -0,0 +1,114 @@ +function GMarkerGroup(active, markers, markersById) { + this.active = active; + this.markers = markers || new Array(); + this.markersById = markersById || new Object(); +} + +GMarkerGroup.prototype = new GOverlay(); + +GMarkerGroup.prototype.initialize = function(map) { + this.map = map; + + if(this.active){ + for(var i = 0 , len = this.markers.length; i < len; i++) { + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } +} + +//If not already done (ie if not inactive) remove all the markers from the map +GMarkerGroup.prototype.remove = function() { + this.deactivate(); +} + +GMarkerGroup.prototype.redraw = function(force){ + //Nothing to do : markers are already taken care of +} + +//Copy the data to a new Marker Group +GMarkerGroup.prototype.copy = function() { + var overlay = new GMarkerGroup(this.active); + overlay.markers = this.markers; //Need to do deep copy + overlay.markersById = this.markersById; //Need to do deep copy + return overlay; +} + +//Inactivate the Marker group and clear the internal content +GMarkerGroup.prototype.clear = function(){ + //deactivate the map first (which removes the markers from the map) + this.deactivate(); + //Clear the internal content + this.markers = new Array(); + this.markersById = new Object(); +} + +//Add a marker to the GMarkerGroup ; Adds it now to the map if the GMarkerGroup is active +GMarkerGroup.prototype.addMarker = function(marker,id){ + if(id == undefined){ + this.markers.push(marker); + }else{ + this.markersById[id] = marker; + } + if(this.active && this.map != undefined ){ + this.map.addOverlay(marker); + } +} + +//Open the info window (or info window tabs) of a marker +GMarkerGroup.prototype.showMarker = function(id){ + var marker = this.markersById[id]; + if(marker != undefined){ + GEvent.trigger(marker,"click"); + } +} + +//Activate (or deactivate depending on the argument) the GMarkerGroup +GMarkerGroup.prototype.activate = function(active){ + active = (active == undefined) ? true : active; + if(!active){ + if(this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.removeOverlay(this.markers[i]) + } + for(var id in this.markersById){ + this.map.removeOverlay(this.markersById[id]); + } + } + this.active = false; + } + }else{ + if(!this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } + this.active = true; + } + } +} + +GMarkerGroup.prototype.centerAndZoomOnMarkers = function() { + if(this.map != undefined){ + //merge markers and markersById + var tmpMarkers = this.markers.slice(); + for (var id in this.markersById){ + tmpMarkers.push(this.markersById[id]); + } + if(tmpMarkers.length > 0){ + this.map.centerAndZoomOnMarkers(tmpMarkers); + } + } +} + +//Deactivate the Group Overlay (convenience method) +GMarkerGroup.prototype.deactivate = function(){ + this.activate(false); +} diff --git a/public/javascripts/prototype.js b/public/javascripts/prototype.js new file mode 100644 index 0000000..5058221 --- /dev/null +++ b/public/javascripts/prototype.js @@ -0,0 +1,2515 @@ +/* Prototype JavaScript framework, version 1.5.0 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.5.0', + BrowserFeatures: { + XPath: !!document.evaluate + }, + + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + emptyFunction: function() {}, + K: function(x) { return x } +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (var property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.extend(Object, { + inspect: function(object) { + try { + if (object === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({}, object); + } +}); + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments))); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(this); + } finally { + this.currentlyExecuting = false; + } + } + } +} +String.interpret = function(value){ + return value == null ? '' : String(value); +} + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return this; + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : this; + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return {}; + + return match[1].split(separator || '&').inject({}, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var name = decodeURIComponent(pair[0]); + var value = pair[1] ? decodeURIComponent(pair[1]) : undefined; + + if (hash[name] !== undefined) { + if (hash[name].constructor != Array) + hash[name] = [hash[name]]; + if (value) hash[name].push(value); + } + else hash[name] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function(){ + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.replace(/\\/g, '\\\\'); + if (useDoubleQuotes) + return '"' + escapedString.replace(/"/g, '\\"') + '"'; + else + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (typeof replacement == 'function') return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +} + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var Template = Class.create(); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; +Template.prototype = { + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + return this.template.gsub(this.pattern, function(match) { + var before = match[1]; + if (before == '\\') return match[2]; + return before + String.interpret(object[match[3]]); + }); + } +} + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator) { + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.map(iterator); + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = false; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push((iterator || Prototype.K)(value, index)); + }); + return results; + }, + + detect: function(iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0, length = iterable.length; i < length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) + Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value && value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0, length = this.length; i < length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function() { + return this.inject([], function(array, value) { + return array.include(value) ? array : array.concat([value]); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string){ + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if(window.opera){ + Array.prototype.concat = function(){ + var array = []; + for(var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for(var i = 0, length = arguments.length; i < length; i++) { + if(arguments[i].constructor == Array) { + for(var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + } +} +var Hash = function(obj) { + Object.extend(this, obj || {}); +}; + +Object.extend(Hash, { + toQueryString: function(obj) { + var parts = []; + + this.prototype._each.call(obj, function(pair) { + if (!pair.key) return; + + if (pair.value && pair.value.constructor == Array) { + var values = pair.value.compact(); + if (values.length < 2) pair.value = values.reduce(); + else { + key = encodeURIComponent(pair.key); + values.each(function(value) { + value = value != undefined ? encodeURIComponent(value) : ''; + parts.push(key + '=' + encodeURIComponent(value)); + }); + return; + } + } + if (pair.value == undefined) pair[1] = ''; + parts.push(pair.map(encodeURIComponent).join('=')); + }); + + return parts.join('&'); + } +}); + +Object.extend(Hash.prototype, Enumerable); +Object.extend(Hash.prototype, { + _each: function(iterator) { + for (var key in this) { + var value = this[key]; + if (value && value == Hash.prototype[key]) continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject(this, function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + remove: function() { + var result; + for(var i = 0, length = arguments.length; i < length; i++) { + var value = this[arguments[i]]; + if (value !== undefined){ + if (result === undefined) result = value; + else { + if (result.constructor != Array) result = [result]; + result.push(value) + } + } + delete this[arguments[i]]; + } + return result; + }, + + toQueryString: function() { + return Hash.toQueryString(this); + }, + + inspect: function() { + return '#'; + } +}); + +function $H(object) { + if (object && object.constructor == Hash) return object; + return new Hash(object); +}; +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '' + } + Object.extend(this.options, options || {}); + + this.options.method = this.options.method.toLowerCase(); + if (typeof this.options.parameters == 'string') + this.options.parameters = this.options.parameters.toQueryParams(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + _complete: false, + + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = this.options.parameters; + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + params = Hash.toQueryString(params); + if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_=' + + // when GET, append parameters to URL + if (this.method == 'get' && params) + this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params; + + try { + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) + setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + var body = this.method == 'post' ? (this.options.postBody || params) : null; + + this.transport.send(body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (typeof extras.push == 'function') + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + return !this.transport.status + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + this.transport.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.getHeader('Content-type') || 'text/javascript').strip(). + match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i)) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + state, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalJSON: function() { + try { + var json = this.getHeader('X-JSON'); + return json ? eval('(' + json + ')') : null; + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, param) { + this.updateContent(); + onComplete(transport, param); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.container[this.success() ? 'success' : 'failure']; + var response = this.transport.responseText; + + if (!this.options.evalScripts) response = response.stripScripts(); + + if (receiver = $(receiver)) { + if (this.options.insertion) + new this.options.insertion(receiver, response); + else + receiver.update(response); + } + + if (this.success()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (typeof element == 'string') + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(query.snapshotItem(i)); + return results; + }; +} + +document.getElementsByClassName = function(className, parentElement) { + if (Prototype.BrowserFeatures.XPath) { + var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; + return document._getElementsByXPath(q, parentElement); + } else { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + var elements = [], child; + for (var i = 0, length = children.length; i < length; i++) { + child = children[i]; + if (Element.hasClassName(child, className)) + elements.push(Element.extend(child)); + } + return elements; + } +}; + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) + var Element = new Object(); + +Element.extend = function(element) { + if (!element || _nativeExtensions || element.nodeType == 3) return element; + + if (!element._extended && element.tagName && element != window) { + var methods = Object.clone(Element.Methods), cache = Element.extend.cache; + + if (element.tagName == 'FORM') + Object.extend(methods, Form.Methods); + if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) + Object.extend(methods, Form.Element.Methods); + + Object.extend(methods, Element.Methods.Simulated); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + } + + element._extended = true; + return element; +}; + +Element.extend.cache = { + findOrStore: function(value) { + return this[value] = this[value] || function() { + return value.apply(null, [this].concat($A(arguments))); + } + } +}; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, html) { + html = typeof html == 'undefined' ? '' : html.toString(); + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + replace: function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + if (element.outerHTML) { + element.outerHTML = html.stripScripts(); + } else { + var range = element.ownerDocument.createRange(); + range.selectNodeContents(element); + element.parentNode.replaceChild( + range.createContextualFragment(html.stripScripts()), element); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $A($(element).getElementsByTagName('*')); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (typeof selector == 'string') + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + return Selector.findElement($(element).ancestors(), expression, index); + }, + + down: function(element, expression, index) { + return Selector.findElement($(element).descendants(), expression, index); + }, + + previous: function(element, expression, index) { + return Selector.findElement($(element).previousSiblings(), expression, index); + }, + + next: function(element, expression, index) { + return Selector.findElement($(element).nextSiblings(), expression, index); + }, + + getElementsBySelector: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + getElementsByClassName: function(element, className) { + return document.getElementsByClassName(className, element); + }, + + readAttribute: function(element, name) { + element = $(element); + if (document.all && !window.opera) { + var t = Element._attributeTranslations; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + var attribute = element.attributes[name]; + if(attribute) return attribute.nodeValue; + } + return element.getAttribute(name); + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + if (elementClassName.length == 0) return false; + if (elementClassName == className || + elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + return true; + return false; + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).add(className); + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element).remove(className); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className); + return element; + }, + + observe: function() { + Event.observe.apply(Event, arguments); + return $A(arguments).first(); + }, + + stopObserving: function() { + Event.stopObserving.apply(Event, arguments); + return $A(arguments).first(); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = Position.cumulativeOffset(element); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + if (['float','cssFloat'].include(style)) + style = (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat'); + style = style.camelize(); + var value = element.style[style]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } else if (element.currentStyle) { + value = element.currentStyle[style]; + } + } + + if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none')) + value = element['offset'+style.capitalize()] + 'px'; + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + if(style == 'opacity') { + if(value) return parseFloat(value); + if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if(value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (var name in style) { + var value = style[name]; + if(name == 'opacity') { + if (value == 1) { + value = (/Gecko/.test(navigator.userAgent) && + !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else if(value == '') { + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,''); + } else { + if(value < 0.00001) value = 0; + if(/MSIE/.test(navigator.userAgent) && !window.opera) + element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') + + 'alpha(opacity='+value*100+')'; + } + } else if(['float','cssFloat'].include(name)) name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat'; + element.style[name.camelize()] = value; + } + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = element.style.overflow || 'auto'; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + } +}; + +Object.extend(Element.Methods, {childOf: Element.Methods.descendantOf}); + +Element._attributeTranslations = {}; + +Element._attributeTranslations.names = { + colspan: "colSpan", + rowspan: "rowSpan", + valign: "vAlign", + datetime: "dateTime", + accesskey: "accessKey", + tabindex: "tabIndex", + enctype: "encType", + maxlength: "maxLength", + readonly: "readOnly", + longdesc: "longDesc" +}; + +Element._attributeTranslations.values = { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + + title: function(element) { + var node = element.getAttributeNode('title'); + return node.specified ? node.nodeValue : null; + } +}; + +Object.extend(Element._attributeTranslations.values, { + href: Element._attributeTranslations.values._getAttr, + src: Element._attributeTranslations.values._getAttr, + disabled: Element._attributeTranslations.values._flag, + checked: Element._attributeTranslations.values._flag, + readonly: Element._attributeTranslations.values._flag, + multiple: Element._attributeTranslations.values._flag +}); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + var t = Element._attributeTranslations; + attribute = t.names[attribute] || attribute; + return $(element).getAttributeNode(attribute).specified; + } +}; + +// IE is missing .innerHTML support for TABLE-related elements +if (document.all && !window.opera){ + Element.Methods.update = function(element, html) { + element = $(element); + html = typeof html == 'undefined' ? '' : html.toString(); + var tagName = element.tagName.toUpperCase(); + if (['THEAD','TBODY','TR','TD'].include(tagName)) { + var div = document.createElement('div'); + switch (tagName) { + case 'THEAD': + case 'TBODY': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 2; + break; + case 'TR': + div.innerHTML = '' + html.stripScripts() + '
    '; + depth = 3; + break; + case 'TD': + div.innerHTML = '
    ' + html.stripScripts() + '
    '; + depth = 4; + } + $A(element.childNodes).each(function(node){ + element.removeChild(node) + }); + depth.times(function(){ div = div.firstChild }); + + $A(div.childNodes).each( + function(node){ element.appendChild(node) }); + } else { + element.innerHTML = html.stripScripts(); + } + setTimeout(function() {html.evalScripts()}, 10); + return element; + } +}; + +Object.extend(Element, Element.Methods); + +var _nativeExtensions = false; + +if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { + var className = 'HTML' + tag + 'Element'; + if(window[className]) return; + var klass = window[className] = {}; + klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; + }); + +Element.addMethods = function(methods) { + Object.extend(Element.Methods, methods || {}); + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + var cache = Element.extend.cache; + for (var property in methods) { + var value = methods[property]; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = cache.findOrStore(value); + } + } + + if (typeof HTMLElement != 'undefined') { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + copy(Form.Methods, HTMLFormElement.prototype); + [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { + copy(Form.Element.Methods, klass.prototype); + }); + _nativeExtensions = true; + } +} + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + var tagName = this.element.tagName.toUpperCase(); + if (['TBODY', 'TR'].include(tagName)) { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
    '; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set($A(this).concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set($A(this).without(classNameToRemove).join(' ')); + }, + + toString: function() { + return $A(this).join(' '); + } +}; + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Selector = Class.create(); +Selector.prototype = { + initialize: function(expression) { + this.params = {classNames: []}; + this.expression = expression.toString().strip(); + this.parseExpression(); + this.compileMatcher(); + }, + + parseExpression: function() { + function abort(message) { throw 'Parse error in selector: ' + message; } + + if (this.expression == '') abort('empty expression'); + + var params = this.params, expr = this.expression, match, modifier, clause, rest; + while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:"([^"]*)"|([^\]\s]*)))?\]$/i)) { + params.attributes = params.attributes || []; + params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''}); + expr = match[1]; + } + + if (expr == '*') return this.params.wildcard = true; + + while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) { + modifier = match[1], clause = match[2], rest = match[3]; + switch (modifier) { + case '#': params.id = clause; break; + case '.': params.classNames.push(clause); break; + case '': + case undefined: params.tagName = clause.toUpperCase(); break; + default: abort(expr.inspect()); + } + expr = rest; + } + + if (expr.length > 0) abort(expr.inspect()); + }, + + buildMatchExpression: function() { + var params = this.params, conditions = [], clause; + + if (params.wildcard) + conditions.push('true'); + if (clause = params.id) + conditions.push('element.readAttribute("id") == ' + clause.inspect()); + if (clause = params.tagName) + conditions.push('element.tagName.toUpperCase() == ' + clause.inspect()); + if ((clause = params.classNames).length > 0) + for (var i = 0, length = clause.length; i < length; i++) + conditions.push('element.hasClassName(' + clause[i].inspect() + ')'); + if (clause = params.attributes) { + clause.each(function(attribute) { + var value = 'element.readAttribute(' + attribute.name.inspect() + ')'; + var splitValueBy = function(delimiter) { + return value + ' && ' + value + '.split(' + delimiter.inspect() + ')'; + } + + switch (attribute.operator) { + case '=': conditions.push(value + ' == ' + attribute.value.inspect()); break; + case '~=': conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break; + case '|=': conditions.push( + splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect() + ); break; + case '!=': conditions.push(value + ' != ' + attribute.value.inspect()); break; + case '': + case undefined: conditions.push('element.hasAttribute(' + attribute.name.inspect() + ')'); break; + default: throw 'Unknown operator ' + attribute.operator + ' in selector'; + } + }); + } + + return conditions.join(' && '); + }, + + compileMatcher: function() { + this.match = new Function('element', 'if (!element.tagName) return false; \ + element = $(element); \ + return ' + this.buildMatchExpression()); + }, + + findElements: function(scope) { + var element; + + if (element = $(this.params.id)) + if (this.match(element)) + if (!scope || Element.childOf(element, scope)) + return [element]; + + scope = (scope || document).getElementsByTagName(this.params.tagName || '*'); + + var results = []; + for (var i = 0, length = scope.length; i < length; i++) + if (this.match(element = scope[i])) + results.push(Element.extend(element)); + + return results; + }, + + toString: function() { + return this.expression; + } +} + +Object.extend(Selector, { + matchElements: function(elements, expression) { + var selector = new Selector(expression); + return elements.select(selector.match.bind(selector)).map(Element.extend); + }, + + findElement: function(elements, expression, index) { + if (typeof expression == 'number') index = expression, expression = false; + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + return expressions.map(function(expression) { + return expression.match(/[^\s"]+(?:"[^"]*"[^\s"]+)*/g).inject([null], function(results, expr) { + var selector = new Selector(expr); + return results.inject([], function(elements, result) { + return elements.concat(selector.findElements(result || element)); + }); + }); + }).flatten(); + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, getHash) { + var data = elements.inject({}, function(result, element) { + if (!element.disabled && element.name) { + var key = element.name, value = $(element).getValue(); + if (value != undefined) { + if (result[key]) { + if (result[key].constructor != Array) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return getHash ? data : Hash.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, getHash) { + return Form.serializeElements(Form.getElements(form), getHash); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + form.getElements().each(function(element) { + element.blur(); + element.disabled = 'true'; + }); + return form; + }, + + enable: function(form) { + form = $(form); + form.getElements().each(function(element) { + element.disabled = ''; + }); + return form; + }, + + findFirstElement: function(form) { + return $(form).getElements().find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + } +} + +Object.extend(Form, Form.Methods); + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +} + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = {}; + pair[element.name] = value; + return Hash.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select && ( element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type) ) ) + element.select(); + return element; + }, + + disable: function(element) { + element = $(element); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.blur(); + element.disabled = false; + return element; + } +} + +Object.extend(Form.Element, Form.Element.Methods); +var Field = Form.Element; +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + default: + return Form.Element.Serializers.textarea(element); + } + }, + + inputSelector: function(element) { + return element.checked ? element.value : null; + }, + + textarea: function(element) { + return element.value; + }, + + select: function(element) { + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +} + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + var changed = ('string' == typeof this.lastValue && 'string' == typeof value + ? this.lastValue != value : String(this.lastValue) != String(value)); + if (changed) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback.bind(this)); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0, length = Event.observers.length; i < length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + Event._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + try { + element.detachEvent('on' + name, observer); + } catch (e) {} + } + } +}); + +/* prevent memory leaks in IE */ +if (navigator.appVersion.match(/\bMSIE\b/)) + Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if(element.tagName=='BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!window.opera || element.tagName=='BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} + +Element.addMethods(); \ No newline at end of file diff --git a/public/javascripts/wms-gs.js b/public/javascripts/wms-gs.js new file mode 100644 index 0000000..c67146b --- /dev/null +++ b/public/javascripts/wms-gs.js @@ -0,0 +1,69 @@ +/* + * Call generic wms service for GoogleMaps v2 + * John Deck, UC Berkeley + * Inspiration & Code from: + * Mike Williams http://www.econym.demon.co.uk/googlemaps2/ V2 Reference & custommap code + * Brian Flood http://www.spatialdatalogic.com/cs/blogs/brian_flood/archive/2005/07/11/39.aspx V1 WMS code + * Kyle Mulka http://blog.kylemulka.com/?p=287 V1 WMS code modifications + * http://search.cpan.org/src/RRWO/GPS-Lowrance-0.31/lib/Geo/Coordinates/MercatorMeters.pm + * + * Modified by Chris Holmes, TOPP to work by default with GeoServer. + * + * Bundled with YM4R with John Deck's permission. + * Slightly modified to fit YM4R. + * See johndeck.blogspot.com for the original version and for examples and instructions of how to use it. + */ + +var WGS84_SEMI_MAJOR_AXIS = 6378137.0; //equatorial radius +var WGS84_ECCENTRICITY = 0.0818191913108718138; +var DEG2RAD=0.0174532922519943; +var PI=3.14159267; + +function dd2MercMetersLng(p_lng) { + return WGS84_SEMI_MAJOR_AXIS * (p_lng*DEG2RAD); +} + +function dd2MercMetersLat(p_lat) { + var lat_rad = p_lat * DEG2RAD; + return WGS84_SEMI_MAJOR_AXIS * Math.log(Math.tan((lat_rad + PI / 2) / 2) * Math.pow( ((1 - WGS84_ECCENTRICITY * Math.sin(lat_rad)) / (1 + WGS84_ECCENTRICITY * Math.sin(lat_rad))), (WGS84_ECCENTRICITY/2))); +} + +function addWMSPropertiesToLayer(tile_layer,base_url,layers,styles,format,merc_proj,use_geo){ + tile_layer.format = format; + tile_layer.baseURL = base_url; + tile_layer.styles = styles; + tile_layer.layers = layers; + tile_layer.mercatorEpsg = merc_proj; + tile_layer.useGeographic = use_geo; + return tile_layer; +} + +getTileUrlForWMS=function(a,b,c) { + var lULP = new GPoint(a.x*256,(a.y+1)*256); + var lLRP = new GPoint((a.x+1)*256,a.y*256); + var lUL = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lULP,b,c); + var lLR = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lLRP,b,c); + + if (this.useGeographic){ + var lBbox=lUL.x+","+lUL.y+","+lLR.x+","+lLR.y; + var lSRS="EPSG:4326"; + }else{ + var lBbox=dd2MercMetersLng(lUL.x)+","+dd2MercMetersLat(lUL.y)+","+dd2MercMetersLng(lLR.x)+","+dd2MercMetersLat(lLR.y); + var lSRS="EPSG:" + this.mercatorEpsg; + } + var lURL=this.baseURL; + lURL+="?REQUEST=GetMap"; + lURL+="&SERVICE=WMS"; + lURL+="&VERSION=1.1.1"; + lURL+="&LAYERS="+this.layers; + lURL+="&STYLES="+this.styles; + lURL+="&FORMAT=image/"+this.format; + lURL+="&BGCOLOR=0xFFFFFF"; + lURL+="&TRANSPARENT=TRUE"; + lURL+="&SRS="+lSRS; + lURL+="&BBOX="+lBbox; + lURL+="&WIDTH=256"; + lURL+="&HEIGHT=256"; + lURL+="&reaspect=false"; + return lURL; +} diff --git a/public/javascripts/ym4r-gm.js b/public/javascripts/ym4r-gm.js new file mode 100644 index 0000000..1c768df --- /dev/null +++ b/public/javascripts/ym4r-gm.js @@ -0,0 +1,117 @@ +// JS helper functions for YM4R + +function addInfoWindowToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowHtml(info,options);}); + return marker; +} + +function addInfoWindowTabsToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowTabsHtml(info,options);}); + return marker; +} + +function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){ + layer.getTileUrl = getTile; + layer.getCopyright = copyright; + layer.getOpacity = opacity; + layer.isPng = isPng; + return layer; +} + +function addOptionsToIcon(icon,options){ + for(var k in options){ + icon[k] = options[k]; + } + return icon; +} + +function addCodeToFunction(func,code){ + if(func == undefined) + return code; + else{ + return function(){ + func(); + code(); + } + } +} + +function addGeocodingToMarker(marker,address){ + marker.orig_initialize = marker.initialize; + orig_redraw = marker.redraw; + marker.redraw = function(force){}; //empty the redraw method so no error when called by addOverlay. + marker.initialize = function(map){ + new GClientGeocoder().getLatLng(address, + function(latlng){ + if(latlng){ + marker.redraw = orig_redraw; + marker.orig_initialize(map); //init before setting point + marker.setPoint(latlng); + }//do nothing + }); + }; + return marker; +} + + + +GMap2.prototype.centerAndZoomOnMarkers = function(markers) { + var bounds = new GLatLngBounds(markers[0].getPoint(), + markers[0].getPoint()); + for (var i=1, len = markers.length ; i + updated_at: <%= Time.now.to_s :db %> + published_at: <%= Time.now.to_s :db %> + category_id: 1 +some_gossip: + id: 2 + user_id: 1 + title: Rails Updated + synopsis: A new update to Rails was released + body: Time to update, folks! + published: true + created_at: <%= Time.now.to_s :db %> + updated_at: <%= Time.now.to_s :db %> + published_at: <%= Time.now.to_s :db %> + category_id: 2 diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml new file mode 100644 index 0000000..2681aea --- /dev/null +++ b/test/fixtures/categories.yml @@ -0,0 +1,6 @@ +site_news: + id: 1 + name: Site News +gossip: + id: 2 + name: Rails News diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml new file mode 100644 index 0000000..a4c1fec --- /dev/null +++ b/test/fixtures/comments.yml @@ -0,0 +1,6 @@ +valid_comment: + id: 1 + entry_id: 1 + user_id: 2 + body: a quick comment + created_at: <%= 1.days.ago.to_s(:db) %> diff --git a/test/fixtures/emails.yml b/test/fixtures/emails.yml new file mode 100644 index 0000000..b49c4eb --- /dev/null +++ b/test/fixtures/emails.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 +two: + id: 2 diff --git a/test/fixtures/entries.yml b/test/fixtures/entries.yml new file mode 100644 index 0000000..474353c --- /dev/null +++ b/test/fixtures/entries.yml @@ -0,0 +1,7 @@ +valid_entry: + id: 1 + user_id: 1 + title: first post + body: blah blah + created_at: <%= 1.days.ago.to_s(:db) %> + updated_at: <%= 1.days.ago.to_s(:db) %> diff --git a/test/fixtures/forums.yml b/test/fixtures/forums.yml new file mode 100644 index 0000000..6259550 --- /dev/null +++ b/test/fixtures/forums.yml @@ -0,0 +1,4 @@ +valid_forum: + id: 1 + name: Forum 1 + description: Just a test forum diff --git a/test/fixtures/friendships.yml b/test/fixtures/friendships.yml new file mode 100644 index 0000000..9cf1af9 --- /dev/null +++ b/test/fixtures/friendships.yml @@ -0,0 +1,5 @@ +valid_friendship: + id: 1 + user_id: 1 + friend_id: 2 + xfn_met: true \ No newline at end of file diff --git a/test/fixtures/newsletters.yml b/test/fixtures/newsletters.yml new file mode 100644 index 0000000..39eb3a9 --- /dev/null +++ b/test/fixtures/newsletters.yml @@ -0,0 +1,5 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 + subject: this is a newsletter + body: it's very interesting diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml new file mode 100644 index 0000000..532ad00 --- /dev/null +++ b/test/fixtures/pages.yml @@ -0,0 +1,14 @@ +valid_page: + id: 1 + title: Welcome Page + permalink: welcome-page + body: Welcome to RailsCoders +invalid_page_short_title: + id: 2 + title: a + permalink: a + body: The title is shorter than 3 character +valid_with_auto_permalink: + id: 3 + title: Another Page, but without a permalink + body: No permalink is given so should be automatically generated diff --git a/test/fixtures/photos.yml b/test/fixtures/photos.yml new file mode 100644 index 0000000..df03fbb --- /dev/null +++ b/test/fixtures/photos.yml @@ -0,0 +1,28 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +parent_photo: + id: 1 + user_id: 1 + title: a test photo + body: just a test + content_type: image/jpeg + filename: testimage.jpg + size: 1000 + width: 640 + height: 480 + created_at: <%= 1.days.ago.to_s(:db) %> +thumb_photo: + id: 2 + parent_id: 1 + width: 160 + height: 120 + filename: testimage_thumb.jpg + thumbnail: thumb + created_at: <%= 1.days.ago.to_s(:db) %> +tiny_photo: + id: 3 + parent_id: 1 + width: 80 + height: 80 + filename: testimage_tiny.jpg + thumbnail: tiny + created_at: <%= 1.days.ago.to_s(:db) %> \ No newline at end of file diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml new file mode 100644 index 0000000..1e54f37 --- /dev/null +++ b/test/fixtures/posts.yml @@ -0,0 +1,6 @@ +valid_post: + id: 1 + topic_id: 1 + user_id: 1 + body: we have a forum + created_at: <%= 1.days.ago.to_s(:db) %> diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml new file mode 100644 index 0000000..66ae780 --- /dev/null +++ b/test/fixtures/roles.yml @@ -0,0 +1,9 @@ +admin: + id: 1 + name: Administrator +editor: + id: 2 + name: Editor +moderator: + id: 3 + name: Moderator diff --git a/test/fixtures/roles_users.yml b/test/fixtures/roles_users.yml new file mode 100644 index 0000000..3d0a1bc --- /dev/null +++ b/test/fixtures/roles_users.yml @@ -0,0 +1,9 @@ +admin: + role_id: 1 + user_id: 2 +editor: + role_id: 2 + user_id: 3 +moderator: + role_id: 3 + user_id: 4 diff --git a/test/fixtures/topics.yml b/test/fixtures/topics.yml new file mode 100644 index 0000000..5171c97 --- /dev/null +++ b/test/fixtures/topics.yml @@ -0,0 +1,6 @@ +valid_topic: + id: 1 + forum_id: 1 + user_id: 1 + name: we have a forum + created_at: <%= 1.days.ago.to_s(:db) %> diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..ce9028b --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,26 @@ +valid_user: + id: 1 + username: joe + email: joe@example.com + hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 + # clear password = 12345 + profile: Just a regular Joe + created_at: <%= 1.days.ago.to_s(:db) %> +admin_user: + id: 2 + username: admin + email: admin@example.com + hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 # pw = 12345 + created_at: <%= 1.days.ago.to_s(:db) %> +editor_user: + id: 3 + username: editor + email: editor@example.com + hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 # pw = 12345 + created_at: <%= 1.days.ago.to_s(:db) %> +moderator_user: + id: 4 + username: moderator + email: moderator@example.com + hashed_password: 5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5 + created_at: <%= 1.days.ago.to_s(:db) %> diff --git a/test/fixtures/usertemplates.yml b/test/fixtures/usertemplates.yml new file mode 100644 index 0000000..b7e1192 --- /dev/null +++ b/test/fixtures/usertemplates.yml @@ -0,0 +1,15 @@ +valid_blog_index_for_joe: + id: 1 + user_id: 1 + name: blog_index + body: a template +valid_blog_entry_for_joe: + id: 2 + user_id: 1 + name: blog_entry + body: a template +valid_blog_index_for_admin: + id: 3 + user_id: 2 + name: blog_index + body: my template \ No newline at end of file diff --git a/test/functional/account_controller_test.rb b/test/functional/account_controller_test.rb new file mode 100644 index 0000000..67f2daf --- /dev/null +++ b/test/functional/account_controller_test.rb @@ -0,0 +1,35 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'account_controller' + +# Re-raise errors caught by the controller. +class AccountController; def rescue_action(e) raise e end; end + +class AccountControllerTest < Test::Unit::TestCase + fixtures :users + + def setup + @controller = AccountController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_valid_login_and_redirect + post :authenticate, :user => {:username => 'joe', :password => '12345'} + assert session[:user] + assert_response :redirect + end + def test_invalid_login + post :authenticate, :user => {:username => 'joe', :password => 'abc'} + assert !session[:user] + assert_response :redirect + assert_redirected_to :action => 'login' + assert flash.has_key?(:error) + end + def test_logout + post :authenticate, :user => {:username => 'joe', :password => '12345'} + assert session[:user] + post :logout + assert !session[:user] + assert_response :redirect + end +end diff --git a/test/functional/articles_controller_test.rb b/test/functional/articles_controller_test.rb new file mode 100644 index 0000000..14cc5d9 --- /dev/null +++ b/test/functional/articles_controller_test.rb @@ -0,0 +1,53 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'articles_controller' + +class ArticlesController; def rescue_action(e) raise e end; end + +class ArticlesControllerTest < Test::Unit::TestCase + fixtures :articles, :users, :roles, :roles_users + + def setup + @controller = ArticlesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_index + get :index + assert_response :success + assert_not_nil assigns(:articles) + end + + def test_index_as_xml + @request.env['HTTP_ACCEPT'] = 'application/xml' + get :index + assert_response :success + assert_not_nil assigns(:articles) + end + + def test_show + get :show, :id => 1 + assert_response :success + assert_not_nil assigns(:article) + end + + def test_create_article_with_http_auth_and_xml + old_count = Article.count + @request.env['HTTP_ACCEPT'] = 'application/xml' + @request.env['Authorization'] = 'Basic ' + Base64::b64encode('editor:12345') + + post :create, :article => { :title => 'New article', :synopsis => 'Just a test', + :body => 'Nothing to see here', :published => true } + + assert_response :success + assert_equal old_count + 1, Article.count + assert_not_nil assigns(:article) + end + + def test_rest_routing + with_options :controller => 'articles' do |test| + test.assert_routing 'articles', :action => 'index' + test.assert_routing 'articles/1', :action => 'show', :id => '1' + end + end +end diff --git a/test/functional/backend_api_test.rb b/test/functional/backend_api_test.rb new file mode 100644 index 0000000..5e14475 --- /dev/null +++ b/test/functional/backend_api_test.rb @@ -0,0 +1,46 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'backend_controller' + +class BackendController; def rescue_action(e) raise e end; end + +class BackendControllerApiTest < Test::Unit::TestCase + fixtures :users, :entries + + def setup + @controller = BackendController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_get_users_blogs + blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345' + assert_equal '1', blogs[0]['blogId'] + end + + def test_get_post + entry = invoke_layered :blogger, :getPost, '', '1', 'joe', '12345' + assert_equal '1', entry['postId'] + end + + def test_get_recent_posts + entries = invoke_layered :blogger, :getRecentPosts, '', '1', 'joe', '12345', '1' + assert_equal 1, entries.size + assert_equal '1', entries[0]['postId'] + end + + def test_new_post + blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345' + new_post = invoke_layered :blogger, :newPost, '', blogs[0]['blogId'], + 'joe', '12345', 'New Post', true + assert new_post.is_a?(Integer) + end + + def test_new_and_edit_post + blogs = invoke_layered :blogger, :getUsersBlogs, '', 'joe', '12345' + new_post = invoke_layered :blogger, :newPost, '', blogs[0]['blogId'], + 'joe', '12345','New Post', true + result = invoke_layered :blogger, :editPost, '', new_post, 'joe', '12345', + 'Edited Post', true + assert_equal true, result + end +end diff --git a/test/functional/blogs_controller_test.rb b/test/functional/blogs_controller_test.rb new file mode 100644 index 0000000..89ae8ce --- /dev/null +++ b/test/functional/blogs_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'blogs_controller' + +# Re-raise errors caught by the controller. +class BlogsController; def rescue_action(e) raise e end; end + +class BlogsControllerTest < Test::Unit::TestCase + def setup + @controller = BlogsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/categories_controller_test.rb b/test/functional/categories_controller_test.rb new file mode 100644 index 0000000..7b7a8bc --- /dev/null +++ b/test/functional/categories_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'categories_controller' + +# Re-raise errors caught by the controller. +class CategoriesController; def rescue_action(e) raise e end; end + +class CategoriesControllerTest < Test::Unit::TestCase + def setup + @controller = CategoriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/comments_controller_test.rb b/test/functional/comments_controller_test.rb new file mode 100644 index 0000000..c37ef96 --- /dev/null +++ b/test/functional/comments_controller_test.rb @@ -0,0 +1,42 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'comments_controller' + +# Re-raise errors caught by the controller. +class CommentsController; def rescue_action(e) raise e end; end + +class CommentsControllerTest < Test::Unit::TestCase + fixtures :comments, :users, :entries + + def setup + @controller = CommentsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_create_comment + login_as(:valid_user) + old_count = Comment.count + post :create,{:user_id => 1, :entry_id => 1, + :comment => {:body => 'that is great'}} + assert_equal old_count+1, Comment.count + assert_redirected_to entry_path(:user_id => 1, :id => 1) + end + + def test_should_destroy_comment + login_as(:valid_user) + old_count = Comment.count + delete :destroy, :user_id => 1, :entry_id => 1, :id => 1 + assert_equal old_count-1, Comment.count + assert_redirected_to entry_path(:user_id => 1, :id => 1) + end + + def test_send_notify_email + num_deliveries = ActionMailer::Base.deliveries.size + + login_as(:valid_user) + post :create,{:user_id => 1, :entry_id => 1, + :comment => {:body => 'that is great'}} + + assert_equal num_deliveries + 1, ActionMailer::Base.deliveries.size + end +end diff --git a/test/functional/entries_controller_test.rb b/test/functional/entries_controller_test.rb new file mode 100644 index 0000000..2037d81 --- /dev/null +++ b/test/functional/entries_controller_test.rb @@ -0,0 +1,61 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'entries_controller' + +# Re-raise errors caught by the controller. +class EntriesController; def rescue_action(e) raise e end; end + +class EntriesControllerTest < Test::Unit::TestCase + fixtures :entries, :users + + def setup + @controller = EntriesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, {:user_id => 1} + assert_response :success + assert assigns(:entries) + end + + def test_should_get_new + login_as(:valid_user) + get :new, {:user_id => 1} + assert_response :success + end + + def test_should_create_entry + login_as(:valid_user) + old_count = Entry.count + post :create, :entry => {:title => 'test entry', :body => 'a blog entry'} + assert_equal old_count+1, Entry.count + assert_redirected_to entry_path(:user_id => 1, :id => assigns(:entry)) + end + + def test_should_show_entry + get :show, {:user_id => 1, :id => 1} + assert_response :success + end + + def test_should_get_edit + login_as(:valid_user) + get :edit, {:user_id => 1, :id => 1} + assert_response :success + end + + def test_should_update_entry + login_as(:valid_user) + put :update, {:user_id => 1, :id => 1, + :entry => {:title => 'test entry', :body => 'a blog entry'} } + assert_redirected_to entry_path(:user_id => 1, :id => 1) + end + + def test_should_destroy_entry + login_as(:valid_user) + old_count = Entry.count + delete :destroy, {:user_id => 1, :id => 1} + assert_equal old_count-1, Entry.count + assert_redirected_to entries_path + end +end diff --git a/test/functional/forums_controller_test.rb b/test/functional/forums_controller_test.rb new file mode 100644 index 0000000..ade7a67 --- /dev/null +++ b/test/functional/forums_controller_test.rb @@ -0,0 +1,61 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'forums_controller' + +# Re-raise errors caught by the controller. +class ForumsController; def rescue_action(e) raise e end; end + +class ForumsControllerTest < Test::Unit::TestCase + fixtures :forums, :users, :roles, :roles_users + + def setup + @controller = ForumsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index + assert_response :success + assert assigns(:forums) + end + + def test_should_get_new + login_as(:moderator_user) + get :new + assert_response :success + end + + def test_should_create_forum + login_as(:moderator_user) + old_count = Forum.count + post :create, :forum => { :name => 'testing', :description => 'just a test'} + assert_equal old_count+1, Forum.count + assert_redirected_to forums_path + end + + def test_should_show_forum + get :show, :id => 1 + assert_redirected_to :controller => 'topics', :action => 'index', :forum_id => 1 + end + + def test_should_get_edit + login_as(:moderator_user) + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_forum + login_as(:moderator_user) + put :update, :id => 1, :forum => { :name => 'testing', :description => 'a test'} + assert_redirected_to forum_path(assigns(:forum)) + end + + def test_should_destroy_forum + login_as(:moderator_user) + old_count = Forum.count + delete :destroy, :id => 1 + assert_equal old_count-1, Forum.count + + assert_redirected_to forums_path + end +end diff --git a/test/functional/friends_controller_test.rb b/test/functional/friends_controller_test.rb new file mode 100644 index 0000000..172d523 --- /dev/null +++ b/test/functional/friends_controller_test.rb @@ -0,0 +1,64 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'friends_controller' + +# Re-raise errors caught by the controller. +class FriendsController; def rescue_action(e) raise e end; end + +class FriendsControllerTest < Test::Unit::TestCase + fixtures :friendships, :users, :roles, :roles_users + + def setup + @controller = FriendsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, {:user_id => 1} + assert_response :success + assert assigns(:user) + end + + def test_should_get_new + login_as(:valid_user) + get :new, {:user_id => 1, :friend_id => 3} + assert_response :success + end + + def test_should_create_friendship + login_as(:valid_user) + old_count = Friendship.count + post :create, {:user_id => 1, :friend_id => 3, :friendship => { :xfn_met => true } } + assert_equal old_count + 1, Friendship.count + assert_redirected_to friends_path(:user_id => 1) + end + + def test_should_get_edit + login_as(:valid_user) + get :edit, :user_id => 1, :id => 2 + assert_response :success + end + + def test_should_update_friendship + login_as(:valid_user) + + get :index, {:user_id => 1} + assert_select "a#friend-2[rel~=crush]", false + + put :update, {:user_id => 1, :id => 2, :friendship => { :xfn_crush => true} } + assert_redirected_to friends_path(:user_id => 1) + + get :index, {:user_id => 1} + assert_response :success + assert_select "a#friend-2[rel~=crush]", true + end + + def test_should_destroy_friendship + login_as(:valid_user) + old_count = Friendship.count + delete :destroy, :user_id => 1, :id => 2 + assert_equal old_count - 1, Friendship.count + assert_redirected_to friends_path(:user_id => 1) + end + +end diff --git a/test/functional/newsletters_controller_test.rb b/test/functional/newsletters_controller_test.rb new file mode 100644 index 0000000..c6ca28b --- /dev/null +++ b/test/functional/newsletters_controller_test.rb @@ -0,0 +1,64 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'newsletters_controller' + +# Re-raise errors caught by the controller. +class NewslettersController; def rescue_action(e) raise e end; end + +class NewslettersControllerTest < Test::Unit::TestCase + fixtures :newsletters, :users, :roles, :roles_users + + def setup + @controller = NewslettersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + login_as(:admin_user) + get :index + assert_response :success + assert assigns(:newsletters) + end + + def test_should_get_new + login_as(:admin_user) + get :new + assert_response :success + end + + def test_should_create_newsletter + login_as(:admin_user) + old_count = Newsletter.count + post :create, :newsletter => { :subject => 'new newsletter', :body => 'interesting news goes here' } + assert_equal old_count+1, Newsletter.count + + assert_redirected_to newsletter_path(assigns(:newsletter)) + end + + def test_should_show_newsletter + login_as(:admin_user) + get :show, :id => 1 + assert_response :success + end + + def test_should_get_edit + login_as(:admin_user) + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_newsletter + login_as(:admin_user) + put :update, :id => 1, :newsletter => { :subject => 'new newsletter', :body => 'interesting news goes here' } + assert_redirected_to newsletter_path(assigns(:newsletter)) + end + + def test_should_destroy_newsletter + login_as(:admin_user) + old_count = Newsletter.count + delete :destroy, :id => 1 + assert_equal old_count-1, Newsletter.count + + assert_redirected_to newsletters_path + end +end diff --git a/test/functional/pages_controller_test.rb b/test/functional/pages_controller_test.rb new file mode 100644 index 0000000..79e3581 --- /dev/null +++ b/test/functional/pages_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'pages_controller' + +# Re-raise errors caught by the controller. +class PagesController; def rescue_action(e) raise e end; end + +class PagesControllerTest < Test::Unit::TestCase + def setup + @controller = PagesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/photos_controller_test.rb b/test/functional/photos_controller_test.rb new file mode 100644 index 0000000..3f72c13 --- /dev/null +++ b/test/functional/photos_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'photos_controller' + +# Re-raise errors caught by the controller. +class PhotosController; def rescue_action(e) raise e end; end + +class PhotosControllerTest < Test::Unit::TestCase + def setup + @controller = PhotosController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb new file mode 100644 index 0000000..3448005 --- /dev/null +++ b/test/functional/posts_controller_test.rb @@ -0,0 +1,62 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'posts_controller' + +# Re-raise errors caught by the controller. +class PostsController; def rescue_action(e) raise e end; end + +class PostsControllerTest < Test::Unit::TestCase + fixtures :posts, :topics, :forums, :users, :roles, :roles_users + + def setup + @controller = PostsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, {:forum_id => 1, :topic_id => 1} + assert_response :success + assert assigns(:posts) + end + + def test_should_get_new + login_as(:valid_user) + get :new, {:forum_id => 1, :topic_id => 1} + assert_response :success + end + + def test_should_create_post + login_as(:valid_user) + old_count = Post.count + post :create, {:forum_id => 1, :topic_id => 1, + :post => { :body => 'test message' } } + assert_equal old_count+1, Post.count + assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1) + end + + def test_should_show_post + get :show, {:id => 1, :forum_id => 1, :topic_id => 1} + assert_response :success + end + + def test_should_get_edit + login_as(:moderator_user) + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_post + login_as(:moderator_user) + put :update, {:forum_id => 1, :topic_id => 1, :id => 1, + :post => { :body => 'test message'} } + assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1) + end + + def test_should_destroy_post + login_as(:moderator_user) + old_count = Post.count + delete :destroy, :id => 1, :forum_id => 1, :topic_id => 1 + assert_equal old_count-1, Post.count + assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1) + end +end diff --git a/test/functional/tags_controller_test.rb b/test/functional/tags_controller_test.rb new file mode 100644 index 0000000..5656ff2 --- /dev/null +++ b/test/functional/tags_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'tags_controller' + +# Re-raise errors caught by the controller. +class TagsController; def rescue_action(e) raise e end; end + +class TagsControllerTest < Test::Unit::TestCase + def setup + @controller = TagsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/topics_controller_test.rb b/test/functional/topics_controller_test.rb new file mode 100644 index 0000000..ae4dcb0 --- /dev/null +++ b/test/functional/topics_controller_test.rb @@ -0,0 +1,65 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'topics_controller' + +# Re-raise errors caught by the controller. +class TopicsController; def rescue_action(e) raise e end; end + +class TopicsControllerTest < Test::Unit::TestCase + fixtures :topics, :forums, :users, :roles, :roles_users + + def setup + @controller = TopicsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, {:forum_id => 1} + assert_response :success + assert assigns(:topics) + end + + def test_should_get_new + login_as(:moderator_user) + get :new, {:forum_id => 1} + assert_response :success + end + + def test_should_create_topic + login_as(:moderator_user) + old_count = Topic.count + post :create, {:forum_id => 1, + :topic => { :name => 'a test topic' }, + :post => { :body => 'and the message'} } + assert_equal old_count+1, Topic.count + assert_redirected_to posts_path(:forum_id => 1, :topic_id => assigns(:topic)) + end + + def test_should_show_topic + get :show, { :id => 1, :forum_id => 1 } + assert_redirected_to :controller => 'posts', :action => 'index', + :forum_id => 1, :topic_id => 1 + assert_redirected_to posts_path(:forum_id => 1, :topic_id => 1) + end + + def test_should_get_edit + login_as(:moderator_user) + get :edit, { :id => 1, :forum_id => 1 } + assert_response :success + end + + def test_should_update_topic + login_as(:moderator_user) + put :update, {:id => 1, :forum_id => 1, :topic => { :name => 'a test' } } + assert_redirected_to :controller => 'posts', :action => 'index', + :forum_id => 1, :topic_id => 1 + end + + def test_should_destroy_topic + login_as(:moderator_user) + old_count = Topic.count + delete :destroy, { :id => 1, :forum_id => 1 } + assert_equal old_count-1, Topic.count + assert_redirected_to topics_path(:forum_id => 1) + end +end diff --git a/test/functional/user_photos_controller_test.rb b/test/functional/user_photos_controller_test.rb new file mode 100644 index 0000000..b78bfa7 --- /dev/null +++ b/test/functional/user_photos_controller_test.rb @@ -0,0 +1,90 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'user_photos_controller' + +# Re-raise errors caught by the controller. +class UserPhotosController; def rescue_action(e) raise e end; end + +class UserPhotosControllerTest < Test::Unit::TestCase + fixtures :photos, :users + + def setup + @controller = UserPhotosController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, {:user_id => 1} + assert_response :success + assert assigns(:photos) + end + + def test_should_get_new + login_as(:valid_user) + get :new, {:user_id => 1} + assert_response :success + end + + def test_should_create_photo + login_as(:valid_user) + old_count = Photo.count + image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png') + + post :create, + :photo => {:title => 'test photo', + :body => 'a test image', + :temp_path => image_file, + :content_type => 'image/png', + :filename => 'rails.png'} + + assert_equal old_count+3, Photo.count + assert_redirected_to user_photos_path(:user_id => 1) + end + + def test_should_show_photo + get :show, {:user_id => 1, :id => 1} + assert_response :success + end + + def test_should_get_edit + login_as(:valid_user) + get :edit, {:user_id => 1, :id => 1} + assert_response :success + end + + def test_should_update_photo + login_as(:valid_user) + + # upload a test image + image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png') + post :create, + :photo => {:title => 'test photo', + :body => 'a test image', + :temp_path => image_file, + :content_type => 'image/png', + :filename => 'rails.png'} + + put :update, {:user_id => assigns['photo'].user_id, :id => assigns['photo'].id, :photo => {:body => 'this has been edited' }} + assert_redirected_to user_photo_path(:user_id => assigns['photo'].user_id, :id => assigns['photo'].id) + end + + def test_should_destroy_photo + login_as(:valid_user) + + # upload a test image + image_file = File.join(RAILS_ROOT, 'public', 'images', 'rails.png') + post :create, + :photo => {:title => 'test photo', + :body => 'a test image', + :temp_path => image_file, + :content_type => 'image/png', + :filename => 'rails.png'} + + + old_count = Photo.count + delete :destroy, {:user_id => assigns['photo'].user_id, :id => assigns['photo'].id} + assert_equal old_count-3, Photo.count + + assert_redirected_to user_photos_path + end +end diff --git a/test/functional/user_tags_controller_test.rb b/test/functional/user_tags_controller_test.rb new file mode 100644 index 0000000..3a3cfbb --- /dev/null +++ b/test/functional/user_tags_controller_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'user_tags_controller' + +# Re-raise errors caught by the controller. +class UserTagsController; def rescue_action(e) raise e end; end + +class UserTagsControllerTest < Test::Unit::TestCase + def setup + @controller = UserTagsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb new file mode 100644 index 0000000..05ab9d8 --- /dev/null +++ b/test/functional/users_controller_test.rb @@ -0,0 +1,36 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'users_controller' + +# Re-raise errors caught by the controller. +class UsersController; def rescue_action(e) raise e end; end + +class UsersControllerTest < Test::Unit::TestCase + def setup + @controller = UsersController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_signup_page + get :new + assert_response :success + end + def test_valid_signup_and_redirect + post :create, :user => {:username => 'fred', + :email => 'fred@example.com', + :password => 'abc123', + :password_confirmation => 'abc123', + :profile => 'A regular guy'} + assert_response :redirect + end + def test_invalid_signup_dupe_username + post :create, :user => {:username => 'joe', + :email => 'fred@example.com', + :password => 'abc123', + :password_confirmation => 'abc123', + :profile => 'A regular guy'} + assert assigns(:user).errors.on(:username) + assert_response :success + assert_template 'users/new' + end +end diff --git a/test/functional/usertemplates_controller_test.rb b/test/functional/usertemplates_controller_test.rb new file mode 100644 index 0000000..2717718 --- /dev/null +++ b/test/functional/usertemplates_controller_test.rb @@ -0,0 +1,48 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'usertemplates_controller' + +# Re-raise errors caught by the controller. +class UsertemplatesController; def rescue_action(e) raise e end; end + +class UsertemplatesControllerTest < Test::Unit::TestCase + fixtures :usertemplates, :users + + def setup + @controller = UsertemplatesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + end + + def test_should_get_index + login_as(:valid_user) + get :index + assert_response :success + assert assigns(:usertemplates) + end + + def test_should_get_edit + login_as(:valid_user) + get :edit, :id => 1 + assert_response :success + end + + def test_should_update_usertemplate + login_as(:valid_user) + put :update, :id => 1, :usertemplate => { :body => 'a different template'} + assert_redirected_to usertemplates_path + end + + def test_should_fail_get_edit_for_other_user + login_as(:valid_user) + get :edit, :id => 3 + assert_response :redirect + assert_redirected_to :action => 'index' + end + + def test_should_fail_update_for_other_user + login_as(:valid_user) + put :update, :id => 3, :usertemplate => { :body => 'a different template'} + assert_response :redirect + assert_redirected_to :action => 'index' + end +end diff --git a/test/integration/articles_stories_test.rb b/test/integration/articles_stories_test.rb new file mode 100644 index 0000000..e795695 --- /dev/null +++ b/test/integration/articles_stories_test.rb @@ -0,0 +1,19 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class ArticleStoriesTest < ActionController::IntegrationTest + fixtures :users, :articles, :categories + + def test_view_all_articles + get articles_url + assert_response :success + assert_template 'articles/index' + assert_equal assigns['articles'].length, 2 + end + + def test_view_one_category + get category_articles_url(:category_id => 1) + assert_response :success + assert_template 'articles/index' + assert_equal assigns['articles'].length, 1 + end +end diff --git a/test/integration/login_stories_test.rb b/test/integration/login_stories_test.rb new file mode 100644 index 0000000..86ccb23 --- /dev/null +++ b/test/integration/login_stories_test.rb @@ -0,0 +1,34 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class LoginStoriesTest < ActionController::IntegrationTest + fixtures :users, :pages + + def test_valid_login + get edit_user_url(1) + assert_response :redirect + follow_redirect! + assert_response :success + assert_template 'account/login' + + go_to_login + + login :user => {:username => 'joe', :password => '12345'} + + get edit_user_url(1) + assert_response :success + assert_template 'users/edit' + end + + private + + def go_to_login + get 'account/login' + assert_response :success + assert_template 'account/login' + end + + def login(options) + post 'account/authenticate', options + assert_response :redirect + end +end diff --git a/test/integration/mobile_login_stories_test.rb b/test/integration/mobile_login_stories_test.rb new file mode 100644 index 0000000..3f7b96f --- /dev/null +++ b/test/integration/mobile_login_stories_test.rb @@ -0,0 +1,29 @@ +require "#{File.dirname(__FILE__)}/../test_helper" + +class MobileLoginStoriesTest < ActionController::IntegrationTest + fixtures :users, :pages + + def test_valid_mobile_login + get 'mobile/login' + assert_response :success + assert_template 'mobile/account/login' + + post 'mobile/account/authenticate', :user => {:username => 'joe', :password => '12345'} + assert_response :redirect + follow_redirect! + assert_response :success + assert_template 'mobile/pages/show' + end + + def test_invalid_mobile_login + get 'mobile/login' + assert_response :success + assert_template 'mobile/account/login' + + post 'mobile/account/authenticate', :user => {:username => 'joe', :password => 'wrong'} + assert_response :redirect + follow_redirect! + assert_response :success + assert_template 'mobile/account/login' + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..4acf2b9 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,31 @@ +ENV["RAILS_ENV"] = "test" +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") +require 'test_help' + +class Test::Unit::TestCase + # Transactional fixtures accelerate your tests by wrapping each test method + # in a transaction that's rolled back on completion. This ensures that the + # test database remains unchanged so your fixtures don't have to be reloaded + # between every test method. Fewer database queries means faster tests. + # + # Read Mike Clark's excellent walkthrough at + # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting + # + # Every Active Record database supports transactions except MyISAM tables + # in MySQL. Turn off transactional fixtures in this case; however, if you + # don't care one way or the other, switching from MyISAM to InnoDB tables + # is recommended. + self.use_transactional_fixtures = true + + # Instantiated fixtures are slow, but give you @david where otherwise you + # would need people(:david). If you don't want to migrate your existing + # test cases which use the @david style and don't mind the speed hit (each + # instantiated fixtures translates to a database query per test method), + # then set this back to true. + self.use_instantiated_fixtures = false + + # Add more helper methods to be used by all tests here... + def login_as(user) + @request.session[:user] = users(user).id + end +end diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb new file mode 100644 index 0000000..813dcbc --- /dev/null +++ b/test/unit/article_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ArticleTest < Test::Unit::TestCase + fixtures :articles + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/category_test.rb b/test/unit/category_test.rb new file mode 100644 index 0000000..10aa26c --- /dev/null +++ b/test/unit/category_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class CategoryTest < Test::Unit::TestCase + fixtures :categories + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb new file mode 100644 index 0000000..f3042e4 --- /dev/null +++ b/test/unit/comment_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class CommentTest < Test::Unit::TestCase + fixtures :comments + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/email_test.rb b/test/unit/email_test.rb new file mode 100644 index 0000000..fcfa5aa --- /dev/null +++ b/test/unit/email_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class EmailTest < Test::Unit::TestCase + fixtures :emails + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/entry_test.rb b/test/unit/entry_test.rb new file mode 100644 index 0000000..3f193d0 --- /dev/null +++ b/test/unit/entry_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class EntryTest < Test::Unit::TestCase + fixtures :entries + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/forum_test.rb b/test/unit/forum_test.rb new file mode 100644 index 0000000..91815de --- /dev/null +++ b/test/unit/forum_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ForumTest < Test::Unit::TestCase + fixtures :forums + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/friendship_test.rb b/test/unit/friendship_test.rb new file mode 100644 index 0000000..bfc3041 --- /dev/null +++ b/test/unit/friendship_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class FriendshipTest < Test::Unit::TestCase + fixtures :friendships + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/newsletter_test.rb b/test/unit/newsletter_test.rb new file mode 100644 index 0000000..7c5c047 --- /dev/null +++ b/test/unit/newsletter_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class NewsletterTest < Test::Unit::TestCase + fixtures :newsletters + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/notifier_test.rb b/test/unit/notifier_test.rb new file mode 100644 index 0000000..ce511ac --- /dev/null +++ b/test/unit/notifier_test.rb @@ -0,0 +1,37 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class NotifierTest < Test::Unit::TestCase + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures' + CHARSET = "utf-8" + fixtures :entries, :comments, :users + + include ActionMailer::Quoting + + def setup + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries = [] + + @expected = TMail::Mail.new + @expected.set_content_type "text", "plain", { "charset" => CHARSET } + @expected.mime_version = '1.0' + end + + def test_comment_notify + comment = Comment.find(1) + response = Notifier.create_new_comment_notification(comment) + assert_equal "A new comment has been left on your blog", response.subject + assert_match /Hi #{comment.entry.user.username}/, response.body + assert_match /The comment was left by '#{comment.user.username}' at #{comment.created_at.to_s(:short)}/, response.body + assert_match /go to http:\/\/railscoders.net\/users\/1\/entries\/1/, response.body + end + + private + def read_fixture(action) + IO.readlines("#{FIXTURES_PATH}/notifier/#{action}") + end + + def encode(subject) + quoted_printable(subject, CHARSET) + end +end diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb new file mode 100644 index 0000000..17cff64 --- /dev/null +++ b/test/unit/page_test.rb @@ -0,0 +1,25 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PageTest < Test::Unit::TestCase + fixtures :pages + + def test_invalid_if_any_field_empty + page = Page.new + assert !page.valid? + assert page.errors.invalid?(:title) + assert page.errors.invalid?(:body) + end + def test_valid_fields + page = pages(:valid_page) + assert page.valid? + end + def test_invalid_short_title + page = pages(:invalid_page_short_title) + assert !page.valid? + end + + def test_auto_permalink + page = pages(:valid_with_auto_permalink) + assert page.valid? + end +end diff --git a/test/unit/photo_test.rb b/test/unit/photo_test.rb new file mode 100644 index 0000000..5657d41 --- /dev/null +++ b/test/unit/photo_test.rb @@ -0,0 +1,47 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PhotoTest < Test::Unit::TestCase + fixtures :photos, :users + + def test_should_upload_photo_and_create_thumbnails + photo_object = upload_file 'rails.png', users(:valid_user) + assert_file_exists photo_object.id, "rails.png" + assert_file_exists photo_object.id, "rails_thumb.png" + assert_file_exists photo_object.id, "rails_tiny.png" + end + + def test_should_delete_db_row_and_files + photo_object = upload_file 'rails.png', users(:valid_user) + photo_count = Photo.count + + assert_file_exists photo_object.id, "rails.png" + Photo.destroy(photo_object.id) + + assert_equal photo_count-3, Photo.count + assert_file_does_not_exist photo_object.id, "rails.png" + assert_file_does_not_exist photo_object.id, "rails_thumb.png" + assert_file_does_not_exist photo_object.id, "rails_tiny.png" + end + + protected + def upload_file(image_file, user) + image_file = File.join(RAILS_ROOT, 'public', 'images', image_file) + photo = user.photos.create(:filename => image_file, + :content_type => 'image/png', + :temp_path => image_file) + assert_valid photo + photo + end + + def assert_file_exists(photo_id, image_file) + file = File.join(RAILS_ROOT, 'public', 'photos', + "#{photo_id}", "#{image_file}") + assert File.file?(file), "File not found: #{image_file}" + end + + def assert_file_does_not_exist(photo_id, image_file) + file = File.join(RAILS_ROOT, 'public', 'photos', + "#{photo_id}", "#{image_file}") + assert !File.file?(file) + end +end diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb new file mode 100644 index 0000000..b87ec67 --- /dev/null +++ b/test/unit/post_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class PostTest < Test::Unit::TestCase + fixtures :posts + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb new file mode 100644 index 0000000..05d6652 --- /dev/null +++ b/test/unit/role_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class RoleTest < Test::Unit::TestCase + fixtures :roles + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/topic_test.rb b/test/unit/topic_test.rb new file mode 100644 index 0000000..1fe7c24 --- /dev/null +++ b/test/unit/topic_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class TopicTest < Test::Unit::TestCase + fixtures :topics + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb new file mode 100644 index 0000000..405cfe9 --- /dev/null +++ b/test/unit/user_test.rb @@ -0,0 +1,18 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class UserTest < Test::Unit::TestCase + fixtures :users + + def test_create_valid_user + user = User.new(:username => 'fred', :email => 'fred@example.com', + :password => 'abc123', :password_confirmation => 'abc123', + :profile => 'A regular guy') + assert user.save + end + def test_invalid_duplicate_username + user = User.new(:username => 'joe', :email => 'fred@example.com', + :password => 'abc123', :password_confirmation => 'abc123', + :profile => 'A regular guy') + assert !user.save + end +end diff --git a/test/unit/usertemplate_test.rb b/test/unit/usertemplate_test.rb new file mode 100644 index 0000000..fcc7e7a --- /dev/null +++ b/test/unit/usertemplate_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class UsertemplateTest < Test::Unit::TestCase + fixtures :usertemplates + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG new file mode 100644 index 0000000..a12b709 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG @@ -0,0 +1,52 @@ +[23 June 2007] + +* Add validation to Tag model. + +* find_options_for_tagged_with should always return a hash. + +* find_tagged_with passing in no tags should return an empty array. + +* Improve compatibility with PostgreSQL. + +[21 June 2007] + +* Remove extra .rb from generated migration file name. + +[15 June 2007] + +* Introduce TagList class. + +* Various cleanups and improvements. + +* Use TagList.delimiter now, not Tag.delimiter. Tag.delimiter will be removed at some stage. + +[11 June 2007] + +* Restructure the creation of the options for find_tagged_with [Thijs Cadier] + +* Add an example migration with a generator. + +* Add caching. + +* Fix compatibility with Ruby < 1.8.6 + +[23 April 2007] + +* Make tag_list to respect Tag.delimiter + +[31 March 2007] + +* Add Tag.delimiter accessor to change how tags are parsed. +* Fix :include => :tags when used with find_tagged_with + +[7 March 2007] + +* Fix tag_counts for SQLServer [Brad Young] + +[21 Feb 2007] + +* Use scoping instead of TagCountsExtension [Michael Schuerig] + +[7 Jan 2007] + +* Add :match_all to find_tagged_with [Michael Sheakoski] diff --git a/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE new file mode 100644 index 0000000..602bda2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006 Jonathan Viney + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/plugins/acts_as_taggable_on_steroids/README b/vendor/plugins/acts_as_taggable_on_steroids/README new file mode 100644 index 0000000..670d13a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/README @@ -0,0 +1,116 @@ += acts_as_taggable_on_steroids + +If you find this plugin useful, please consider a donation to show your support! + + http://www.paypal.com/cgi-bin/webscr?cmd=_send-money + + Email address: jonathan.viney@gmail.com + +== Instructions + +This plugin is based on acts_as_taggable by DHH but includes extras +such as tests, smarter tag assignment, and tag cloud calculations. + +Thanks to www.fanacious.com for allowing this plugin to be released. Please check out +their site to show your support. + +== Resources + +Install + * script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids + +== Usage + +=== Prepare database + +Generate and apply the migration: + + ruby script/generate acts_as_taggable_migration + rake db:migrate + +=== Basic tagging + +Using the examples from the tests, let's suppose we have users that have many posts and we want those +posts to be able to be tagged by the user. + +As usual, we add +acts_as_taggable+ to the Post class: + + class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + end + +We can now use the tagging methods provided by acts_as_taggable, tag_list and tag_list=. Both these +methods work like regular attribute accessors. + + p = Post.find(:first) + p.tag_list.to_s # "" + p.tag_list = "Funny, Silly" + p.save + p.reload.tag_list.to_s # "Funny, Silly" + +You can also add or remove arrays of tags. + + p.tag_list.add("Great", "Awful") + p.tag_list.remove("Funny") + +=== Finding tagged objects + +To retrieve objects tagged with a certain tag, use find_tagged_with. + + Post.find_tagged_with('Funny, Silly') + +By default, find_tagged_with will find objects that have any of the given tags. To +find only objects that are tagged with all the given tags, use match_all. + + Post.find_tagged_with('Funny, Silly', :match_all => true) + +=== Tag cloud calculations + +To construct tag clouds, the frequency of each tag needs to be calculated. +Because we specified +acts_as_taggable+ on the Post class, we can +get a calculation of all the tag counts by using Post.tag_counts. But what if we wanted a tag count for +an single user's posts? To achieve this we call tag_counts on the association: + + User.find(:first).posts.tag_counts + +=== Caching + +It is useful to cache the list of tags to reduce the number of queries executed. To do this, +add a column named cached_tag_list to the model which is being tagged. + + class CachePostTagList < ActiveRecord::Migration + def self.up + # You should make sure that the column is long enough to hold + # the full tag list. In some situations the :text type may be more appropriate. + add_column :posts, :cached_tag_list, :string + end + end + + class Post < ActiveRecord::Base + acts_as_taggable + + # The caching column defaults to cached_tag_list, but can be changed: + # + # set_cached_tag_list_column_name "my_caching_column_name" + end + +The details of the caching are handled for you. Just continue to use the tag_list accessor as you normally would. +Note that the cached tag list will not be updated if you directly create Tagging objects or manually append to the +tags or taggings associations. To update the cached tag list you should call save_cached_tag_list manually. + +=== Delimiter + +If you want to change the delimiter used to parse and present tags, set TagList.delimiter. +For example, to use spaces instead of commas, add the following to config/environment.rb: + + TagList.delimiter = " " + +=== Other + +Problems, comments, and suggestions all welcome. jonathan.viney@gmail.com + +== Credits + +www.fanacious.com diff --git a/vendor/plugins/acts_as_taggable_on_steroids/Rakefile b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile new file mode 100644 index 0000000..d2c0003 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the acts_as_taggable_on_steroids plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the acts_as_taggable_on_steroids plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'Acts As Taggable On Steroids' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb new file mode 100644 index 0000000..be9b39c --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/acts_as_taggable_migration_generator.rb @@ -0,0 +1,11 @@ +class ActsAsTaggableMigrationGenerator < Rails::Generator::Base + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end + + def file_name + "acts_as_taggable_migration" + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb new file mode 100644 index 0000000..ea0c2cc --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/generators/acts_as_taggable_migration/templates/migration.rb @@ -0,0 +1,26 @@ +class ActsAsTaggableMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.column :name, :string + end + + create_table :taggings do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + + # You should make sure that the column created is + # long enough to store the required class names. + t.column :taggable_type, :string + + t.column :created_at, :datetime + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/init.rb b/vendor/plugins/acts_as_taggable_on_steroids/init.rb new file mode 100644 index 0000000..5d3aa8e --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/init.rb @@ -0,0 +1,4 @@ +require File.dirname(__FILE__) + '/lib/acts_as_taggable' + +require File.dirname(__FILE__) + '/lib/tagging' +require File.dirname(__FILE__) + '/lib/tag' diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb new file mode 100644 index 0000000..060c625 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/acts_as_taggable.rb @@ -0,0 +1,155 @@ +module ActiveRecord + module Acts #:nodoc: + module Taggable #:nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def acts_as_taggable(options = {}) + has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag + has_many :tags, :through => :taggings + + before_save :save_cached_tag_list + after_save :save_tags + + include ActiveRecord::Acts::Taggable::InstanceMethods + extend ActiveRecord::Acts::Taggable::SingletonMethods + + alias_method :reload_without_tag_list, :reload + alias_method :reload, :reload_with_tag_list + end + + def cached_tag_list_column_name + "cached_tag_list" + end + + def set_cached_tag_list_column_name(value = nil, &block) + define_attr_method :cached_tag_list_column_name, value, &block + end + end + + module SingletonMethods + # Pass either a tag string, or an array of strings or tags + # + # Options: + # :exclude - Find models that are not tagged with the given tags + # :match_all - Find models that match all of the gievn tags, not just one + # :conditions - A piece of SQL conditions to add to the query + def find_options_for_tagged_with(tags, options = {}) + tags = TagList.from(tags).names if tags.is_a?(String) + tags.compact! + tags.map!(&:to_s) + + return {} if tags.empty? + + conditions = sanitize_sql(["#{table_name}_tags.name #{"NOT" if options.delete(:exclude)} IN (?)", tags]) + conditions << " AND #{sanitize_sql(options.delete(:conditions))}" if options[:conditions] + + group = "#{table_name}_taggings.taggable_id HAVING COUNT(#{table_name}_taggings.taggable_id) = #{tags.size}" if options.delete(:match_all) + + { :select => "DISTINCT #{table_name}.*", + :joins => "LEFT OUTER JOIN taggings #{table_name}_taggings ON #{table_name}_taggings.taggable_id = #{table_name}.#{primary_key} AND #{table_name}_taggings.taggable_type = '#{name}' " + + "LEFT OUTER JOIN tags #{table_name}_tags ON #{table_name}_tags.id = #{table_name}_taggings.tag_id", + :conditions => conditions, + :group => group + }.update(options) + end + + def find_tagged_with(*args) + options = find_options_for_tagged_with(*args) + options.blank? ? [] : find(:all, options) + end + + # Options: + # :start_at - Restrict the tags to those created after a certain time + # :end_at - Restrict the tags to those created before a certain time + # :conditions - A piece of SQL conditions to add to the query + # :limit - The maximum number of tags to return + # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' + # :at_least - Exclude tags with a frequency less than the given value + # :at_most - Exclude tags with a frequency greater then the given value + def tag_counts(options = {}) + options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit + + scope = scope(:find) + start_at = sanitize_sql(['taggings.created_at >= ?', options[:start_at]]) if options[:start_at] + end_at = sanitize_sql(['taggings.created_at <= ?', options[:end_at]]) if options[:end_at] + + conditions = [ + "taggings.taggable_type = #{quote_value(name)}", + options[:conditions], + scope && scope[:conditions], + start_at, + end_at + ] + conditions = conditions.compact.join(' and ') + + at_least = sanitize_sql(['COUNT(*) >= ?', options[:at_least]]) if options[:at_least] + at_most = sanitize_sql(['COUNT(*) <= ?', options[:at_most]]) if options[:at_most] + having = [at_least, at_most].compact.join(' and ') + group_by = 'tags.id, tags.name HAVING COUNT(*) > 0' + group_by << " AND #{having}" unless having.blank? + + Tag.find(:all, + :select => 'tags.id, tags.name, COUNT(*) AS count', + :joins => "LEFT OUTER JOIN taggings ON tags.id = taggings.tag_id LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = taggings.taggable_id", + :conditions => conditions, + :group => group_by, + :order => options[:order], + :limit => options[:limit] + ) + end + end + + module InstanceMethods + def tag_list + if @tag_list + @tag_list + elsif caching_tag_list? and !send(self.class.cached_tag_list_column_name).nil? + @tag_list = TagList.from(send(self.class.cached_tag_list_column_name)) + else + @tag_list = TagList.new(tags.map(&:name)) + end + end + + def tag_list=(value) + @tag_list = TagList.from(value) + end + + def save_cached_tag_list + if caching_tag_list? and !tag_list.blank? + self[self.class.cached_tag_list_column_name] = tag_list.to_s + end + end + + def save_tags + return unless @tag_list + + new_tag_names = @tag_list.names - tags.map(&:name) + old_tags = tags.reject { |tag| @tag_list.names.include?(tag.name) } + + self.class.transaction do + new_tag_names.each do |new_tag_name| + tags << Tag.find_or_create_by_name(new_tag_name) + end + + tags.delete(*old_tags) if old_tags.any? + end + true + end + + def reload_with_tag_list(*args) + @tag_list = nil + reload_without_tag_list(*args) + end + + def caching_tag_list? + self.class.column_names.include?(self.class.cached_tag_list_column_name) + end + end + end + end +end + +ActiveRecord::Base.send(:include, ActiveRecord::Acts::Taggable) diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb new file mode 100644 index 0000000..852ee33 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag.rb @@ -0,0 +1,22 @@ +class Tag < ActiveRecord::Base + has_many :taggings + + validates_presence_of :name + validates_uniqueness_of :name + + class << self + delegate :delimiter, :delimeter=, :to => TagList + end + + def ==(object) + super || (object.is_a?(Tag) && name == object.name) + end + + def to_s + name + end + + def count + read_attribute(:count).to_i + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb new file mode 100644 index 0000000..01defe0 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_counts_extension.rb @@ -0,0 +1,2 @@ +module TagCountsExtension +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb new file mode 100644 index 0000000..08306e1 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tag_list.rb @@ -0,0 +1,57 @@ +class TagList + cattr_accessor :delimiter + self.delimiter = ',' + + attr_reader :names + + def initialize(*names) + @names = [] + add(*names) + end + + def add(*names) + names = names.flatten + + # Strip whitespace and remove blank or duplicate tags + names.map!(&:strip) + names.reject!(&:blank?) + + @names.concat(names) + @names.uniq! + end + + def remove(*names) + names = names.flatten + @names.delete_if { |name| names.include?(name) } + end + + def blank? + @names.empty? + end + + def to_s + @names.map do |name| + name.include?(delimiter) ? "\"#{name}\"" : name + end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ") + end + + def ==(other) + super || (other.is_a?(TagList) && other.names == @names) + end + + class << self + def from(string) + new(parse(string)) + end + + def parse(string) + returning [] do |names| + string = string.to_s.dup + # Parse the quoted tags + string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { names << $1; "" } + + names.concat(string.split(delimiter)) + end + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb new file mode 100644 index 0000000..33daf86 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb @@ -0,0 +1,4 @@ +class Tagging < ActiveRecord::Base + belongs_to :tag + belongs_to :taggable, :polymorphic => true +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb new file mode 100644 index 0000000..9f5258e --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/abstract_unit.rb @@ -0,0 +1,82 @@ +require 'test/unit' + +begin + require File.dirname(__FILE__) + '/../../../../config/environment' +rescue LoadError + require 'rubygems' + require_gem 'activerecord' + require_gem 'actionpack' +end + +# Search for fixtures first +fixture_path = File.dirname(__FILE__) + '/fixtures/' +begin + Dependencies.load_paths.insert(0, fixture_path) +rescue + $LOAD_PATH.unshift(fixture_path) +end + +require 'active_record/fixtures' + +require File.dirname(__FILE__) + '/../lib/acts_as_taggable' +require_dependency File.dirname(__FILE__) + '/../lib/tag_list' + +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log') +ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.establish_connection(ENV['DB'] || 'mysql') + +load(File.dirname(__FILE__) + '/schema.rb') + +Test::Unit::TestCase.fixture_path = fixture_path + +class Test::Unit::TestCase #:nodoc: + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def assert_equivalent(expected, actual, message = nil) + if expected.first.is_a?(ActiveRecord::Base) + assert_equal expected.sort_by(&:id), actual.sort_by(&:id), message + else + assert_equal expected.sort, actual.sort, message + end + end + + def assert_tag_counts(tags, expected_values) + # Map the tag fixture names to real tag names + expected_values = expected_values.inject({}) do |hash, (tag, count)| + hash[tags(tag).name] = count + hash + end + + tags.each do |tag| + value = expected_values.delete(tag.name) + assert_not_nil value, "Expected count for #{tag.name} was not provided" if value.nil? + assert_equal value, tag.count, "Expected value of #{value} for #{tag.name}, but was #{tag.count}" + end + + unless expected_values.empty? + assert false, "The following tag counts were not present: #{expected_values.inspect}" + end + end + + def assert_queries(num = 1) + $query_count = 0 + yield + ensure + assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed." + end + + def assert_no_queries(&block) + assert_queries(0, &block) + end +end + +ActiveRecord::Base.connection.class.class_eval do + def execute_with_counting(sql, name = nil, &block) + $query_count ||= 0 + $query_count += 1 + execute_without_counting(sql, name, &block) + end + + alias_method_chain :execute, :counting +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb new file mode 100644 index 0000000..d6e5166 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/acts_as_taggable_test.rb @@ -0,0 +1,211 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class ActsAsTaggableOnSteroidsTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos + + def test_find_tagged_with + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with('"Very good"') + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with(['Very good']) + assert_equal Post.find_tagged_with('"Very good"'), Post.find_tagged_with([tags(:good)]) + + assert_equivalent [photos(:jonathan_dog), photos(:sam_flower), photos(:sam_sky)], Photo.find_tagged_with('Nature') + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with(['Nature']) + assert_equal Photo.find_tagged_with('Nature'), Photo.find_tagged_with([tags(:nature)]) + + assert_equivalent [photos(:jonathan_bad_cat), photos(:jonathan_dog), photos(:jonathan_questioning_dog)], Photo.find_tagged_with('"Crazy animal" Bad') + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with(['Crazy animal', 'Bad']) + assert_equal Photo.find_tagged_with('"Crazy animal" Bad'), Photo.find_tagged_with([tags(:animal), tags(:bad)]) + end + + def test_find_tagged_with_nothing + assert_equal [], Post.find_tagged_with("") + assert_equal [], Post.find_tagged_with([]) + end + + def test_find_tagged_with_nonexistant_tags + assert_equal [], Post.find_tagged_with('ABCDEFG') + assert_equal [], Photo.find_tagged_with(['HIJKLM']) + assert_equal [], Photo.find_tagged_with([Tag.new(:name => 'unsaved tag')]) + end + + def test_find_tagged_with_matching_all_tags + assert_equivalent [photos(:jonathan_dog)], Photo.find_tagged_with('Crazy animal, "Nature"', :match_all => true) + assert_equivalent [posts(:jonathan_sky), posts(:sam_flowers)], Post.find_tagged_with(['Very good', 'Nature'], :match_all => true) + end + + def test_find_options_for_tagged_with_no_tags_returns_empty_hash + assert_equal Hash.new, Post.find_options_for_tagged_with("") + assert_equal Hash.new, Post.find_options_for_tagged_with([nil]) + end + + def test_include_tags_on_find_tagged_with + assert_nothing_raised do + Photo.find_tagged_with('Nature', :include => :tags) + Photo.find_tagged_with("Nature", :include => { :taggings => :tag }) + end + end + + def test_basic_tag_counts_on_class + assert_tag_counts Post.tag_counts, :good => 2, :nature => 5, :question => 1, :bad => 1 + assert_tag_counts Photo.tag_counts, :good => 1, :nature => 3, :question => 1, :bad => 1, :animal => 3 + end + + def test_tag_counts_on_class_with_date_conditions + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 4)), :good => 1, :nature => 3, :question => 1, :bad => 1 + assert_tag_counts Post.tag_counts(:end_at => Date.new(2006, 8, 6)), :good => 1, :nature => 4, :question => 1 + assert_tag_counts Post.tag_counts(:start_at => Date.new(2006, 8, 5), :end_at => Date.new(2006, 8, 8)), :good => 1, :nature => 2, :bad => 1 + + assert_tag_counts Photo.tag_counts(:start_at => Date.new(2006, 8, 12), :end_at => Date.new(2006, 8, 17)), :good => 1, :nature => 1, :bad => 1, :question => 1, :animal => 2 + end + + def test_tag_counts_on_class_with_frequencies + assert_tag_counts Photo.tag_counts(:at_least => 2), :nature => 3, :animal => 3 + assert_tag_counts Photo.tag_counts(:at_most => 2), :good => 1, :question => 1, :bad => 1 + end + + def test_tag_counts_with_limit + assert_equal 2, Photo.tag_counts(:limit => 2).size + assert_equal 1, Post.tag_counts(:at_least => 4, :limit => 2).size + end + + def test_tag_counts_with_limit_and_order + assert_equal [tags(:nature), tags(:good)], Post.tag_counts(:order => 'count desc', :limit => 2) + end + + def test_tag_counts_on_association + assert_tag_counts users(:jonathan).posts.tag_counts, :good => 1, :nature => 3, :question => 1 + assert_tag_counts users(:sam).posts.tag_counts, :good => 1, :nature => 2, :bad => 1 + + assert_tag_counts users(:jonathan).photos.tag_counts, :animal => 3, :nature => 1, :question => 1, :bad => 1 + assert_tag_counts users(:sam).photos.tag_counts, :nature => 2, :good => 1 + end + + def test_tag_counts_on_association_with_options + assert_equal [], users(:jonathan).posts.tag_counts(:conditions => '1=0') + assert_tag_counts users(:jonathan).posts.tag_counts(:at_most => 2), :good => 1, :question => 1 + end + + def test_tag_list_reader + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + assert_equivalent ["Bad", "Crazy animal"], photos(:jonathan_bad_cat).tag_list.names + end + + def test_reassign_tag_list + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names + posts(:jonathan_rain).taggings.reload + + # Only an update of the posts table should be executed + assert_queries 1 do + posts(:jonathan_rain).update_attributes!(:tag_list => posts(:jonathan_rain).tag_list.to_s) + end + + assert_equivalent ["Nature", "Question"], posts(:jonathan_rain).tag_list.names + end + + def test_new_tags + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "#{posts(:jonathan_sky).tag_list}, One, Two") + assert_equivalent ["Very good", "Nature", "One", "Two"], posts(:jonathan_sky).tag_list.names + end + + def test_remove_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature") + assert_equivalent ["Nature"], posts(:jonathan_sky).tag_list.names + end + + def test_remove_and_add_tag + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + posts(:jonathan_sky).update_attributes!(:tag_list => "Nature, Beautiful") + assert_equivalent ["Nature", "Beautiful"], posts(:jonathan_sky).tag_list.names + end + + def test_tags_not_saved_if_validation_fails + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + assert !posts(:jonathan_sky).update_attributes(:tag_list => "One, Two", :text => "") + assert_equivalent ["Very good", "Nature"], Post.find(posts(:jonathan_sky).id).tag_list.names + end + + def test_tag_list_accessors_on_new_record + p = Post.new(:text => 'Test') + + assert p.tag_list.blank? + p.tag_list = "One, Two" + assert_equal "One, Two", p.tag_list.to_s + end + + def test_clear_tag_list_with_nil + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => nil) + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_clear_tag_list_with_string + p = photos(:jonathan_questioning_dog) + + assert !p.tag_list.blank? + assert p.update_attributes(:tag_list => ' ') + assert p.tag_list.blank? + + assert p.reload.tag_list.blank? + end + + def test_tag_list_reset_on_reload + p = photos(:jonathan_questioning_dog) + assert !p.tag_list.blank? + p.tag_list = nil + assert p.tag_list.blank? + assert !p.reload.tag_list.blank? + end + + def test_tag_list_populated_when_cache_nil + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equal posts(:jonathan_sky).tag_list.to_s, posts(:jonathan_sky).cached_tag_list + end + + def test_cached_tag_list_used + posts(:jonathan_sky).save! + posts(:jonathan_sky).reload + + assert_no_queries do + assert_equivalent ["Very good", "Nature"], posts(:jonathan_sky).tag_list.names + end + end + + def test_cached_tag_list_not_used + # Load fixture and column information + posts(:jonathan_sky).taggings(:reload) + + assert_queries 1 do + # Tags association will be loaded + posts(:jonathan_sky).tag_list + end + end + + def test_cached_tag_list_updated + assert_nil posts(:jonathan_sky).cached_tag_list + posts(:jonathan_sky).save! + assert_equivalent ["Very good", "Nature"], TagList.from(posts(:jonathan_sky).cached_tag_list).names + posts(:jonathan_sky).update_attributes!(:tag_list => "None") + + assert_equal 'None', posts(:jonathan_sky).cached_tag_list + assert_equal 'None', posts(:jonathan_sky).reload.cached_tag_list + end +end + +class ActsAsTaggableOnSteroidsFormTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts, :users, :photos + + include ActionView::Helpers::FormHelper + + def test_tag_list_contents + fields_for :post, posts(:jonathan_sky) do |f| + assert_match /Very good, Nature/, f.text_field(:tag_list) + end + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml new file mode 100644 index 0000000..47c3736 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/database.yml @@ -0,0 +1,10 @@ +mysql: + :adapter: mysql + :host: localhost + :username: rails + :password: + :database: rails_plugin_test + +sqlite3: + :adapter: sqlite3 + :database: ':memory:' diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb new file mode 100644 index 0000000..224957f --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb @@ -0,0 +1,8 @@ +class Photo < ActiveRecord::Base + acts_as_taggable + + belongs_to :user +end + +class SpecialPhoto < Photo +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml new file mode 100644 index 0000000..25a4118 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photos.yml @@ -0,0 +1,24 @@ +jonathan_dog: + id: 1 + user_id: 1 + title: A small dog + +jonathan_questioning_dog: + id: 2 + user_id: 1 + title: What does this dog want? + +jonathan_bad_cat: + id: 3 + user_id: 1 + title: Bad cat + +sam_flower: + id: 4 + user_id: 2 + title: Flower + +sam_sky: + id: 5 + user_id: 2 + title: Sky diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb new file mode 100644 index 0000000..bee100a --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb @@ -0,0 +1,7 @@ +class Post < ActiveRecord::Base + acts_as_taggable + + belongs_to :user + + validates_presence_of :text +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml new file mode 100644 index 0000000..d0cd9ac --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/posts.yml @@ -0,0 +1,24 @@ +jonathan_sky: + id: 1 + user_id: 1 + text: The sky is particularly blue today + +jonathan_grass: + id: 2 + user_id: 1 + text: The grass seems very green + +jonathan_rain: + id: 3 + user_id: 1 + text: Why does the rain fall? + +sam_ground: + id: 4 + user_id: 2 + text: The ground is looking too brown + +sam_flowers: + id: 5 + user_id: 2 + text: Why are the flowers dead? \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml new file mode 100644 index 0000000..b6eb440 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/taggings.yml @@ -0,0 +1,126 @@ +jonathan_sky_good: + id: 1 + tag_id: 1 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-01 + +jonathan_sky_nature: + id: 2 + tag_id: 3 + taggable_id: 1 + taggable_type: Post + created_at: 2006-08-02 + +jonathan_grass_nature: + id: 3 + tag_id: 3 + taggable_id: 2 + taggable_type: Post + created_at: 2006-08-03 + +jonathan_rain_question: + id: 4 + tag_id: 4 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-04 + +jonathan_rain_nature: + id: 5 + tag_id: 3 + taggable_id: 3 + taggable_type: Post + created_at: 2006-08-05 + +sam_ground_nature: + id: 6 + tag_id: 3 + taggable_id: 4 + taggable_type: Post + created_at: 2006-08-06 + +sam_ground_bad: + id: 7 + tag_id: 2 + taggable_id: 4 + taggable_type: Post + created_at: 2006-08-07 + +sam_flowers_good: + id: 8 + tag_id: 1 + taggable_id: 5 + taggable_type: Post + created_at: 2006-08-08 + +sam_flowers_nature: + id: 9 + tag_id: 3 + taggable_id: 5 + taggable_type: Post + created_at: 2006-08-09 + + +jonathan_dog_animal: + id: 10 + tag_id: 5 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-10 + +jonathan_dog_nature: + id: 11 + tag_id: 3 + taggable_id: 1 + taggable_type: Photo + created_at: 2006-08-11 + +jonathan_questioning_dog_animal: + id: 12 + tag_id: 5 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-12 + +jonathan_questioning_dog_question: + id: 13 + tag_id: 4 + taggable_id: 2 + taggable_type: Photo + created_at: 2006-08-13 + +jonathan_bad_cat_bad: + id: 14 + tag_id: 2 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-14 + +jonathan_bad_cat_animal: + id: 15 + tag_id: 5 + taggable_id: 3 + taggable_type: Photo + created_at: 2006-08-15 + +sam_flower_nature: + id: 16 + tag_id: 3 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-16 + +sam_flower_good: + id: 17 + tag_id: 1 + taggable_id: 4 + taggable_type: Photo + created_at: 2006-08-17 + +sam_sky_nature: + id: 18 + tag_id: 3 + taggable_id: 5 + taggable_type: Photo + created_at: 2006-08-18 diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml new file mode 100644 index 0000000..b8f8367 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/tags.yml @@ -0,0 +1,19 @@ +good: + id: 1 + name: Very good + +bad: + id: 2 + name: Bad + +nature: + id: 3 + name: Nature + +question: + id: 4 + name: Question + +animal: + id: 5 + name: Crazy animal \ No newline at end of file diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb new file mode 100644 index 0000000..c85a292 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/user.rb @@ -0,0 +1,4 @@ +class User < ActiveRecord::Base + has_many :posts + has_many :photos +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml new file mode 100644 index 0000000..da94fea --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml @@ -0,0 +1,7 @@ +jonathan: + id: 1 + name: Jonathan + +sam: + id: 2 + name: Sam diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb new file mode 100644 index 0000000..3d6f008 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/schema.rb @@ -0,0 +1,27 @@ +ActiveRecord::Schema.define :version => 0 do + create_table :tags, :force => true do |t| + t.column :name, :string + end + + create_table :taggings, :force => true do |t| + t.column :tag_id, :integer + t.column :taggable_id, :integer + t.column :taggable_type, :string + t.column :created_at, :datetime + end + + create_table :users, :force => true do |t| + t.column :name, :string + end + + create_table :posts, :force => true do |t| + t.column :text, :text + t.column :cached_tag_list, :string + t.column :user_id, :integer + end + + create_table :photos, :force => true do |t| + t.column :title, :string + t.column :user_id, :integer + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb new file mode 100644 index 0000000..efa84d6 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_list_test.rb @@ -0,0 +1,98 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagListTest < Test::Unit::TestCase + def test_blank? + assert TagList.new.blank? + end + + def test_equality + assert_equal TagList.new, TagList.new + assert_equal TagList.new("Tag"), TagList.new("Tag") + + assert_not_equal TagList.new, "" + assert_not_equal TagList.new, TagList.new("Tag") + end + + def test_parse_leaves_string_unchanged + tags = '"One ", Two' + original = tags.dup + TagList.parse(tags) + assert_equal tags, original + end + + def test_from_single_name + assert_equal %w(Fun), TagList.from("Fun").names + assert_equal %w(Fun), TagList.from('"Fun"').names + end + + def test_from_blank + assert_equal [], TagList.from(nil).names + assert_equal [], TagList.from("").names + end + + def test_from_single_quoted_tag + assert_equal ['with, comma'], TagList.from('"with, comma"').names + end + + def test_spaces_do_not_delineate + assert_equal ['A B', 'C'], TagList.from('A B, C').names + end + + def test_from_multiple_tags + assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from("Alpha, Beta, Delta, Gamma").names.sort + end + + def test_from_multiple_tags_with_quotes + assert_equivalent %w(Alpha Beta Delta Gamma), TagList.from('Alpha, "Beta", Gamma , "Delta"').names.sort + end + + def test_from_multiple_tags_with_quote_and_commas + assert_equivalent ['Alpha, Beta', 'Delta', 'Gamma, something'], TagList.from('"Alpha, Beta", Delta, "Gamma, something"').names + end + + def test_from_removes_white_space + assert_equivalent %w(Alpha Beta), TagList.from('" Alpha ", "Beta "').names + assert_equivalent %w(Alpha Beta), TagList.from(' Alpha, Beta ').names + end + + def test_alternative_delimiter + TagList.delimiter = " " + + assert_equal %w(One Two), TagList.from("One Two").names + assert_equal ['One two', 'three', 'four'], TagList.from('"One two" three four').names + ensure + TagList.delimiter = "," + end + + def test_duplicate_tags_removed + assert_equal %w(One), TagList.from("One, One").names + end + + def test_to_s_with_commas + assert_equal "Question, Crazy Animal", TagList.new(["Question", "Crazy Animal"]).to_s + end + + def test_to_s_with_alternative_delimiter + TagList.delimiter = " " + + assert_equal '"Crazy Animal" Question', TagList.new(["Crazy Animal", "Question"]).to_s + ensure + TagList.delimiter = "," + end + + def test_add + tag_list = TagList.new("One") + assert_equal %w(One), tag_list.names + + tag_list.add("Two") + assert_equal %w(One Two), tag_list.names + end + + def test_remove + tag_list = TagList.new("One", "Two") + assert_equal %w(One Two), tag_list.names + + tag_list.remove("One") + assert_equal %w(Two), tag_list.names + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb new file mode 100644 index 0000000..c882a53 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tag_test.rb @@ -0,0 +1,34 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TagTest < Test::Unit::TestCase + fixtures :tags, :taggings, :users, :photos, :posts + + def test_name_required + t = Tag.create + assert_match /blank/, t.errors[:name].to_s + end + + def test_name_unique + t = Tag.create!(:name => "My tag") + duplicate = t.clone + + assert !duplicate.save + assert_match /taken/, duplicate.errors[:name].to_s + end + + def test_taggings + assert_equivalent [taggings(:jonathan_sky_good), taggings(:sam_flowers_good), taggings(:sam_flower_good)], tags(:good).taggings + assert_equivalent [taggings(:sam_ground_bad), taggings(:jonathan_bad_cat_bad)], tags(:bad).taggings + end + + def test_to_s + assert_equal tags(:good).name, tags(:good).to_s + end + + def test_equality + assert_equal tags(:good), tags(:good) + assert_equal Tag.find(1), Tag.find(1) + assert_equal Tag.new(:name => 'A'), Tag.new(:name => 'A') + assert_not_equal Tag.new(:name => 'A'), Tag.new(:name => 'B') + end +end diff --git a/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb new file mode 100644 index 0000000..172b8e2 --- /dev/null +++ b/vendor/plugins/acts_as_taggable_on_steroids/test/tagging_test.rb @@ -0,0 +1,13 @@ +require File.dirname(__FILE__) + '/abstract_unit' + +class TaggingTest < Test::Unit::TestCase + fixtures :tags, :taggings, :posts + + def test_tag + assert_equal tags(:good), taggings(:jonathan_sky_good).tag + end + + def test_taggable + assert_equal posts(:jonathan_sky), taggings(:jonathan_sky_good).taggable + end +end diff --git a/vendor/plugins/attachment_fu/CHANGELOG b/vendor/plugins/attachment_fu/CHANGELOG new file mode 100644 index 0000000..3dd22dd --- /dev/null +++ b/vendor/plugins/attachment_fu/CHANGELOG @@ -0,0 +1,19 @@ +* April 2, 2007 * + +* don't copy the #full_filename to the default #temp_paths array if it doesn't exist +* add default ID partitioning for attachments +* add #binmode call to Tempfile (note: ruby should be doing this!) [Eric Beland] +* Check for current type of :thumbnails option. +* allow customization of the S3 configuration file path with the :s3_config_path option. +* Don't try to remove thumbnails if there aren't any. Closes #3 [ben stiglitz] + +* BC * (before changelog) + +* add default #temp_paths entry [mattly] +* add MiniMagick support to attachment_fu [Isacc] +* update #destroy_file to clear out any empty directories too [carlivar] +* fix references to S3Backend module [Hunter Hillegas] +* make #current_data public with db_file and s3 backends [ebryn] +* oops, actually svn add the files for s3 backend. [Jeffrey Hardy] +* experimental s3 support, egad, no tests.... [Jeffrey Hardy] +* doh, fix a few bad references to ActsAsAttachment [sixty4bit] \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/README b/vendor/plugins/attachment_fu/README new file mode 100644 index 0000000..331fbfb --- /dev/null +++ b/vendor/plugins/attachment_fu/README @@ -0,0 +1,162 @@ +attachment-fu +===================== + +attachment_fu is a plugin by Rick Olson (aka technoweenie ) and is the successor to acts_as_attachment. To get a basic run-through of its capabilities, check out Mike Clark's tutorial . + + +attachment_fu functionality +=========================== + +attachment_fu facilitates file uploads in Ruby on Rails. There are a few storage options for the actual file data, but the plugin always at a minimum stores metadata for each file in the database. + +There are three storage options for files uploaded through attachment_fu: + File system + Database file + Amazon S3 + +Each method of storage many options associated with it that will be covered in the following section. Something to note, however, is that the Amazon S3 storage requires you to modify config/amazon_s3.yml and the Database file storage requires an extra table. + + +attachment_fu models +==================== + +For all three of these storage options a table of metadata is required. This table will contain information about the file (hence the 'meta') and its location. This table has no restrictions on naming, unlike the extra table required for database storage, which must have a table name of db_files (and by convention a model of DbFile). + +In the model there are two methods made available by this plugins: has_attachment and validates_as_attachment. + +has_attachment(options = {}) + This method accepts the options in a hash: + :content_type # Allowed content types. + # Allows all by default. Use :image to allow all standard image types. + :min_size # Minimum size allowed. + # 1 byte is the default. + :max_size # Maximum size allowed. + # 1.megabyte is the default. + :size # Range of sizes allowed. + # (1..1.megabyte) is the default. This overrides the :min_size and :max_size options. + :resize_to # Used by RMagick to resize images. + # Pass either an array of width/height, or a geometry string. + :thumbnails # Specifies a set of thumbnails to generate. + # This accepts a hash of filename suffixes and RMagick resizing options. + # This option need only be included if you want thumbnailing. + :thumbnail_class # Set which model class to use for thumbnails. + # This current attachment class is used by default. + :path_prefix # path to store the uploaded files. + # Uses public/#{table_name} by default for the filesystem, and just #{table_name} for the S3 backend. + # Setting this sets the :storage to :file_system. + :storage # Specifies the storage system to use.. + # Defaults to :db_system. Options are :file_system, :db_file, and :s3. + :processor # Sets the image processor to use for resizing of the attached image. + # Options include ImageScience, Rmagick, and MiniMagick. Default is whatever is installed. + + + Examples: + has_attachment :max_size => 1.kilobyte + has_attachment :size => 1.megabyte..2.megabytes + has_attachment :content_type => 'application/pdf' + has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain'] + has_attachment :content_type => :image, :resize_to => [50,50] + has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50' + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + has_attachment :storage => :file_system, :path_prefix => 'public/files' + has_attachment :storage => :file_system, :path_prefix => 'public/files', + :content_type => :image, :resize_to => [50,50] + has_attachment :storage => :file_system, :path_prefix => 'public/files', + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + has_attachment :storage => :s3 + +validates_as_attachment + This method prevents files outside of the valid range (:min_size to :max_size, or the :size range) from being saved. It does not however, halt the upload of such files. They will be uploaded into memory regardless of size before validation. + + Example: + validates_as_attachment + + +attachment_fu migrations +======================== + +Fields for attachment_fu metadata tables... + in general: + size, :integer # file size in bytes + content_type, :string # mime type, ex: application/mp3 + filename, :string # sanitized filename + that reference images: + height, :integer # in pixels + width, :integer # in pixels + that reference images that will be thumbnailed: + parent_id, :integer # id of parent image (on the same table, a self-referencing foreign-key). + # Only populated if the current object is a thumbnail. + thumbnail, :string # the 'type' of thumbnail this attachment record describes. + # Only populated if the current object is a thumbnail. + # Usage: + # [ In Model 'Avatar' ] + # has_attachment :content_type => :image, + # :storage => :file_system, + # :max_size => 500.kilobytes, + # :resize_to => '320x200>', + # :thumbnails => { :small => '10x10>', + # :thumb => '100x100>' } + # [ Elsewhere ] + # @user.avatar.thumbnails.first.thumbnail #=> 'small' + that reference files stored in the database (:db_file): + db_file_id, :integer # id of the file in the database (foreign key) + +Field for attachment_fu db_files table: + data, :binary # binary file data, for use in database file storage + + +attachment_fu views +=================== + +There are two main views tasks that will be directly affected by attachment_fu: upload forms and displaying uploaded images. + +There are two parts of the upload form that differ from typical usage. + 1. Include ':multipart => true' in the html options of the form_for tag. + Example: + <% form_for(:attachment_metadata, :url => { :action => "create" }, :html => { :multipart => true }) do |form| %> + + 2. Use the file_field helper with :uploaded_data as the field name. + Example: + <%= form.file_field :uploaded_data %> + +Displaying uploaded images is made easy by the public_filename method of the ActiveRecord attachment objects using file system and s3 storage. + +public_filename(thumbnail = nil) + Returns the public path to the file. If a thumbnail prefix is specified it will return the public file path to the corresponding thumbnail. + Examples: + attachment_obj.public_filename #=> /attachments/2/file.jpg + attachment_obj.public_filename(:thumb) #=> /attachments/2/file_thumb.jpg + attachment_obj.public_filename(:small) #=> /attachments/2/file_small.jpg + +When serving files from database storage, doing more than simply downloading the file is beyond the scope of this document. + + +attachment_fu controllers +========================= + +There are two considerations to take into account when using attachment_fu in controllers. + +The first is when the files have no publicly accessible path and need to be downloaded through an action. + +Example: + def readme + send_file '/path/to/readme.txt', :type => 'plain/text', :disposition => 'inline' + end + +See the possible values for send_file for reference. + + +The second is when saving the file when submitted from a form. +Example in view: + <%= form.file_field :attachable, :uploaded_data %> + +Example in controller: + def create + @attachable_file = AttachmentMetadataModel.new(params[:attachable]) + if @attachable_file.save + flash[:notice] = 'Attachment was successfully created.' + redirect_to attachable_url(@attachable_file) + else + render :action => :new + end + end diff --git a/vendor/plugins/attachment_fu/Rakefile b/vendor/plugins/attachment_fu/Rakefile new file mode 100644 index 0000000..0851dd4 --- /dev/null +++ b/vendor/plugins/attachment_fu/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the attachment_fu plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the attachment_fu plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'ActsAsAttachment' + rdoc.options << '--line-numbers --inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/attachment_fu/amazon_s3.yml.tpl b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl new file mode 100644 index 0000000..81cb807 --- /dev/null +++ b/vendor/plugins/attachment_fu/amazon_s3.yml.tpl @@ -0,0 +1,14 @@ +development: + bucket_name: appname_development + access_key_id: + secret_access_key: + +test: + bucket_name: appname_test + access_key_id: + secret_access_key: + +production: + bucket_name: appname + access_key_id: + secret_access_key: diff --git a/vendor/plugins/attachment_fu/init.rb b/vendor/plugins/attachment_fu/init.rb new file mode 100644 index 0000000..0239e56 --- /dev/null +++ b/vendor/plugins/attachment_fu/init.rb @@ -0,0 +1,14 @@ +require 'tempfile' + +Tempfile.class_eval do + # overwrite so tempfiles use the extension of the basename. important for rmagick and image science + def make_tmpname(basename, n) + ext = nil + sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext) + end +end + +require 'geometry' +ActiveRecord::Base.send(:extend, Technoweenie::AttachmentFu::ActMethods) +Technoweenie::AttachmentFu.tempfile_path = ATTACHMENT_FU_TEMPFILE_PATH if Object.const_defined?(:ATTACHMENT_FU_TEMPFILE_PATH) +FileUtils.mkdir_p Technoweenie::AttachmentFu.tempfile_path \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/install.rb b/vendor/plugins/attachment_fu/install.rb new file mode 100644 index 0000000..2938164 --- /dev/null +++ b/vendor/plugins/attachment_fu/install.rb @@ -0,0 +1,5 @@ +require 'fileutils' + +s3_config = File.dirname(__FILE__) + '/../../../config/amazon_s3.yml' +FileUtils.cp File.dirname(__FILE__) + '/amazon_s3.yml.tpl', s3_config unless File.exist?(s3_config) +puts IO.read(File.join(File.dirname(__FILE__), 'README')) \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/geometry.rb b/vendor/plugins/attachment_fu/lib/geometry.rb new file mode 100644 index 0000000..2d6e381 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/geometry.rb @@ -0,0 +1,93 @@ +# This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry. +# Use #new_dimensions_for to get new dimensons +# Used so I can use spiffy RMagick geometry strings with ImageScience +class Geometry + # ! and @ are removed until support for them is added + FLAGS = ['', '%', '<', '>']#, '!', '@'] + RFLAGS = { '%' => :percent, + '!' => :aspect, + '<' => :>, + '>' => :<, + '@' => :area } + + attr_accessor :width, :height, :x, :y, :flag + + def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil) + # Support floating-point width and height arguments so Geometry + # objects can be used to specify Image#density= arguments. + raise ArgumentError, "width must be >= 0: #{width}" if width < 0 + raise ArgumentError, "height must be >= 0: #{height}" if height < 0 + @width = width.to_f + @height = height.to_f + @x = x.to_i + @y = y.to_i + @flag = flag + end + + # Construct an object from a geometry string + RE = /\A(\d*)(?:x(\d+))?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/ + + def self.from_s(str) + raise(ArgumentError, "no geometry string specified") unless str + + if m = RE.match(str) + new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]]) + else + raise ArgumentError, "invalid geometry format" + end + end + + # Convert object to a geometry string + def to_s + str = '' + str << "%g" % @width if @width > 0 + str << 'x' if (@width > 0 || @height > 0) + str << "%g" % @height if @height > 0 + str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0) + str << FLAGS[@flag.to_i] + end + + # attempts to get new dimensions for the current geometry string given these old dimensions. + # This doesn't implement the aspect flag (!) or the area flag (@). PDI + def new_dimensions_for(orig_width, orig_height) + new_width = orig_width + new_height = orig_height + + case @flag + when :percent + scale_x = @width.zero? ? 100 : @width + scale_y = @height.zero? ? @width : @height + new_width = scale_x.to_f * (orig_width.to_f / 100.0) + new_height = scale_y.to_f * (orig_height.to_f / 100.0) + when :<, :>, nil + scale_factor = + if new_width.zero? || new_height.zero? + 1.0 + else + if @width.nonzero? && @height.nonzero? + [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min + else + @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f) + end + end + new_width = scale_factor * new_width.to_f + new_height = scale_factor * new_height.to_f + new_width = orig_width if @flag && orig_width.send(@flag, new_width) + new_height = orig_height if @flag && orig_height.send(@flag, new_height) + end + + [new_width, new_height].collect! { |v| v.round } + end +end + +class Array + # allows you to get new dimensions for the current array of dimensions with a given geometry string + # + # [50, 64] / '40>' # => [40, 51] + def /(geometry) + raise ArgumentError, "Only works with a [width, height] pair" if size != 2 + raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry) + geometry = Geometry.from_s(geometry) if geometry.is_a?(String) + geometry.new_dimensions_for first, last + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb new file mode 100644 index 0000000..8894283 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu.rb @@ -0,0 +1,405 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + @@default_processors = %w(ImageScience Rmagick MiniMagick) + @@tempfile_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu') + @@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'] + mattr_reader :content_types, :tempfile_path, :default_processors + mattr_writer :tempfile_path + + class ThumbnailError < StandardError; end + class AttachmentError < StandardError; end + + module ActMethods + # Options: + # * :content_type - Allowed content types. Allows all by default. Use :image to allow all standard image types. + # * :min_size - Minimum size allowed. 1 byte is the default. + # * :max_size - Maximum size allowed. 1.megabyte is the default. + # * :size - Range of sizes allowed. (1..1.megabyte) is the default. This overrides the :min_size and :max_size options. + # * :resize_to - Used by RMagick to resize images. Pass either an array of width/height, or a geometry string. + # * :thumbnails - Specifies a set of thumbnails to generate. This accepts a hash of filename suffixes and RMagick resizing options. + # * :thumbnail_class - Set what class to use for thumbnails. This attachment class is used by default. + # * :path_prefix - path to store the uploaded files. Uses public/#{table_name} by default for the filesystem, and just #{table_name} + # for the S3 backend. Setting this sets the :storage to :file_system. + # * :storage - Use :file_system to specify the attachment data is stored with the file system. Defaults to :db_system. + # + # Examples: + # has_attachment :max_size => 1.kilobyte + # has_attachment :size => 1.megabyte..2.megabytes + # has_attachment :content_type => 'application/pdf' + # has_attachment :content_type => ['application/pdf', 'application/msword', 'text/plain'] + # has_attachment :content_type => :image, :resize_to => [50,50] + # has_attachment :content_type => ['application/pdf', :image], :resize_to => 'x50' + # has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # has_attachment :storage => :file_system, :path_prefix => 'public/files' + # has_attachment :storage => :file_system, :path_prefix => 'public/files', + # :content_type => :image, :resize_to => [50,50] + # has_attachment :storage => :file_system, :path_prefix => 'public/files', + # :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # has_attachment :storage => :s3 + def has_attachment(options = {}) + # this allows you to redefine the acts' options for each subclass, however + options[:min_size] ||= 1 + options[:max_size] ||= 1.megabyte + options[:size] ||= (options[:min_size]..options[:max_size]) + options[:thumbnails] ||= {} + options[:thumbnail_class] ||= self + options[:s3_access] ||= :public_read + options[:content_type] = [options[:content_type]].flatten.collect! { |t| t == :image ? Technoweenie::AttachmentFu.content_types : t }.flatten unless options[:content_type].nil? + + unless options[:thumbnails].is_a?(Hash) + raise ArgumentError, ":thumbnails option should be a hash: e.g. :thumbnails => { :foo => '50x50' }" + end + + # doing these shenanigans so that #attachment_options is available to processors and backends + class_inheritable_accessor :attachment_options + self.attachment_options = options + + # only need to define these once on a class + unless included_modules.include?(InstanceMethods) + attr_accessor :thumbnail_resize_options + + attachment_options[:storage] ||= (attachment_options[:file_system_path] || attachment_options[:path_prefix]) ? :file_system : :db_file + attachment_options[:path_prefix] ||= attachment_options[:file_system_path] + if attachment_options[:path_prefix].nil? + attachment_options[:path_prefix] = attachment_options[:storage] == :s3 ? table_name : File.join("public", table_name) + end + attachment_options[:path_prefix] = attachment_options[:path_prefix][1..-1] if options[:path_prefix].first == '/' + + with_options :foreign_key => 'parent_id' do |m| + m.has_many :thumbnails, :class_name => attachment_options[:thumbnail_class].to_s + m.belongs_to :parent, :class_name => base_class.to_s + end + before_destroy :destroy_thumbnails + + before_validation :set_size_from_temp_path + after_save :after_process_attachment + after_destroy :destroy_file + extend ClassMethods + include InstanceMethods + include Technoweenie::AttachmentFu::Backends.const_get("#{options[:storage].to_s.classify}Backend") + case attachment_options[:processor] + when :none + when nil + processors = Technoweenie::AttachmentFu.default_processors.dup + begin + include Technoweenie::AttachmentFu::Processors.const_get("#{processors.first}Processor") if processors.any? + rescue LoadError, MissingSourceFile + processors.shift + retry + end + else + begin + include Technoweenie::AttachmentFu::Processors.const_get("#{options[:processor].to_s.classify}Processor") + rescue LoadError, MissingSourceFile + puts "Problems loading #{options[:processor]}Processor: #{$!}" + end + end + after_validation :process_attachment + end + end + end + + module ClassMethods + delegate :content_types, :to => Technoweenie::AttachmentFu + + # Performs common validations for attachment models. + def validates_as_attachment + validates_presence_of :size, :content_type, :filename + validate :attachment_attributes_valid? + end + + # Returns true or false if the given content type is recognized as an image. + def image?(content_type) + content_types.include?(content_type) + end + + # Callback after an image has been resized. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # after_resize do |record, img| + # record.aspect_ratio = img.columns.to_f / img.rows.to_f + # end + # end + def after_resize(&block) + write_inheritable_array(:after_resize, [block]) + end + + # Callback after an attachment has been saved either to the file system or the DB. + # Only called if the file has been changed, not necessarily if the record is updated. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # after_attachment_saved do |record| + # ... + # end + # end + def after_attachment_saved(&block) + write_inheritable_array(:after_attachment_saved, [block]) + end + + # Callback before a thumbnail is saved. Use this to pass any necessary extra attributes that may be required. + # + # class Foo < ActiveRecord::Base + # acts_as_attachment + # before_thumbnail_saved do |record, thumbnail| + # ... + # end + # end + def before_thumbnail_saved(&block) + write_inheritable_array(:before_thumbnail_saved, [block]) + end + + # Get the thumbnail class, which is the current attachment class by default. + # Configure this with the :thumbnail_class option. + def thumbnail_class + attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class) + attachment_options[:thumbnail_class] + end + + # Copies the given file path to a new tempfile, returning the closed tempfile. + def copy_to_temp_file(file, temp_base_name) + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp| + tmp.close + FileUtils.cp file, tmp.path + end + end + + # Writes the given data to a new tempfile, returning the closed tempfile. + def write_to_temp_file(data, temp_base_name) + returning Tempfile.new(temp_base_name, Technoweenie::AttachmentFu.tempfile_path) do |tmp| + tmp.binmode + tmp.write data + tmp.close + end + end + end + + module InstanceMethods + # Checks whether the attachment's content type is an image content type + def image? + self.class.image?(content_type) + end + + # Returns true/false if an attachment is thumbnailable. A thumbnailable attachment has an image content type and the parent_id attribute. + def thumbnailable? + image? && respond_to?(:parent_id) + end + + # Returns the class used to create new thumbnails for this attachment. + def thumbnail_class + self.class.thumbnail_class + end + + # Gets the thumbnail name for a filename. 'foo.jpg' becomes 'foo_thumbnail.jpg' + def thumbnail_name_for(thumbnail = nil) + return filename if thumbnail.blank? + ext = nil + basename = filename.gsub /\.\w+$/ do |s| + ext = s; '' + end + "#{basename}_#{thumbnail}#{ext}" + end + + # Creates or updates the thumbnail for the current attachment. + def create_or_update_thumbnail(temp_file, file_name_suffix, *size) + thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column")) + returning find_or_initialize_thumbnail(file_name_suffix) do |thumb| + thumb.attributes = { + :content_type => content_type, + :filename => thumbnail_name_for(file_name_suffix), + :temp_path => temp_file, + :thumbnail_resize_options => size + } + callback_with_args :before_thumbnail_saved, thumb + thumb.save! + end + end + + # Sets the content type. + def content_type=(new_type) + write_attribute :content_type, new_type.to_s.strip + end + + # Sanitizes a filename. + def filename=(new_name) + write_attribute :filename, sanitize_filename(new_name) + end + + # Returns the width/height in a suitable format for the image_tag helper: (100x100) + def image_size + [width.to_s, height.to_s] * 'x' + end + + # Returns true if the attachment data will be written to the storage system on the next save + def save_attachment? + File.file?(temp_path.to_s) + end + + # nil placeholder in case this field is used in a form. + def uploaded_data() nil; end + + # This method handles the uploaded file object. If you set the field name to uploaded_data, you don't need + # any special code in your controller. + # + # <% form_for :attachment, :html => { :multipart => true } do |f| -%> + #

    <%= f.file_field :uploaded_data %>

    + #

    <%= submit_tag :Save %> + # <% end -%> + # + # @attachment = Attachment.create! params[:attachment] + # + # TODO: Allow it to work with Merb tempfiles too. + def uploaded_data=(file_data) + return nil if file_data.nil? || file_data.size == 0 + self.content_type = file_data.content_type + self.filename = file_data.original_filename if respond_to?(:filename) + if file_data.is_a?(StringIO) + file_data.rewind + self.temp_data = file_data.read + else + self.temp_path = file_data.path + end + end + + # Gets the latest temp path from the collection of temp paths. While working with an attachment, + # multiple Tempfile objects may be created for various processing purposes (resizing, for example). + # An array of all the tempfile objects is stored so that the Tempfile instance is held on to until + # it's not needed anymore. The collection is cleared after saving the attachment. + def temp_path + p = temp_paths.first + p.respond_to?(:path) ? p.path : p.to_s + end + + # Gets an array of the currently used temp paths. Defaults to a copy of #full_filename. + def temp_paths + @temp_paths ||= (new_record? || !File.exist?(full_filename)) ? [] : [copy_to_temp_file(full_filename)] + end + + # Adds a new temp_path to the array. This should take a string or a Tempfile. This class makes no + # attempt to remove the files, so Tempfiles should be used. Tempfiles remove themselves when they go out of scope. + # You can also use string paths for temporary files, such as those used for uploaded files in a web server. + def temp_path=(value) + temp_paths.unshift value + temp_path + end + + # Gets the data from the latest temp file. This will read the file into memory. + def temp_data + save_attachment? ? File.read(temp_path) : nil + end + + # Writes the given data to a Tempfile and adds it to the collection of temp files. + def temp_data=(data) + self.temp_path = write_to_temp_file data unless data.nil? + end + + # Copies the given file to a randomly named Tempfile. + def copy_to_temp_file(file) + self.class.copy_to_temp_file file, random_tempfile_filename + end + + # Writes the given file to a randomly named Tempfile. + def write_to_temp_file(data) + self.class.write_to_temp_file data, random_tempfile_filename + end + + # Stub for creating a temp file from the attachment data. This should be defined in the backend module. + def create_temp_file() end + + # Allows you to work with a processed representation (RMagick, ImageScience, etc) of the attachment in a block. + # + # @attachment.with_image do |img| + # self.data = img.thumbnail(100, 100).to_blob + # end + # + def with_image(&block) + self.class.with_image(temp_path, &block) + end + + protected + # Generates a unique filename for a Tempfile. + def random_tempfile_filename + "#{rand Time.now.to_i}#{filename || 'attachment'}" + end + + def sanitize_filename(filename) + returning filename.strip do |name| + # NOTE: File.basename doesn't work right with Windows paths on Unix + # get only the filename, not the whole path + name.gsub! /^.*(\\|\/)/, '' + + # Finally, replace all non alphanumeric, underscore or periods with underscore + name.gsub! /[^\w\.\-]/, '_' + end + end + + # before_validation callback. + def set_size_from_temp_path + self.size = File.size(temp_path) if save_attachment? + end + + # validates the size and content_type attributes according to the current model's options + def attachment_attributes_valid? + [:size, :content_type].each do |attr_name| + enum = attachment_options[attr_name] + errors.add attr_name, ActiveRecord::Errors.default_error_messages[:inclusion] unless enum.nil? || enum.include?(send(attr_name)) + end + end + + # Initializes a new thumbnail with the given suffix. + def find_or_initialize_thumbnail(file_name_suffix) + respond_to?(:parent_id) ? + thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) : + thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s) + end + + # Stub for a #process_attachment method in a processor + def process_attachment + @saved_attachment = save_attachment? + end + + # Cleans up after processing. Thumbnails are created, the attachment is stored to the backend, and the temp_paths are cleared. + def after_process_attachment + if @saved_attachment + if respond_to?(:process_attachment_with_processing) && thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil? + temp_file = temp_path || create_temp_file + attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) } + end + save_to_storage + @temp_paths.clear + @saved_attachment = nil + callback :after_attachment_saved + end + end + + # Resizes the given processed img object with either the attachment resize options or the thumbnail resize options. + def resize_image_or_thumbnail!(img) + if (!respond_to?(:parent_id) || parent_id.nil?) && attachment_options[:resize_to] # parent image + resize_image(img, attachment_options[:resize_to]) + elsif thumbnail_resize_options # thumbnail + resize_image(img, thumbnail_resize_options) + end + end + + # Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self. + # Only accept blocks, however + def callback_with_args(method, arg = self) + notify(method) + + result = nil + callbacks_for(method).each do |callback| + result = callback.call(self, arg) + return false if result == false + end + + return result + end + + # Removes the thumbnails for the attachment, if it has any + def destroy_thumbnails + self.thumbnails.each { |thumbnail| thumbnail.destroy } if thumbnailable? + end + end + end +end diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb new file mode 100644 index 0000000..23881e7 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/db_file_backend.rb @@ -0,0 +1,39 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # Methods for DB backed attachments + module DbFileBackend + def self.included(base) #:nodoc: + Object.const_set(:DbFile, Class.new(ActiveRecord::Base)) unless Object.const_defined?(:DbFile) + base.belongs_to :db_file, :class_name => '::DbFile', :foreign_key => 'db_file_id' + end + + # Creates a temp file with the current db data. + def create_temp_file + write_to_temp_file current_data + end + + # Gets the current data from the database + def current_data + db_file.data + end + + protected + # Destroys the file. Called in the after_destroy callback + def destroy_file + db_file.destroy if db_file + end + + # Saves the data to the DbFile model + def save_to_storage + if save_attachment? + (db_file || build_db_file).data = temp_data + db_file.save! + self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id] + end + true + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb new file mode 100644 index 0000000..464b9c7 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/file_system_backend.rb @@ -0,0 +1,97 @@ +require 'ftools' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # Methods for file system backed attachments + module FileSystemBackend + def self.included(base) #:nodoc: + base.before_update :rename_file + end + + # Gets the full path to the filename in this format: + # + # # This assumes a model name like MyModel + # # public/#{table_name} is the default filesystem path + # RAILS_ROOT/public/my_models/5/blah.jpg + # + # Overwrite this method in your model to customize the filename. + # The optional thumbnail argument will output the thumbnail's filename. + def full_filename(thumbnail = nil) + file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s + File.join(RAILS_ROOT, file_system_path, *partitioned_path(thumbnail_name_for(thumbnail))) + end + + # Used as the base path that #public_filename strips off full_filename to create the public path + def base_path + @base_path ||= File.join(RAILS_ROOT, 'public') + end + + # The attachment ID used in the full path of a file + def attachment_path_id + ((respond_to?(:parent_id) && parent_id) || id).to_i + end + + # overrwrite this to do your own app-specific partitioning. + # you can thank Jamis Buck for this: http://www.37signals.com/svn/archives2/id_partitioning.php + def partitioned_path(*args) + ("%08d" % attachment_path_id).scan(/..../) + args + end + + # Gets the public path to the file + # The optional thumbnail argument will output the thumbnail's filename. + def public_filename(thumbnail = nil) + full_filename(thumbnail).gsub %r(^#{Regexp.escape(base_path)}), '' + end + + def filename=(value) + @old_filename = full_filename unless filename.nil? || @old_filename + write_attribute :filename, sanitize_filename(value) + end + + # Creates a temp file from the currently saved file. + def create_temp_file + copy_to_temp_file full_filename + end + + protected + # Destroys the file. Called in the after_destroy callback + def destroy_file + FileUtils.rm full_filename + # remove directory also if it is now empty + Dir.rmdir(File.dirname(full_filename)) if (Dir.entries(File.dirname(full_filename))-['.','..']).empty? + rescue + logger.info "Exception destroying #{full_filename.inspect}: [#{$!.class.name}] #{$1.to_s}" + logger.warn $!.backtrace.collect { |b| " > #{b}" }.join("\n") + end + + # Renames the given file before saving + def rename_file + return unless @old_filename && @old_filename != full_filename + if save_attachment? && File.exists?(@old_filename) + FileUtils.rm @old_filename + elsif File.exists?(@old_filename) + FileUtils.mv @old_filename, full_filename + end + @old_filename = nil + true + end + + # Saves the file to the file system + def save_to_storage + if save_attachment? + # TODO: This overwrites the file if it exists, maybe have an allow_overwrite option? + FileUtils.mkdir_p(File.dirname(full_filename)) + File.cp(temp_path, full_filename) + File.chmod(attachment_options[:chmod] || 0644, full_filename) + end + @old_filename = nil + true + end + + def current_data + File.file?(full_filename) ? File.read(full_filename) : nil + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb new file mode 100644 index 0000000..dab1291 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/backends/s3_backend.rb @@ -0,0 +1,309 @@ +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Backends + # = AWS::S3 Storage Backend + # + # Enables use of {Amazon's Simple Storage Service}[http://aws.amazon.com/s3] as a storage mechanism + # + # == Requirements + # + # Requires the {AWS::S3 Library}[http://amazon.rubyforge.org] for S3 by Marcel Molina Jr. installed either + # as a gem or a as a Rails plugin. + # + # == Configuration + # + # Configuration is done via RAILS_ROOT/config/amazon_s3.yml and is loaded according to the RAILS_ENV. + # The minimum connection options that you must specify are a bucket name, your access key id and your secret access key. + # If you don't already have your access keys, all you need to sign up for the S3 service is an account at Amazon. + # You can sign up for S3 and get access keys by visiting http://aws.amazon.com/s3. + # + # Example configuration (RAILS_ROOT/config/amazon_s3.yml) + # + # development: + # bucket_name: appname_development + # access_key_id: + # secret_access_key: + # + # test: + # bucket_name: appname_test + # access_key_id: + # secret_access_key: + # + # production: + # bucket_name: appname + # access_key_id: + # secret_access_key: + # + # You can change the location of the config path by passing a full path to the :s3_config_path option. + # + # has_attachment :storage => :s3, :s3_config_path => (RAILS_ROOT + '/config/s3.yml') + # + # === Required configuration parameters + # + # * :access_key_id - The access key id for your S3 account. Provided by Amazon. + # * :secret_access_key - The secret access key for your S3 account. Provided by Amazon. + # * :bucket_name - A unique bucket name (think of the bucket_name as being like a database name). + # + # If any of these required arguments is missing, a MissingAccessKey exception will be raised from AWS::S3. + # + # == About bucket names + # + # Bucket names have to be globaly unique across the S3 system. And you can only have up to 100 of them, + # so it's a good idea to think of a bucket as being like a database, hence the correspondance in this + # implementation to the development, test, and production environments. + # + # The number of objects you can store in a bucket is, for all intents and purposes, unlimited. + # + # === Optional configuration parameters + # + # * :server - The server to make requests to. Defaults to s3.amazonaws.com. + # * :port - The port to the requests should be made on. Defaults to 80 or 443 if :use_ssl is set. + # * :use_ssl - If set to true, :port will be implicitly set to 443, unless specified otherwise. Defaults to false. + # + # == Usage + # + # To specify S3 as the storage mechanism for a model, set the acts_as_attachment :storage option to :s3. + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3 + # end + # + # === Customizing the path + # + # By default, files are prefixed using a pseudo hierarchy in the form of :table_name/:id, which results + # in S3 urls that look like: http(s)://:server/:bucket_name/:table_name/:id/:filename with :table_name + # representing the customizable portion of the path. You can customize this prefix using the :path_prefix + # option: + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3, :path_prefix => 'my/custom/path' + # end + # + # Which would result in URLs like http(s)://:server/:bucket_name/my/custom/path/:id/:filename. + # + # === Permissions + # + # By default, files are stored on S3 with public access permissions. You can customize this using + # the :s3_access option to has_attachment. Available values are + # :private, :public_read_write, and :authenticated_read. + # + # === Other options + # + # Of course, all the usual configuration options apply, such as content_type and thumbnails: + # + # class Photo < ActiveRecord::Base + # has_attachment :storage => :s3, :content_type => ['application/pdf', :image], :resize_to => 'x50' + # has_attachment :storage => :s3, :thumbnails => { :thumb => [50, 50], :geometry => 'x50' } + # end + # + # === Accessing S3 URLs + # + # You can get an object's URL using the s3_url accessor. For example, assuming that for your postcard app + # you had a bucket name like 'postcard_world_development', and an attachment model called Photo: + # + # @postcard.s3_url # => http(s)://s3.amazonaws.com/postcard_world_development/photos/1/mexico.jpg + # + # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file. + # The optional thumbnail argument will output the thumbnail's filename (if any). + # + # Additionally, you can get an object's base path relative to the bucket root using + # base_path: + # + # @photo.file_base_path # => photos/1 + # + # And the full path (including the filename) using full_filename: + # + # @photo.full_filename # => photos/ + # + # Niether base_path or full_filename include the bucket name as part of the path. + # You can retrieve the bucket name using the bucket_name method. + module S3Backend + class RequiredLibraryNotFoundError < StandardError; end + class ConfigFileNotFoundError < StandardError; end + + def self.included(base) #:nodoc: + mattr_reader :bucket_name, :s3_config + + begin + require 'aws/s3' + include AWS::S3 + rescue LoadError + raise RequiredLibraryNotFoundError.new('AWS::S3 could not be loaded') + end + + begin + @@s3_config_path = base.attachment_options[:s3_config_path] || (RAILS_ROOT + '/config/amazon_s3.yml') + @@s3_config = YAML.load_file(@@s3_config_path)[ENV['RAILS_ENV']].symbolize_keys + #rescue + # raise ConfigFileNotFoundError.new('File %s not found' % @@s3_config_path) + end + + @@bucket_name = s3_config[:bucket_name] + + Base.establish_connection!( + :access_key_id => s3_config[:access_key_id], + :secret_access_key => s3_config[:secret_access_key], + :server => s3_config[:server], + :port => s3_config[:port], + :use_ssl => s3_config[:use_ssl] + ) + + # Bucket.create(@@bucket_name) + + base.before_update :rename_file + end + + def self.protocol + @protocol ||= s3_config[:use_ssl] ? 'https://' : 'http://' + end + + def self.hostname + @hostname ||= s3_config[:server] || AWS::S3::DEFAULT_HOST + end + + def self.port_string + @port_string ||= s3_config[:port] == (s3_config[:use_ssl] ? 443 : 80) ? '' : ":#{s3_config[:port]}" + end + + module ClassMethods + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + end + + # Overwrites the base filename writer in order to store the old filename + def filename=(value) + @old_filename = filename unless filename.nil? || @old_filename + write_attribute :filename, sanitize_filename(value) + end + + # The attachment ID used in the full path of a file + def attachment_path_id + ((respond_to?(:parent_id) && parent_id) || id).to_s + end + + # The pseudo hierarchy containing the file relative to the bucket name + # Example: :table_name/:id + def base_path + File.join(attachment_options[:path_prefix], attachment_path_id) + end + + # The full path to the file relative to the bucket name + # Example: :table_name/:id/:filename + def full_filename(thumbnail = nil) + File.join(base_path, thumbnail_name_for(thumbnail)) + end + + # All public objects are accessible via a GET request to the S3 servers. You can generate a + # url for an object using the s3_url method. + # + # @photo.s3_url + # + # The resulting url is in the form: http(s)://:server/:bucket_name/:table_name/:id/:file where + # the :server variable defaults to AWS::S3 URL::DEFAULT_HOST (s3.amazonaws.com) and can be + # set using the configuration parameters in RAILS_ROOT/config/amazon_s3.yml. + # + # The optional thumbnail argument will output the thumbnail's filename (if any). + def s3_url(thumbnail = nil) + File.join(s3_protocol + s3_hostname + s3_port_string, bucket_name, full_filename(thumbnail)) + end + alias :public_filename :s3_url + + # All private objects are accessible via an authenticated GET request to the S3 servers. You can generate an + # authenticated url for an object like this: + # + # @photo.authenticated_s3_url + # + # By default authenticated urls expire 5 minutes after they were generated. + # + # Expiration options can be specified either with an absolute time using the :expires option, + # or with a number of seconds relative to now with the :expires_in option: + # + # # Absolute expiration date (October 13th, 2025) + # @photo.authenticated_s3_url(:expires => Time.mktime(2025,10,13).to_i) + # + # # Expiration in five hours from now + # @photo.authenticated_s3_url(:expires_in => 5.hours) + # + # You can specify whether the url should go over SSL with the :use_ssl option. + # By default, the ssl settings for the current connection will be used: + # + # @photo.authenticated_s3_url(:use_ssl => true) + # + # Finally, the optional thumbnail argument will output the thumbnail's filename (if any): + # + # @photo.authenticated_s3_url('thumbnail', :expires_in => 5.hours, :use_ssl => true) + def authenticated_s3_url(*args) + thumbnail = args.first.is_a?(String) ? args.first : nil + options = args.last.is_a?(Hash) ? args.last : {} + S3Object.url_for(full_filename(thumbnail), bucket_name, options) + end + + def create_temp_file + write_to_temp_file current_data + end + + def current_data + S3Object.value full_filename, bucket_name + end + + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + + protected + # Called in the after_destroy callback + def destroy_file + S3Object.delete full_filename, bucket_name + end + + def rename_file + return unless @old_filename && @old_filename != filename + + old_full_filename = File.join(base_path, @old_filename) + + S3Object.rename( + old_full_filename, + full_filename, + bucket_name, + :access => attachment_options[:s3_access] + ) + + @old_filename = nil + true + end + + def save_to_storage + if save_attachment? + S3Object.store( + full_filename, + temp_data, + bucket_name, + :content_type => content_type, + :access => attachment_options[:s3_access] + ) + end + + @old_filename = nil + true + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb new file mode 100644 index 0000000..37c1415 --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/image_science_processor.rb @@ -0,0 +1,55 @@ +require 'image_science' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module ImageScienceProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an RMagick Image for the given binary data. + def with_image(file, &block) + ::ImageScience.with_image file, &block + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing && image? + with_image do |img| + self.width = img.width if respond_to?(:width) + self.height = img.height if respond_to?(:height) + resize_image_or_thumbnail! img + end + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + # create a dummy temp file to write to + filename.sub! /gif$/, 'png' + self.temp_path = write_to_temp_file(filename) + grab_dimensions = lambda do |img| + self.width = img.width if respond_to?(:width) + self.height = img.height if respond_to?(:height) + img.save temp_path + callback_with_args :after_resize, img + end + + size = size.first if size.is_a?(Array) && size.length == 1 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + if size.is_a?(Fixnum) + img.thumbnail(size, &grab_dimensions) + else + img.resize(size[0], size[1], &grab_dimensions) + end + else + new_size = [img.width, img.height] / size.to_s + img.resize(new_size[0], new_size[1], &grab_dimensions) + end + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb new file mode 100644 index 0000000..e5a534c --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/mini_magick_processor.rb @@ -0,0 +1,56 @@ +require 'mini_magick' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module MiniMagickProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an MiniMagick Image for the given binary data. + def with_image(file, &block) + begin + binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick) + rescue + # Log the failure to load the image. + logger.debug("Exception working with image: #{$!}") + binary_data = nil + end + block.call binary_data if block && binary_data + ensure + !binary_data.nil? + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing + with_image do |img| + resize_image_or_thumbnail! img + self.width = img[:width] if respond_to?(:width) + self.height = img[:height] if respond_to?(:height) + callback_with_args :after_resize, img + end if image? + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + size = size.first if size.is_a?(Array) && size.length == 1 + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + if size.is_a?(Fixnum) + size = [size, size] + img.resize(size.join('x')) + else + img.resize(size.join('x') + '!') + end + else + img.resize(size.to_s) + end + self.temp_path = img + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb new file mode 100644 index 0000000..7999edb --- /dev/null +++ b/vendor/plugins/attachment_fu/lib/technoweenie/attachment_fu/processors/rmagick_processor.rb @@ -0,0 +1,53 @@ +require 'RMagick' +module Technoweenie # :nodoc: + module AttachmentFu # :nodoc: + module Processors + module RmagickProcessor + def self.included(base) + base.send :extend, ClassMethods + base.alias_method_chain :process_attachment, :processing + end + + module ClassMethods + # Yields a block containing an RMagick Image for the given binary data. + def with_image(file, &block) + begin + binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick) + rescue + # Log the failure to load the image. This should match ::Magick::ImageMagickError + # but that would cause acts_as_attachment to require rmagick. + logger.debug("Exception working with image: #{$!}") + binary_data = nil + end + block.call binary_data if block && binary_data + ensure + !binary_data.nil? + end + end + + protected + def process_attachment_with_processing + return unless process_attachment_without_processing + with_image do |img| + resize_image_or_thumbnail! img + self.width = img.columns if respond_to?(:width) + self.height = img.rows if respond_to?(:height) + callback_with_args :after_resize, img + end if image? + end + + # Performs the actual resizing operation for a thumbnail + def resize_image(img, size) + size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum) + if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum)) + size = [size, size] if size.is_a?(Fixnum) + img.thumbnail!(*size) + else + img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) } + end + self.temp_path = write_to_temp_file(img.to_blob) + end + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/amazon_s3.yml b/vendor/plugins/attachment_fu/test/amazon_s3.yml new file mode 100644 index 0000000..0024c8e --- /dev/null +++ b/vendor/plugins/attachment_fu/test/amazon_s3.yml @@ -0,0 +1,6 @@ +test: + bucket_name: afu + access_key_id: YOURACCESSKEY + secret_access_key: YOURSECRETACCESSKEY + server: 127.0.0.1 + port: 3002 \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/db_file_test.rb b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb new file mode 100644 index 0000000..e95bb49 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/db_file_test.rb @@ -0,0 +1,16 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class DbFileTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model Attachment + + def test_should_call_after_attachment_saved(klass = Attachment) + attachment_model.saves = 0 + assert_created do + upload_file :filename => '/files/rails.png' + end + assert_equal 1, attachment_model.saves + end + + test_against_subclass :test_should_call_after_attachment_saved, Attachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/file_system_test.rb b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb new file mode 100644 index 0000000..d3250c1 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/file_system_test.rb @@ -0,0 +1,80 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class FileSystemTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model FileAttachment + + def test_filesystem_size_for_file_attachment(klass = FileAttachment) + attachment_model klass + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_equal attachment.size, File.open(attachment.full_filename).stat.size + end + end + + test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment + + def test_should_not_overwrite_file_attachment(klass = FileAttachment) + attachment_model klass + assert_created 2 do + real = upload_file :filename => '/files/rails.png' + assert_valid real + assert !real.new_record?, real.errors.full_messages.join("\n") + assert !real.size.zero? + + fake = upload_file :filename => '/files/fake/rails.png' + assert_valid fake + assert !fake.size.zero? + + assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size + end + end + + test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment + + def test_should_store_file_attachment_in_filesystem(klass = FileAttachment) + attachment_model klass + attachment = nil + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + end + attachment + end + + test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment + + def test_should_delete_old_file_when_updating(klass = FileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + old_filename = attachment.full_filename + assert_not_created do + use_temp_file 'files/rails.png' do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + assert !File.exists?(old_filename), "#{old_filename} still exists" + end + end + end + + test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment + + def test_should_delete_old_file_when_renaming(klass = FileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + old_filename = attachment.full_filename + assert_not_created do + attachment.filename = 'rails2.png' + attachment.save + assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist" + assert !File.exists?(old_filename), "#{old_filename} still exists" + assert !attachment.reload.size.zero? + assert_equal 'rails2.png', attachment.filename + end + end + + test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb new file mode 100644 index 0000000..82520a0 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/backends/remote/s3_test.rb @@ -0,0 +1,103 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper')) +require 'net/http' + +class S3Test < Test::Unit::TestCase + if File.exist?(File.join(File.dirname(__FILE__), '../../amazon_s3.yml')) + include BaseAttachmentTests + attachment_model S3Attachment + + def test_should_create_correct_bucket_name(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal attachment.s3_config[:bucket_name], attachment.bucket_name + end + + test_against_subclass :test_should_create_correct_bucket_name, S3Attachment + + def test_should_create_default_path_prefix(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal File.join(attachment_model.table_name, attachment.attachment_path_id), attachment.base_path + end + + test_against_subclass :test_should_create_default_path_prefix, S3Attachment + + def test_should_create_custom_path_prefix(klass = S3WithPathPrefixAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal File.join('some/custom/path/prefix', attachment.attachment_path_id), attachment.base_path + end + + test_against_subclass :test_should_create_custom_path_prefix, S3WithPathPrefixAttachment + + def test_should_create_valid_url(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_equal "#{s3_protocol}#{s3_hostname}#{s3_port_string}/#{attachment.bucket_name}/#{attachment.full_filename}", attachment.s3_url + end + + test_against_subclass :test_should_create_valid_url, S3Attachment + + def test_should_create_authenticated_url(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + assert_match /^http.+AWSAccessKeyId.+Expires.+Signature.+/, attachment.authenticated_s3_url(:use_ssl => true) + end + + test_against_subclass :test_should_create_authenticated_url, S3Attachment + + def test_should_save_attachment(klass = S3Attachment) + attachment_model klass + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + assert !attachment.size.zero? + assert_kind_of Net::HTTPOK, http_response_for(attachment.s3_url) + end + end + + test_against_subclass :test_should_save_attachment, S3Attachment + + def test_should_delete_attachment_from_s3_when_attachment_record_destroyed(klass = S3Attachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + + urls = [attachment.s3_url] + attachment.thumbnails.collect(&:s3_url) + + urls.each {|url| assert_kind_of Net::HTTPOK, http_response_for(url) } + attachment.destroy + urls.each do |url| + begin + http_response_for(url) + rescue Net::HTTPForbidden, Net::HTTPNotFound + nil + end + end + end + + test_against_subclass :test_should_delete_attachment_from_s3_when_attachment_record_destroyed, S3Attachment + + protected + def http_response_for(url) + url = URI.parse(url) + Net::HTTP.start(url.host, url.port) {|http| http.request_head(url.path) } + end + + def s3_protocol + Technoweenie::AttachmentFu::Backends::S3Backend.protocol + end + + def s3_hostname + Technoweenie::AttachmentFu::Backends::S3Backend.hostname + end + + def s3_port_string + Technoweenie::AttachmentFu::Backends::S3Backend.port_string + end + else + def test_flunk_s3 + puts "s3 config file not loaded, tests not running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/base_attachment_tests.rb b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb new file mode 100644 index 0000000..c9dbbd7 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/base_attachment_tests.rb @@ -0,0 +1,57 @@ +module BaseAttachmentTests + def test_should_create_file_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 3, attachment.size + assert_nil attachment.width + assert_nil attachment.height + end + end + + def test_reassign_attribute_data + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.size > 0, "no data was set" + + attachment.temp_data = 'wtf' + assert attachment.save_attachment? + attachment.save! + + assert_equal 'wtf', attachment_model.find(attachment.id).send(:current_data) + end + end + + def test_no_reassign_attribute_data_on_nil + assert_created 1 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.size > 0, "no data was set" + + attachment.temp_data = nil + assert !attachment.save_attachment? + end + end + + def test_should_overwrite_old_contents_when_updating + attachment = upload_file :filename => '/files/rails.png' + assert_not_created do # no new db_file records + use_temp_file 'files/rails.png' do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + end + end + end + + def test_should_save_without_updating_file + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.save_attachment? + assert_nothing_raised { attachment.save! } + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/basic_test.rb b/vendor/plugins/attachment_fu/test/basic_test.rb new file mode 100644 index 0000000..2094eb1 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/basic_test.rb @@ -0,0 +1,64 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class BasicTest < Test::Unit::TestCase + def test_should_set_default_min_size + assert_equal 1, Attachment.attachment_options[:min_size] + end + + def test_should_set_default_max_size + assert_equal 1.megabyte, Attachment.attachment_options[:max_size] + end + + def test_should_set_default_size + assert_equal (1..1.megabyte), Attachment.attachment_options[:size] + end + + def test_should_set_default_thumbnails_option + assert_equal Hash.new, Attachment.attachment_options[:thumbnails] + end + + def test_should_set_default_thumbnail_class + assert_equal Attachment, Attachment.attachment_options[:thumbnail_class] + end + + def test_should_normalize_content_types_to_array + assert_equal %w(pdf), PdfAttachment.attachment_options[:content_type] + assert_equal %w(pdf doc txt), DocAttachment.attachment_options[:content_type] + assert_equal ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageAttachment.attachment_options[:content_type] + assert_equal ['pdf', 'image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'], ImageOrPdfAttachment.attachment_options[:content_type] + end + + def test_should_sanitize_content_type + @attachment = Attachment.new :content_type => ' foo ' + assert_equal 'foo', @attachment.content_type + end + + def test_should_sanitize_filenames + @attachment = Attachment.new :filename => 'blah/foo.bar' + assert_equal 'foo.bar', @attachment.filename + + @attachment.filename = 'blah\\foo.bar' + assert_equal 'foo.bar', @attachment.filename + + @attachment.filename = 'f o!O-.bar' + assert_equal 'f_o_O-.bar', @attachment.filename + end + + def test_should_convert_thumbnail_name + @attachment = FileAttachment.new :filename => 'foo.bar' + assert_equal 'foo.bar', @attachment.thumbnail_name_for(nil) + assert_equal 'foo.bar', @attachment.thumbnail_name_for('') + assert_equal 'foo_blah.bar', @attachment.thumbnail_name_for(:blah) + assert_equal 'foo_blah.blah.bar', @attachment.thumbnail_name_for('blah.blah') + + @attachment.filename = 'foo.bar.baz' + assert_equal 'foo.bar_blah.baz', @attachment.thumbnail_name_for(:blah) + end + + def test_should_require_valid_thumbnails_option + klass = Class.new(ActiveRecord::Base) + assert_raise ArgumentError do + klass.has_attachment :thumbnails => [] + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/database.yml b/vendor/plugins/attachment_fu/test/database.yml new file mode 100644 index 0000000..1c6ece7 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/database.yml @@ -0,0 +1,18 @@ +sqlite: + :adapter: sqlite + :dbfile: attachment_fu_plugin.sqlite.db +sqlite3: + :adapter: sqlite3 + :dbfile: attachment_fu_plugin.sqlite3.db +postgresql: + :adapter: postgresql + :username: postgres + :password: postgres + :database: attachment_fu_plugin_test + :min_messages: ERROR +mysql: + :adapter: mysql + :host: localhost + :username: rails + :password: + :database: attachment_fu_plugin_test \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/extra_attachment_test.rb b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb new file mode 100644 index 0000000..15b1852 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/extra_attachment_test.rb @@ -0,0 +1,57 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class OrphanAttachmentTest < Test::Unit::TestCase + include BaseAttachmentTests + attachment_model OrphanAttachment + + def test_should_create_image_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + end + end + + def test_should_create_file_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/foo.txt' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + end + end + + def test_should_create_image_from_uploaded_file_with_custom_content_type + assert_created do + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.image? + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + end + end + + def test_should_create_thumbnail + attachment = upload_file :filename => '/files/rails.png' + + assert_raise Technoweenie::AttachmentFu::ThumbnailError do + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50) + end + end + + def test_should_create_thumbnail_with_geometry_string + attachment = upload_file :filename => '/files/rails.png' + + assert_raise Technoweenie::AttachmentFu::ThumbnailError do + attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50') + end + end +end + +class MinimalAttachmentTest < OrphanAttachmentTest + attachment_model MinimalAttachment +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/fixtures/attachment.rb b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb new file mode 100644 index 0000000..77d60c3 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/fixtures/attachment.rb @@ -0,0 +1,127 @@ +class Attachment < ActiveRecord::Base + @@saves = 0 + cattr_accessor :saves + has_attachment :processor => :rmagick + validates_as_attachment + after_attachment_saved do |record| + self.saves += 1 + end +end + +class SmallAttachment < Attachment + has_attachment :max_size => 1.kilobyte +end + +class BigAttachment < Attachment + has_attachment :size => 1.megabyte..2.megabytes +end + +class PdfAttachment < Attachment + has_attachment :content_type => 'pdf' +end + +class DocAttachment < Attachment + has_attachment :content_type => %w(pdf doc txt) +end + +class ImageAttachment < Attachment + has_attachment :content_type => :image, :resize_to => [50,50] +end + +class ImageOrPdfAttachment < Attachment + has_attachment :content_type => ['pdf', :image], :resize_to => 'x50' +end + +class ImageWithThumbsAttachment < Attachment + has_attachment :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55] + after_resize do |record, img| + record.aspect_ratio = img.columns.to_f / img.rows.to_f + end +end + +class FileAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick + validates_as_attachment +end + +class ImageFileAttachment < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :content_type => :image, :resize_to => [50,50] +end + +class ImageWithThumbsFileAttachment < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :thumbnails => { :thumb => [50, 50], :geometry => 'x50' }, :resize_to => [55,55] + after_resize do |record, img| + record.aspect_ratio = img.columns.to_f / img.rows.to_f + end +end + +class ImageWithThumbsClassFileAttachment < FileAttachment + # use file_system_path to test backwards compatibility + has_attachment :file_system_path => 'vendor/plugins/attachment_fu/test/files', + :thumbnails => { :thumb => [50, 50] }, :resize_to => [55,55], + :thumbnail_class => 'ImageThumbnail' +end + +class ImageThumbnail < FileAttachment + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files/thumbnails' +end + +# no parent +class OrphanAttachment < ActiveRecord::Base + has_attachment :processor => :rmagick + validates_as_attachment +end + +# no filename, no size, no content_type +class MinimalAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', :processor => :rmagick + validates_as_attachment + + def filename + "#{id}.file" + end +end + +begin + class ImageScienceAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :image_science, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile + puts $!.message + puts "no ImageScience" +end + +begin + class MiniMagickAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile + puts $!.message + puts "no Mini Magick" +end + +begin + class MiniMagickAttachment < ActiveRecord::Base + has_attachment :path_prefix => 'vendor/plugins/attachment_fu/test/files', + :processor => :mini_magick, :thumbnails => { :thumb => [50, 51], :geometry => '31>' }, :resize_to => 55 + end +rescue MissingSourceFile +end + +begin + class S3Attachment < ActiveRecord::Base + has_attachment :storage => :s3, :processor => :rmagick, :s3_config_path => File.join(File.dirname(__FILE__), '../amazon_s3.yml') + validates_as_attachment + end + + class S3WithPathPrefixAttachment < S3Attachment + has_attachment :storage => :s3, :path_prefix => 'some/custom/path/prefix', :processor => :rmagick + validates_as_attachment + end +rescue Technoweenie::AttachmentFu::Backends::S3Backend::ConfigFileNotFoundError + puts "S3 error: #{$!}" +end diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/fake/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..0543c64edae591831b38730c8000e57bd4e251f8 GIT binary patch literal 4217 zcmV-<5QguGP)=Y7XlbL`H}9+GyYMPd;W5+KB_IGn~#fy4$vo5XQlUyTh74Yp~YlsZqH#tC+w z#P-vsanf9^?K-jJ*iYT0F|Ci(M-vAbiZBQXgg^*O=wQW4yQ{r-XLfe`efJ%WW@lH| zUwNNp{LW~O&;0J+J1+a-t-F#T$@GUFt3J7L<9+wKm#?C`WslZ0E@DhhmZkgSqFJ=^ zf}O|Q>=3Y(qN!PLO3$3*i1?y`kpL#;JWr2=Wf|Kj%WvI($L?=Gi8+UVU=s3JjL0=r z3apb;>=mLt!wFm_l`+*==*8J25h`Of7e5JS79V7+#)H{pMhYct_L@XeHb;}1Ea)Vn zD94|F6%&vZT*(NmeiuwY0SBInX8_8(^N(w`?I`1gzSn*|@!{KZq~Sy{lSoLR6z(R6 zC6isOX>^C7QXm2$sShbP1IA{zMLf=oKVot?)t{~@Oiy< z=#vKDe41tJ73HcX6^+2N<)(%`hq7{NwEflVCx1r|%At5-BuKSyB&c-+C$Xs5sLWt+ zgYUOU@bu3aS7)LGq4&>b&2xSYCG*pS0Kwwg%JN8kam^dMuID-aT_8lNnIr>`j zp?j*<@3JpkYPiNy;lb99e{kj-P3Z}LIz0eE}*?2E)Ve90e#8N8Dw`QCaH_ zjwN9Gn#?pOU~jfpRNIGybiEI}Z0s{4z3)*U=t#t<=P5|Pw|KPRh} zVJ`Pj-4zcgF?Ovx9U79t5p%iI)w+hSWQJdVqu8Y2gC*~vxBUXFR4Tn<|846Z{7NpX zjGaCKLy3pexm1ry@{~EM&JGH@j^I+W*j;zuY$L-3CU zH|`#K{Z};h!VS%pV%^@)kG%Ka>Csq8fx-4iE{$qIe@a?XAnI#-2bP`;^sagM?`tdS zF25K6ldasM*a?$+)gQs>#-kCT%Ap`Qj$VX#DA3fstmSk9nc?Jt=ijh+gsSb0EnoaP z48vaE()GkQX0ZKmFrtwWldr21*@sn25pV!d0!emM>`? z@9w<(%yYdzdNv*JbuL}b3Kmwd6f@381;Flc@O<5^bv3zZGs4sr? zs@~+aQI^d_12N=4E( zI>cxs9g7ne1{P6bL^M}T=3qo^#73x(o{eNO{EIWOF*S9V^5TKg11H)oOBxn#Uar42 z1ybi08_S6+AKiEIz>&UJUP|;|8~OG7ieSOD7EcC2yO<1#vOgG^vMrjbXpZA6Z6*mZ zXq2YW(5WlQp^!~n-~1DKM1_+W zj+~K0DJJQoiH^6G%jLXg`);qB);tWYmZP4pB=`ldfd)QPpo%~RYW92gY=a|E4IG>>Q0B#s!_2l#~__B$&@^C@?5xU zfMr=Z8sp3P1)H~;m@*BBVR!(Q<*k;zSyipvUlFSYy(z#hi#xCU=B>{r~%c6d$81GYE+?v79-Z%|Vrp6-`;WbiJt` zw18#|)jDND{Pg{wk3#5~iYeg`!H#njg$OA>0T6xw!jGPY;?Z+pAAK1x>6NG-eo3v& z7&WaXrfdjIsqubT;>=lAjwh;G7B4$|SMcm5KDvb=Hq8ijmYqAJ|sEC5QduA3Dn@ZR2 z-C4Ef*6|Cy!4JEVzA#0hic^Ke@ucQrnKG8AX<+fE??je#+N_))=0mp=H_0TkdaEkH zqIEZ>~@d>@$dp8hiNsM_1o{^ZcXVgn7lT_ON+l(zHf|#4?nL79F&*b#K{t?X}m` z6mqRkFMjli1r-EsC8&9!BJiE*oTYfl_hH0 zGceKN-Ly?((mF7@24OULEl)dIs-}#6dLXr`Y;9iqoyR}+gYT=Q+-yMVRV}MTk5?6y zlM9uVm5y5l<(RkH;V?$5*IDl^87PPcHfqbMyR*Xu}#96)+R4yA_c)%La)9O zSTsh6%PVF8(lVNJZL`_wMhDRS^wXTvF|P(r*tAt$w~8ugUs1vv$e4VNOK4RM$V9J; zb6H9PX-qht^o<>VU|;)}4hN5&LZvz{tc6P&si;yhmBv6er70~f0@1v{nu*3r0%>!2 z6S7y)%;6_2z?Gb8wH4HYF7R}2cD!nR_?hIwJ}qTRM= zwJ)BL99Day+rDF?Yn|8FCsGuoMZ|hamM`iBIltaALu~{mB`ITTt*xFH7JSz1cCZcJ z#dqA@z>{@{oc~(S`DY92EmCsjm+C-9b2i|FI&nHC8lAP+BQ`7%Tnp#BY=P!V85p5V zWY@FMUOg4V8Id<7VjO-cMWpLv6b?-X1XhxXg!QCMf24R?-AF5fxsO~{@r?vwSP+uB z5@EBlO|_MBhN_Z7gbn!uRSqJRl)b_fBaTVn@^J+ZHUQD#sBhq_owjvr()|PWx_OH# zKH5(wfUC_jT1OTjbwtXb4D`MdZyA}e!Xe^*N)IN zoQ|g+nXkPQnvZ<7Ue^T6rvBsg{`}fZL{VT>=*AM+Ns~_oBZ`GYb|o5P9f~P34mkl1 zVlxPg$_&K`WkSJ(s%z!Jrg<@?C;W-gL6QpHwxN;0WC6#cut~^;o)`-S?Uk-P8Jpkc zSjV_xD>t!{A*}**;^L{1lWn<308B8f>6X5x8-wRB4ajAtN^i*Qin0-rFO{IQbxXUU z{*1CpKB8tbDH@;DmF9T=H0fOX_TrBxJnBl7 ztyKSomOPU&rI`?kv=q*D(_Zv5GXTN(#yY#g{a5De6P)4MsAnKHHBWP;g_lmmOnwf+ zi+7%bCziY~%4p>BvEZoTc!gpPi9$R{qCj?F$WRHE#zOlcOqN-wsIqc=`sS4g@A_|! zl`#U!4NbYwEK=6!#8fj}DHkj>grF8vPS-*4GfB(Ivaovm1UWOeFrCV$Hj)P9@o(dQiaW76m23p$=6V5sAA{q1|#{M}cv`^$&E zJaD?FDV6cuvT4mXj#yo)Hzb4MbKiX2|Mp3c;qn!q-FxH14{3j;C%pV8f9BVb8QUz> z8;0=M`p8q^_KW_u4()Z3{Kcw8NVk{|m~sxY6T2jL*&VRlsModUAD+;EEW%$me($^Q z?Ytd=uWVhm{hz;MNbP3Ff)chxzrMoWVFbR{-%Ip=fjnCl#E&mq|KL|hfI&yl)?w`T zrSt?VXNE3cKx#fG`kNJJhrsmnh zDcq;&)4Pdlj1R}J4&^ga?9#tH3%jP)s_H8em`e~@`Gvn+e#c#$P$2T)>CUr1{>Rn( zAA;_x=Mv>DWstKPV6LBP@A>wVWYIhxrntyRbS+Pg$C#yS)RlBA8=2w%La>(?rzUvx z+mHPKCGxu4@1dWx^j|>dF8ds zZO8iFdUM6L4d}e}OPd>S+1#>gFFgVIoAMcZDp-RSmvPPIAN>>wA8$b46^*&(%xG7% zUw%nUG&D21PqO4sv9>qPJ^PD`ZKvo$&E&GhvTe^ihdWEL@X^10jMfyIPys`m*s_=I zy>sM$K3Hb6X=Jz(ZmfS~;B=RXOXY6ZYIvt;&xa_uYZ_gaMXR-41n%k$#JSSL=YyRi zXagNBA14<1!7(KkbF65>&Br-zXs~~@v$tm5@``FwqYu8{j+h55q$32Kvfg9AfALT6 zU&NR^lWY0NciMWU<_bX@mq;3&TuUAFGdOxt)v6}HisK08$TqZgLV$z?uX;<{`{+uo+dNH`WBdGwIl7`) literal 0 HcmV?d00001 diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt new file mode 100644 index 0000000..1910281 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/fixtures/files/foo.txt @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/fixtures/files/rails.png b/vendor/plugins/attachment_fu/test/fixtures/files/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..b8441f182e06974083cf08f0acaf0e2fd612bd40 GIT binary patch literal 1787 zcmVCLdthj)A!BBmWB&y|X`RY;f`BJ<_ju%@N||NoLFD~mQl$aHGjq>;5dG_D{h(5s}0 z6&=HANU$m__3PuddU(lvR_xWj`}Oho@9EyQt-n!E*P(KhM@X_VFV2l&>deNZJT%y8iwA zoG>u1B`p2=_u9k4v1Mud`1+qvOZoHg#bITJ9U`qBAek?40RR96!AV3xRCwBy*IQ$v zN(=yC9IhRft9V64L`77pqF_Cx@c;kSNoGK)`?Ps*cP(EtGlYZ{D5cxspMQvjKH)Oh6X(pa|J{ zGy1J$Ej7=Z{uvmMfRRsE;v`p;45B~6*ep#hM^ji zl$+7qoWq~}ewG=61uFw0He{tJurMU&4Iv?=B^eR(wAHk!miA)O7p_+YR>lbmU3rmn ze?+ze(+sEd6foB&*l9+?zkr_a-5*v&p*?c}HOGtyHg6r{WFYpQ=#z0Hc7VWLx$>M3|b0|Gn z+5t#z6*ffSVc6DjpmB2?AAR@@vB!wCK?9Yl;33;Q7^%(401QW|k=R8b!OwtLJPjjm zO9Ia;qCq)rOq!1Ia*6#A%#xb}yDx1P*pWla>9j$bnMn3CBqe4`TRll_Iy29kmG?4fbKuF=XqU|?3b@B zA`&a?KIgZ|KJx5eND_c3Em=WZn@xW8hRJ^G&sY^b(FW?WC9W_sb;+lAPdLTdBaKIK;-f}*h4|1aTjw7qX_k~e{TWO7jqcekERN;Jyh%67)q4rKpL*CEYL;|#GY{B@5 zi52XoC?xsoorJKxsliugF#z38MJqrYCWV(t<=G&f;^Me13&AiI9{3jUZ$ zFM`*L(9qc^VMxkz1oaDH!1pcD^IXp>Z0Jb=_qs?Vsrs{mp<^{$N!EC9o+`CO-(o}E zJ`y{*;9s|wr22-QoJ87y^~;)Q@b%P4UgSSsx>2$o@Vd{%Pk0@4qZ^fhB(vt$c1TG> z*{Ad;foraENbld`=MCNm4?9kvlgK~&J>ialpJ7nua zx0oRzwG5;}Qne)Fg(N3kf?JVmB;}y&5(0+~r*aL$0Zof8fe!AtHWH>A^1Y)@G@GsA zup`R{Qg?{+MaxTq#2n{6w|)c&yaJ7{U4ngAH5v6I)*;@rEBE*ehIPBwKBQU)YKE8F0lR!Sm?sE4Xk-sj&E$|A-9n dP56HS1^^A-61FoN)nxzx002ovPDHLkV1kw_Sd9Px literal 0 HcmV?d00001 diff --git a/vendor/plugins/attachment_fu/test/geometry_test.rb b/vendor/plugins/attachment_fu/test/geometry_test.rb new file mode 100644 index 0000000..ade4f48 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/geometry_test.rb @@ -0,0 +1,101 @@ +require 'test/unit' +require File.expand_path(File.join(File.dirname(__FILE__), '../lib/geometry')) unless Object.const_defined?(:Geometry) + +class GeometryTest < Test::Unit::TestCase + def test_should_resize + assert_geometry 50, 64, + "50x50" => [39, 50], + "60x60" => [47, 60], + "100x100" => [78, 100] + end + + def test_should_resize_no_width + assert_geometry 50, 64, + "x50" => [39, 50], + "x60" => [47, 60], + "x100" => [78, 100] + end + + def test_should_resize_no_height + assert_geometry 50, 64, + "50" => [50, 64], + "60" => [60, 77], + "100" => [100, 128] + end + + def test_should_resize_with_percent + assert_geometry 50, 64, + "50x50%" => [25, 32], + "60x60%" => [30, 38], + "120x112%" => [60, 72] + end + + def test_should_resize_with_percent_and_no_width + assert_geometry 50, 64, + "x50%" => [50, 32], + "x60%" => [50, 38], + "x112%" => [50, 72] + end + + def test_should_resize_with_percent_and_no_height + assert_geometry 50, 64, + "50%" => [25, 32], + "60%" => [30, 38], + "120%" => [60, 77] + end + + def test_should_resize_with_less + assert_geometry 50, 64, + "50x50<" => [50, 64], + "60x60<" => [50, 64], + "100x100<" => [78, 100], + "100x112<" => [88, 112], + "40x70<" => [50, 64] + end + + def test_should_resize_with_less_and_no_width + assert_geometry 50, 64, + "x50<" => [50, 64], + "x60<" => [50, 64], + "x100<" => [78, 100] + end + + def test_should_resize_with_less_and_no_height + assert_geometry 50, 64, + "50<" => [50, 64], + "60<" => [60, 77], + "100<" => [100, 128] + end + + def test_should_resize_with_greater + assert_geometry 50, 64, + "50x50>" => [39, 50], + "60x60>" => [47, 60], + "100x100>" => [50, 64], + "100x112>" => [50, 64], + "40x70>" => [40, 51] + end + + def test_should_resize_with_greater_and_no_width + assert_geometry 50, 64, + "x40>" => [31, 40], + "x60>" => [47, 60], + "x100>" => [50, 64] + end + + def test_should_resize_with_greater_and_no_height + assert_geometry 50, 64, + "40>" => [40, 51], + "60>" => [50, 64], + "100>" => [50, 64] + end + + protected + def assert_geometry(width, height, values) + values.each do |geo, result| + # run twice to verify the Geometry string isn't modified after a run + geo = Geometry.from_s(geo) + 2.times { assert_equal result, [width, height] / geo } + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/processors/image_science_test.rb b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb new file mode 100644 index 0000000..636918d --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/image_science_test.rb @@ -0,0 +1,31 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class ImageScienceTest < Test::Unit::TestCase + attachment_model ImageScienceAttachment + + if Object.const_defined?(:ImageScience) + def test_should_resize_image + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + # test image science thumbnail + assert_equal 42, attachment.width + assert_equal 55, attachment.height + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + # test exact resize dimensions + assert_equal 50, thumb.width + assert_equal 51, thumb.height + + # test geometry string + assert_equal 31, geo.width + assert_equal 41, geo.height + end + else + def test_flunk + puts "ImageScience not loaded, tests not running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb new file mode 100644 index 0000000..244a4a2 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/mini_magick_test.rb @@ -0,0 +1,31 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class MiniMagickTest < Test::Unit::TestCase + attachment_model MiniMagickAttachment + + if Object.const_defined?(:MiniMagick) + def test_should_resize_image + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert attachment.image? + # test MiniMagick thumbnail + assert_equal 43, attachment.width + assert_equal 55, attachment.height + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + # test exact resize dimensions + assert_equal 50, thumb.width + assert_equal 51, thumb.height + + # test geometry string + assert_equal 31, geo.width + assert_equal 40, geo.height + end + else + def test_flunk + puts "MiniMagick not loaded, tests not running" + end + end +end diff --git a/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb new file mode 100644 index 0000000..af91193 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/processors/rmagick_test.rb @@ -0,0 +1,240 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper')) + +class RmagickTest < Test::Unit::TestCase + attachment_model Attachment + + if Object.const_defined?(:Magick) + def test_should_create_image_from_uploaded_file + assert_created do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_equal 50, attachment.width + assert_equal 64, attachment.height + assert_equal '50x64', attachment.image_size + end + end + + def test_should_create_image_from_uploaded_file_with_custom_content_type + assert_created do + attachment = upload_file :content_type => 'foo/bar', :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.image? + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_nil attachment.width + assert_nil attachment.height + assert_equal [], attachment.thumbnails + end + end + + def test_should_create_thumbnail + attachment = upload_file :filename => '/files/rails.png' + + assert_created do + basename, ext = attachment.filename.split '.' + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 50, 50) + assert_valid thumbnail + assert !thumbnail.size.zero? + #assert_in_delta 4673, thumbnail.size, 2 + assert_equal 50, thumbnail.width + assert_equal 50, thumbnail.height + assert_equal [thumbnail.id], attachment.thumbnails.collect(&:id) + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id) + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename + end + end + + def test_should_create_thumbnail_with_geometry_string + attachment = upload_file :filename => '/files/rails.png' + + assert_created do + basename, ext = attachment.filename.split '.' + thumbnail = attachment.create_or_update_thumbnail(attachment.create_temp_file, 'thumb', 'x50') + assert_valid thumbnail + assert !thumbnail.size.zero? + #assert_equal 3915, thumbnail.size + assert_equal 39, thumbnail.width + assert_equal 50, thumbnail.height + assert_equal [thumbnail], attachment.thumbnails + assert_equal attachment.id, thumbnail.parent_id if thumbnail.respond_to?(:parent_id) + assert_equal "#{basename}_thumb.#{ext}", thumbnail.filename + end + end + + def test_should_resize_image(klass = ImageAttachment) + attachment_model klass + assert_equal [50, 50], attachment_model.attachment_options[:resize_to] + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_in_delta 4673, attachment.size, 2 + assert_equal 50, attachment.width + assert_equal 50, attachment.height + end + + test_against_subclass :test_should_resize_image, ImageAttachment + + def test_should_resize_image_with_geometry(klass = ImageOrPdfAttachment) + attachment_model klass + assert_equal 'x50', attachment_model.attachment_options[:resize_to] + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file) + assert attachment.image? + assert !attachment.size.zero? + #assert_equal 3915, attachment.size + assert_equal 39, attachment.width + assert_equal 50, attachment.height + end + + test_against_subclass :test_should_resize_image_with_geometry, ImageOrPdfAttachment + + def test_should_give_correct_thumbnail_filenames(klass = ImageWithThumbsFileAttachment) + attachment_model klass + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + + [attachment, thumb, geo].each { |record| assert_valid record } + + assert_match /rails\.png$/, attachment.full_filename + assert_match /rails_geometry\.png$/, attachment.full_filename(:geometry) + assert_match /rails_thumb\.png$/, attachment.full_filename(:thumb) + end + end + + test_against_subclass :test_should_give_correct_thumbnail_filenames, ImageWithThumbsFileAttachment + + def test_should_automatically_create_thumbnails(klass = ImageWithThumbsAttachment) + attachment_model klass + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + assert !attachment.size.zero? + #assert_equal 1784, attachment.size + assert_equal 55, attachment.width + assert_equal 55, attachment.height + assert_equal 2, attachment.thumbnails.length + assert_equal 1.0, attachment.aspect_ratio + + thumb = attachment.thumbnails.detect { |t| t.filename =~ /_thumb/ } + assert !thumb.new_record?, thumb.errors.full_messages.join("\n") + assert !thumb.size.zero? + #assert_in_delta 4673, thumb.size, 2 + assert_equal 50, thumb.width + assert_equal 50, thumb.height + assert_equal 1.0, thumb.aspect_ratio + + geo = attachment.thumbnails.detect { |t| t.filename =~ /_geometry/ } + assert !geo.new_record?, geo.errors.full_messages.join("\n") + assert !geo.size.zero? + #assert_equal 3915, geo.size + assert_equal 50, geo.width + assert_equal 50, geo.height + assert_equal 1.0, geo.aspect_ratio + end + end + + test_against_subclass :test_should_automatically_create_thumbnails, ImageWithThumbsAttachment + + # same as above method, but test it on a file model + test_against_class :test_should_automatically_create_thumbnails, ImageWithThumbsFileAttachment + test_against_subclass :test_should_automatically_create_thumbnails_on_class, ImageWithThumbsFileAttachment + + def test_should_use_thumbnail_subclass(klass = ImageWithThumbsClassFileAttachment) + attachment_model klass + attachment = nil + assert_difference ImageThumbnail, :count do + attachment = upload_file :filename => '/files/rails.png' + assert_valid attachment + end + assert_kind_of ImageThumbnail, attachment.thumbnails.first + assert_equal attachment.id, attachment.thumbnails.first.parent.id + assert_kind_of FileAttachment, attachment.thumbnails.first.parent + assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename + assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail), + "#full_filename does not use thumbnail class' path." + end + + test_against_subclass :test_should_use_thumbnail_subclass, ImageWithThumbsClassFileAttachment + + def test_should_remove_old_thumbnail_files_when_updating(klass = ImageWithThumbsFileAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + end + + old_filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename) + + assert_not_created do + use_temp_file "files/rails.png" do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save + new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename } + new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" } + old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" } + end + end + end + + test_against_subclass :test_should_remove_old_thumbnail_files_when_updating, ImageWithThumbsFileAttachment + + def test_should_delete_file_when_in_file_system_when_attachment_record_destroyed(klass = ImageWithThumbsFileAttachment) + attachment_model klass + attachment = upload_file :filename => '/files/rails.png' + filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename) + filenames.each { |f| assert File.exists?(f), "#{f} never existed to delete on destroy" } + attachment.destroy + filenames.each { |f| assert !File.exists?(f), "#{f} still exists" } + end + + test_against_subclass :test_should_delete_file_when_in_file_system_when_attachment_record_destroyed, ImageWithThumbsFileAttachment + + def test_should_overwrite_old_thumbnail_records_when_updating(klass = ImageWithThumbsAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :filename => '/files/rails.png' + end + assert_not_created do # no new db_file records + use_temp_file "files/rails.png" do |file| + attachment.filename = 'rails2.png' + attachment.temp_path = File.join(fixture_path, file) + attachment.save! + end + end + end + + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_updating, ImageWithThumbsAttachment + + def test_should_overwrite_old_thumbnail_records_when_renaming(klass = ImageWithThumbsAttachment) + attachment_model klass + attachment = nil + assert_created 3 do + attachment = upload_file :class => klass, :filename => '/files/rails.png' + end + assert_not_created do # no new db_file records + attachment.filename = 'rails2.png' + attachment.save + assert !attachment.reload.size.zero? + assert_equal 'rails2.png', attachment.filename + end + end + + test_against_subclass :test_should_overwrite_old_thumbnail_records_when_renaming, ImageWithThumbsAttachment + else + def test_flunk + puts "RMagick not installed, no tests running" + end + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/schema.rb b/vendor/plugins/attachment_fu/test/schema.rb new file mode 100644 index 0000000..b2e284d --- /dev/null +++ b/vendor/plugins/attachment_fu/test/schema.rb @@ -0,0 +1,86 @@ +ActiveRecord::Schema.define(:version => 0) do + create_table :attachments, :force => true do |t| + t.column :db_file_id, :integer + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :aspect_ratio, :float + end + + create_table :file_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + t.column :aspect_ratio, :float + end + + create_table :image_science_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :mini_magick_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :mini_magick_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + end + + create_table :orphan_attachments, :force => true do |t| + t.column :db_file_id, :integer + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + end + + create_table :minimal_attachments, :force => true do |t| + t.column :size, :integer + t.column :content_type, :string, :limit => 255 + end + + create_table :db_files, :force => true do |t| + t.column :data, :binary + end + + create_table :s3_attachments, :force => true do |t| + t.column :parent_id, :integer + t.column :thumbnail, :string + t.column :filename, :string, :limit => 255 + t.column :content_type, :string, :limit => 255 + t.column :size, :integer + t.column :width, :integer + t.column :height, :integer + t.column :type, :string + t.column :aspect_ratio, :float + end +end \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/test_helper.rb b/vendor/plugins/attachment_fu/test/test_helper.rb new file mode 100644 index 0000000..66a0b72 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/test_helper.rb @@ -0,0 +1,142 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +ENV['RAILS_ENV'] = 'test' + +require 'test/unit' +require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb')) +require 'breakpoint' +require 'active_record/fixtures' +require 'action_controller/test_process' + +config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml')) +ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log") + +db_adapter = ENV['DB'] + +# no db passed, try one of these fine config-free DBs before bombing. +db_adapter ||= + begin + require 'rubygems' + require 'sqlite' + 'sqlite' + rescue MissingSourceFile + begin + require 'sqlite3' + 'sqlite3' + rescue MissingSourceFile + end + end + +if db_adapter.nil? + raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3." +end + +ActiveRecord::Base.establish_connection(config[db_adapter]) + +load(File.dirname(__FILE__) + "/schema.rb") + +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures" +$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path) + +class Test::Unit::TestCase #:nodoc: + include ActionController::TestProcess + def create_fixtures(*table_names) + if block_given? + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield } + else + Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) + end + end + + def setup + Attachment.saves = 0 + DbFile.transaction { [Attachment, FileAttachment, OrphanAttachment, MinimalAttachment, DbFile].each { |klass| klass.delete_all } } + attachment_model self.class.attachment_model + end + + def teardown + FileUtils.rm_rf File.join(File.dirname(__FILE__), 'files') + end + + self.use_transactional_fixtures = true + self.use_instantiated_fixtures = false + + def self.attachment_model(klass = nil) + @attachment_model = klass if klass + @attachment_model + end + + def self.test_against_class(test_method, klass, subclass = false) + define_method("#{test_method}_on_#{:sub if subclass}class") do + klass = Class.new(klass) if subclass + attachment_model klass + send test_method, klass + end + end + + def self.test_against_subclass(test_method, klass) + test_against_class test_method, klass, true + end + + protected + def upload_file(options = {}) + use_temp_file options[:filename] do |file| + att = attachment_model.create :uploaded_data => fixture_file_upload(file, options[:content_type] || 'image/png') + att.reload unless att.new_record? + return att + end + end + + def use_temp_file(fixture_filename) + temp_path = File.join('/tmp', File.basename(fixture_filename)) + FileUtils.mkdir_p File.join(fixture_path, 'tmp') + FileUtils.cp File.join(fixture_path, fixture_filename), File.join(fixture_path, temp_path) + yield temp_path + ensure + FileUtils.rm_rf File.join(fixture_path, 'tmp') + end + + def assert_created(num = 1) + assert_difference attachment_model.base_class, :count, num do + if attachment_model.included_modules.include? DbFile + assert_difference DbFile, :count, num do + yield + end + else + yield + end + end + end + + def assert_not_created + assert_created(0) { yield } + end + + def should_reject_by_size_with(klass) + attachment_model klass + assert_not_created do + attachment = upload_file :filename => '/files/rails.png' + assert attachment.new_record? + assert attachment.errors.on(:size) + assert_nil attachment.db_file if attachment.respond_to?(:db_file) + end + end + + def assert_difference(object, method = nil, difference = 1) + initial_value = object.send(method) + yield + assert_equal initial_value + difference, object.send(method) + end + + def assert_no_difference(object, method, &block) + assert_difference object, method, 0, &block + end + + def attachment_model(klass = nil) + @attachment_model = klass if klass + @attachment_model + end +end + +require File.join(File.dirname(__FILE__), 'fixtures/attachment') +require File.join(File.dirname(__FILE__), 'base_attachment_tests') \ No newline at end of file diff --git a/vendor/plugins/attachment_fu/test/validation_test.rb b/vendor/plugins/attachment_fu/test/validation_test.rb new file mode 100644 index 0000000..a14cf99 --- /dev/null +++ b/vendor/plugins/attachment_fu/test/validation_test.rb @@ -0,0 +1,55 @@ +require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) + +class ValidationTest < Test::Unit::TestCase + def test_should_invalidate_big_files + @attachment = SmallAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:size) + + @attachment.size = 2000 + assert !@attachment.valid? + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence + + @attachment.size = 1000 + assert !@attachment.valid? + assert_nil @attachment.errors.on(:size) + end + + def test_should_invalidate_small_files + @attachment = BigAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:size) + + @attachment.size = 2000 + assert !@attachment.valid? + assert @attachment.errors.on(:size), @attachment.errors.full_messages.to_sentence + + @attachment.size = 1.megabyte + assert !@attachment.valid? + assert_nil @attachment.errors.on(:size) + end + + def test_should_validate_content_type + @attachment = PdfAttachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:content_type) + + @attachment.content_type = 'foo' + assert !@attachment.valid? + assert @attachment.errors.on(:content_type) + + @attachment.content_type = 'pdf' + assert !@attachment.valid? + assert_nil @attachment.errors.on(:content_type) + end + + def test_should_require_filename + @attachment = Attachment.new + assert !@attachment.valid? + assert @attachment.errors.on(:filename) + + @attachment.filename = 'foo' + assert !@attachment.valid? + assert_nil @attachment.errors.on(:filename) + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/CHANGELOG b/vendor/plugins/liquid/CHANGELOG new file mode 100644 index 0000000..b1bb18a --- /dev/null +++ b/vendor/plugins/liquid/CHANGELOG @@ -0,0 +1,38 @@ +Changelog + +Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. +To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods. + +Added more tags to standard library + +Added include tag ( like partials in rails ) + +[...] Gazillion of detail improvements + +Added strainers as filter hosts for better security [Tobias Luetke] + +Fixed that rails integration would call filter with the wrong "self" [Michael Geary] + +Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the +filter which was obviously misleading [Tobias Luetke] + +Removed count helper from standard lib. use size [Tobias Luetke] + +Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond] + +Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond] + {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} + + +Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] + + class ProductDrop < Liquid::Drop + def top_sales + Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) + end + end + t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' ) + t.render('product' => ProductDrop.new ) + + +Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond] diff --git a/vendor/plugins/liquid/MIT-LICENSE b/vendor/plugins/liquid/MIT-LICENSE new file mode 100644 index 0000000..441ca02 --- /dev/null +++ b/vendor/plugins/liquid/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2005, 2006 Tobias Luetke + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/plugins/liquid/Manifest.txt b/vendor/plugins/liquid/Manifest.txt new file mode 100644 index 0000000..2941b97 --- /dev/null +++ b/vendor/plugins/liquid/Manifest.txt @@ -0,0 +1,60 @@ +CHANGELOG +MIT-LICENSE +Manifest.txt +README +Rakefile +example/server/example_servlet.rb +example/server/liquid_servlet.rb +example/server/server.rb +example/server/templates/index.liquid +example/server/templates/products.liquid +init.rb +lib/extras/liquid_view.rb +lib/liquid.rb +lib/liquid/block.rb +lib/liquid/condition.rb +lib/liquid/context.rb +lib/liquid/document.rb +lib/liquid/drop.rb +lib/liquid/errors.rb +lib/liquid/extensions.rb +lib/liquid/file_system.rb +lib/liquid/htmltags.rb +lib/liquid/standardfilters.rb +lib/liquid/strainer.rb +lib/liquid/tag.rb +lib/liquid/tags/assign.rb +lib/liquid/tags/capture.rb +lib/liquid/tags/case.rb +lib/liquid/tags/comment.rb +lib/liquid/tags/cycle.rb +lib/liquid/tags/for.rb +lib/liquid/tags/if.rb +lib/liquid/tags/ifchanged.rb +lib/liquid/tags/include.rb +lib/liquid/tags/unless.rb +lib/liquid/template.rb +lib/liquid/variable.rb +test/block_test.rb +test/context_test.rb +test/drop_test.rb +test/error_handling_test.rb +test/extra/breakpoint.rb +test/extra/caller.rb +test/file_system_test.rb +test/filter_test.rb +test/helper.rb +test/html_tag_test.rb +test/if_else_test.rb +test/include_tag_test.rb +test/output_test.rb +test/parsing_quirks_test.rb +test/regexp_test.rb +test/security_test.rb +test/standard_filter_test.rb +test/standard_tag_test.rb +test/statements_test.rb +test/strainer_test.rb +test/template_test.rb +test/unless_else_test.rb +test/variable_test.rb diff --git a/vendor/plugins/liquid/README b/vendor/plugins/liquid/README new file mode 100644 index 0000000..1d019af --- /dev/null +++ b/vendor/plugins/liquid/README @@ -0,0 +1,38 @@ += Liquid template engine + +Liquid is a template engine which I wrote for very specific requirements + +* It has to have beautiful and simple markup. + Template engines which don't produce good looking markup are no fun to use. +* It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. +* It has to be stateless. Compile and render steps have to be seperate so that the expensive parsing and compiling can be done once and later on you can + just render it passing in a hash with local variables and objects. + +== Why should i use Liquid + +* You want to allow your users to edit the appearance of your application but don't want them to run insecure code on your server. +* You want to render templates directly from the database +* You like smarty style template engines +* You need a template engine which does HTML just as well as Emails +* You don't like the markup of your current one + +== What does it look like? + +

      + {% for product in products %} +
    • +

      {{product.name}}

      + Only {{product.price | price }} + + {{product.description | prettyprint | paragraph }} +
    • + {% endfor %} +
    + +== Howto use Liquid + +Liquid supports a very simple API based around the Liquid::Template class. +For standard use you can just pass it the content of a file and call render with a parameters hash. + + @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template + @template.render( 'name' => 'tobi' ) # => "hi tobi" \ No newline at end of file diff --git a/vendor/plugins/liquid/Rakefile b/vendor/plugins/liquid/Rakefile new file mode 100755 index 0000000..1c6df69 --- /dev/null +++ b/vendor/plugins/liquid/Rakefile @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +require 'rubygems' +require 'rake' +require 'hoe' + +PKG_VERSION = "1.7.0" +PKG_NAME = "liquid" +PKG_DESC = "A secure non evaling end user template engine with aesthetic markup." + +Rake::TestTask.new(:test) do |t| + t.libs << "lib" + t.libs << "test" + t.pattern = 'test/*_test.rb' + t.verbose = false +end + +Hoe.new(PKG_NAME, PKG_VERSION) do |p| + p.rubyforge_name = PKG_NAME + p.summary = PKG_DESC + p.description = nil + p.author = "Tobias Luetke" + p.email = "tobi@leetsoft.com" + p.url = "http://home.leetsoft.com/liquid" +end \ No newline at end of file diff --git a/vendor/plugins/liquid/example/server/example_servlet.rb b/vendor/plugins/liquid/example/server/example_servlet.rb new file mode 100644 index 0000000..18e528e --- /dev/null +++ b/vendor/plugins/liquid/example/server/example_servlet.rb @@ -0,0 +1,37 @@ +module ProductsFilter + def price(integer) + sprintf("$%.2d USD", integer / 100.0) + end + + def prettyprint(text) + text.gsub( /\*(.*)\*/, '\1' ) + end + + def count(array) + array.size + end + + def paragraph(p) + "

    #{p}

    " + end +end + +class Servlet < LiquidServlet + + def index + { 'date' => Time.now } + end + + def products + { 'products' => products_list, 'section' => 'Snowboards', 'cool_products' => true} + end + + private + + def products_list + [{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, + {'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'}, + {'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}] + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/example/server/liquid_servlet.rb b/vendor/plugins/liquid/example/server/liquid_servlet.rb new file mode 100644 index 0000000..8f24f00 --- /dev/null +++ b/vendor/plugins/liquid/example/server/liquid_servlet.rb @@ -0,0 +1,28 @@ +class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet + + def do_GET(req, res) + handle(:get, req, res) + end + + def do_POST(req, res) + handle(:post, req, res) + end + + private + + def handle(type, req, res) + @request, @response = req, res + + @request.path_info =~ /(\w+)$/ + @action = $1 || 'index' + @assigns = send(@action) if respond_to?(@action) + + @response['Content-Type'] = "text/html" + @response.status = 200 + @response.body = Liquid::Template.parse(read_template).render(@assigns, :filters => [ProductsFilter]) + end + + def read_template(filename = @action) + File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" ) + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/example/server/server.rb b/vendor/plugins/liquid/example/server/server.rb new file mode 100644 index 0000000..6d71c72 --- /dev/null +++ b/vendor/plugins/liquid/example/server/server.rb @@ -0,0 +1,12 @@ +require 'webrick' +require 'rexml/document' + +require File.dirname(__FILE__) + '/../../lib/liquid' +require File.dirname(__FILE__) + '/liquid_servlet' +require File.dirname(__FILE__) + '/example_servlet' + +# Setup webrick +server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 ) +server.mount('/', Servlet) +trap("INT"){ server.shutdown } +server.start \ No newline at end of file diff --git a/vendor/plugins/liquid/example/server/templates/index.liquid b/vendor/plugins/liquid/example/server/templates/index.liquid new file mode 100644 index 0000000..79a52b4 --- /dev/null +++ b/vendor/plugins/liquid/example/server/templates/index.liquid @@ -0,0 +1,6 @@ +

    Hello world!

    + +

    It is {{date}}

    + + +

    Check out the Products screen

    \ No newline at end of file diff --git a/vendor/plugins/liquid/example/server/templates/products.liquid b/vendor/plugins/liquid/example/server/templates/products.liquid new file mode 100644 index 0000000..05af4f7 --- /dev/null +++ b/vendor/plugins/liquid/example/server/templates/products.liquid @@ -0,0 +1,45 @@ + + + + + + + + products + + + + + + + + + + +

    There are currently {{products | count}} products in the {{section}} catalog

    + + {% if cool_products %} + Cool products :) + {% else %} + Uncool products :( + {% endif %} + +
      + + {% for product in products %} +
    • +

      {{product.name}}

      + Only {{product.price | price }} + + {{product.description | prettyprint | paragraph }} + + {{ 'it rocks!' | paragraph }} + +
    • + {% endfor %} + +
    + + + diff --git a/vendor/plugins/liquid/init.rb b/vendor/plugins/liquid/init.rb new file mode 100644 index 0000000..4d80f13 --- /dev/null +++ b/vendor/plugins/liquid/init.rb @@ -0,0 +1,6 @@ +require 'liquid' +require 'extras/liquid_view' + +ActionView::Base::register_template_handler :liquid, LiquidView + + diff --git a/vendor/plugins/liquid/lib/extras/liquid_view.rb b/vendor/plugins/liquid/lib/extras/liquid_view.rb new file mode 100644 index 0000000..f26d72a --- /dev/null +++ b/vendor/plugins/liquid/lib/extras/liquid_view.rb @@ -0,0 +1,27 @@ +# LiquidView is a action view extension class. You can register it with rails +# and use liquid as an template system for .liquid files +# +# Example +# +# ActionView::Base::register_template_handler :liquid, LiquidView +class LiquidView + + def initialize(action_view) + @action_view = action_view + end + + + def render(template, local_assigns) + @action_view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8' + assigns = @action_view.assigns.dup + + if content_for_layout = @action_view.instance_variable_get("@content_for_layout") + assigns['content_for_layout'] = content_for_layout + end + assigns.merge!(local_assigns) + + liquid = Liquid::Template.parse(template) + liquid.render(assigns, :filters => [@action_view.controller.master_helper_module], :registers => {:action_view => @action_view, :controller => @action_view.controller}) + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid.rb b/vendor/plugins/liquid/lib/liquid.rb new file mode 100644 index 0000000..54dc51d --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid.rb @@ -0,0 +1,66 @@ +# Copyright (c) 2005 Tobias Luetke +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +module Liquid + FilterSperator = /\|/ + ArgumentSeparator = ',' + FilterArgumentSeparator = ':' + VariableAttributeSeparator = '.' + TagStart = /\{\%/ + TagEnd = /\%\}/ + VariableSignature = /\(?[\w\-\.\[\]]\)?/ + VariableSegment = /[\w\-]\??/ + VariableStart = /\{\{/ + VariableEnd = /\}\}/ + QuotedFragment = /"[^"]+"|'[^']+'|[^\s,|]+/ + TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/ + TemplateParser = /(#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableEnd})/ + VariableParser = /\[[^\]]+\]|#{VariableSegment}+/ +end + +require 'liquid/drop' +require 'liquid/extensions' +require 'liquid/errors' +require 'liquid/strainer' +require 'liquid/context' +require 'liquid/tag' +require 'liquid/block' +require 'liquid/document' +require 'liquid/variable' +require 'liquid/file_system' +require 'liquid/template' +require 'liquid/htmltags' +require 'liquid/standardfilters' +require 'liquid/condition' + +# Load all the tags of the standard library +# +Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f } + + + + + + + + diff --git a/vendor/plugins/liquid/lib/liquid/block.rb b/vendor/plugins/liquid/lib/liquid/block.rb new file mode 100644 index 0000000..13cc818 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/block.rb @@ -0,0 +1,102 @@ +module Liquid + + class Block < Tag + + def parse(tokens) + @nodelist ||= [] + @nodelist.clear + + while token = tokens.shift + + case token + when /^#{TagStart}/ + if token =~ /^#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/ + + # if we found the proper block delimitor just end parsing here and let the outer block + # proceed + if block_delimiter == $1 + end_tag + return + end + + # fetch the tag from registered blocks + if tag = Template.tags[$1] + @nodelist << tag.new($1, $2, tokens) + else + # this tag is not registered with the system + # pass it to the current block for special handling or error reporting + unknown_tag($1, $2, tokens) + end + else + raise SyntaxError, "Tag '#{token}' was not properly terminated with regexp: #{TagEnd.inspect} " + end + when /^#{VariableStart}/ + @nodelist << create_variable(token) + when '' + # pass + else + @nodelist << token + end + end + + # Make sure that its ok to end parsing in the current block. + # Effectively this method will throw and exception unless the current block is + # of type Document + assert_missing_delimitation! + end + + def end_tag + end + + def unknown_tag(tag, params, tokens) + case tag + when 'else' + raise SyntaxError, "#{block_name} tag does not expect else tag" + when 'end' + raise SyntaxError, "'end' is not a valid delimiter for #{block_name} tags. use #{block_delimiter}" + else + raise SyntaxError, "Unknown tag '#{tag}'" + end + end + + def block_delimiter + "end#{block_name}" + end + + def block_name + @tag_name + end + + def create_variable(token) + token.scan(/^#{VariableStart}(.*)#{VariableEnd}$/) do |content| + return Variable.new(content.first) + end + raise SyntaxError.new("Variable '#{token}' was not properly terminated with regexp: #{VariableEnd.inspect} ") + end + + def render(context) + render_all(@nodelist, context) + end + + protected + + def assert_missing_delimitation! + raise SyntaxError.new("#{block_name} tag was never closed") + end + + def render_all(list, context) + list.collect do |token| + begin + if token.respond_to?(:render) + token.render(context) + else + token.to_s + end + rescue Exception => e + context.handle_error(e) + end + + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/condition.rb b/vendor/plugins/liquid/lib/liquid/condition.rb new file mode 100644 index 0000000..4746aca --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/condition.rb @@ -0,0 +1,99 @@ +module Liquid + # Container for liquid nodes which conveniently wraps decision making logic + # + # Example: + # + # c = Condition.new('1', '==', '1') + # c.evaluate #=> true + # + class Condition #:nodoc: + @@operators = { + '==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) }, + '!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, + '<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) }, + '<' => :<, + '>' => :>, + '>=' => :>=, + '<=' => :<=, + 'contains' => lambda { |cond, left, right| left.include?(right) }, + } + + def self.operators + @@operators + end + + attr_reader :attachment + attr_accessor :left, :operator, :right + + def initialize(left = nil, operator = nil, right = nil) + @left, @operator, @right = left, operator, right + end + + def evaluate(context = Context.new) + interpret_condition(left, right, operator, context) + end + + def attach(attachment) + @attachment = attachment + end + + def else? + false + end + + private + + def equal_variables(left, right) + if left.is_a?(Symbol) + if right.respond_to?(left) + return right.send(left.to_s) + else + return nil + end + end + + if right.is_a?(Symbol) + if left.respond_to?(right) + return left.send(right.to_s) + else + return nil + end + end + + left == right + end + + def interpret_condition(left, right, op, context) + + # If the operator is empty this means that the decision statement is just + # a single variable. We can just poll this variable from the context and + # return this as the result. + return context[left] if op == nil + + left, right = context[left], context[right] + + + operation = self.class.operators[op] || raise(ArgumentError.new("Error in tag '#{name}' - Unknown operator #{op}")) + + if operation.respond_to?(:call) + operation.call(self, left, right) + elsif left.respond_to?(operation) and right.respond_to?(operation) + left.send(operation, right) + else + nil + end + end + end + + class ElseCondition < Condition + + def else? + true + end + + def evaluate(context) + true + end + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/context.rb b/vendor/plugins/liquid/lib/liquid/context.rb new file mode 100644 index 0000000..2a42f12 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/context.rb @@ -0,0 +1,243 @@ +module Liquid + + class ContextError < StandardError + end + + # Context keeps the variable stack and resolves variables, as well as keywords + # + # context['variable'] = 'testing' + # context['variable'] #=> 'testing' + # context['true'] #=> true + # context['10.2232'] #=> 10.2232 + # + # context.stack do + # context['bob'] = 'bobsen' + # end + # + # context['bob'] #=> nil class Context + class Context + attr_reader :scopes + attr_reader :errors, :registers + + def initialize(assigns = {}, registers = {}, rethrow_errors = false) + @scopes = [(assigns || {})] + @registers = registers + @errors = [] + @rethrow_errors = rethrow_errors + end + + def strainer + @strainer ||= Strainer.create(self) + end + + # adds filters to this context. + # this does not register the filters with the main Template object. see Template.register_filter + # for that + def add_filters(filters) + filters = [filters].flatten.compact + + filters.each do |f| + raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) + strainer.extend(f) + end + end + + def handle_error(e) + errors.push(e) + raise if @rethrow_errors + + case e + when SyntaxError then "Liquid syntax error: #{e.message}" + else "Liquid error: #{e.message}" + end + end + + + def invoke(method, *args) + if strainer.respond_to?(method) + strainer.__send__(method, *args) + else + args.first + end + end + + # push new local scope on the stack. use Context#stack instead + def push + @scopes.unshift({}) + end + + # merge a hash of variables in the current local scope + def merge(new_scopes) + @scopes[0].merge!(new_scopes) + end + + # pop from the stack. use Context#stack instead + def pop + raise ContextError if @scopes.size == 1 + @scopes.shift + end + + # pushes a new local scope on the stack, pops it at the end of the block + # + # Example: + # + # context.stack do + # context['var'] = 'hi' + # end + # context['var] #=> nil + # + def stack(&block) + result = nil + push + begin + result = yield + ensure + pop + end + result + end + + # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop + def []=(key, value) + @scopes[0][key] = value + end + + def [](key) + resolve(key) + end + + def has_key?(key) + resolve(key) != nil + end + + private + + # Look up variable, either resolve directly after considering the name. We can directly handle + # Strings, digits, floats and booleans (true,false). If no match is made we lookup the variable in the current scope and + # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree. + # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions + # + # Example: + # + # products == empty #=> products.empty? + # + def resolve(key) + case key + when nil, 'nil', 'null', '' + nil + when 'true' + true + when 'false' + false + when 'empty' + :empty? + # Single quoted strings + when /^'(.*)'$/ + $1.to_s + # Double quoted strings + when /^"(.*)"$/ + $1.to_s + # Integer and floats + when /^(\d+)$/ + $1.to_i + # Ranges + when /^\((\S+)\.\.(\S+)\)$/ + (resolve($1).to_i..resolve($2).to_i) + # Floats + when /^(\d[\d\.]+)$/ + $1.to_f + else + variable(key) + end + end + + # fetches an object starting at the local scope and then moving up + # the hierachy + def find_variable(key) + @scopes.each do |scope| + if scope.has_key?(key) + variable = scope[key] + variable = scope[key] = variable.call(self) if variable.is_a?(Proc) + variable = variable.to_liquid + variable.context = self if variable.respond_to?(:context=) + return variable + end + end + nil + end + + # resolves namespaced queries gracefully. + # + # Example + # + # @context['hash'] = {"name" => 'tobi'} + # assert_equal 'tobi', @context['hash.name'] + # assert_equal 'tobi', @context['hash[name]'] + # + def variable(markup) + parts = markup.scan(VariableParser) + square_bracketed = /^\[(.*)\]$/ + + first_part = parts.shift + if first_part =~ square_bracketed + first_part = resolve($1) + end + + if object = find_variable(first_part) + + parts.each do |part| + + # If object is a hash we look for the presence of the key and if its available + # we return it + + if part =~ square_bracketed + part = resolve($1) + + object[pos] = object[part].call(self) if object[part].is_a?(Proc) and object.respond_to?(:[]=) + object = object[part].to_liquid + + else + + # Hash + if object.respond_to?(:has_key?) and object.has_key?(part) + + # if its a proc we will replace the entry in the hash table with the proc + res = object[part] + res = object[part] = res.call(self) if res.is_a?(Proc) and object.respond_to?(:[]=) + object = res.to_liquid + + # Array + elsif object.respond_to?(:fetch) and part =~ /^\d+$/ + pos = part.to_i + + object[pos] = object[pos].call(self) if object[pos].is_a?(Proc) and object.respond_to?(:[]=) + object = object[pos].to_liquid + + # Some special cases. If no key with the same name was found we interpret following calls + # as commands and call them on the current object + elsif object.respond_to?(part) and ['size', 'first', 'last'].include?(part) + + object = object.send(part.intern).to_liquid + + # No key was present with the desired value and it wasn't one of the directly supported + # keywords either. The only thing we got left is to return nil + else + return nil + end + end + + # If we are dealing with a drop here we have to + object.context = self if object.respond_to?(:context=) + end + end + + object + end + + private + + def execute_proc(proc) + proc.call(self) + end + end +end diff --git a/vendor/plugins/liquid/lib/liquid/document.rb b/vendor/plugins/liquid/lib/liquid/document.rb new file mode 100644 index 0000000..abffbde --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/document.rb @@ -0,0 +1,17 @@ +module Liquid + class Document < Block + # we don't need markup to open this block + def initialize(tokens) + parse(tokens) + end + + # There isn't a real delimter + def block_delimiter + [] + end + + # Document blocks don't need to be terminated since they are not actually opened + def assert_missing_delimitation! + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/drop.rb b/vendor/plugins/liquid/lib/liquid/drop.rb new file mode 100644 index 0000000..7a2719e --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/drop.rb @@ -0,0 +1,48 @@ +module Liquid + + # A drop in liquid is a class which allows you to to export DOM like things to liquid + # Methods of drops are callable. + # The main use for liquid drops is the implement lazy loaded objects. + # If you would like to make data available to the web designers which you don't want loaded unless needed then + # a drop is a great way to do that + # + # Example: + # + # class ProductDrop < Liquid::Drop + # def top_sales + # Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) + # end + # end + # + # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) + # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. + # + # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a + # catch all + class Drop + attr_writer :context + + # Catch all for the method + def before_method(method) + nil + end + + # called by liquid to invoke a drop + def invoke_drop(method) + result = before_method(method) + result ||= send(method.to_sym) if self.class.public_instance_methods.include?(method.to_s) + result + end + + def has_key?(name) + true + end + + def to_liquid + self + end + + alias :[] :invoke_drop + end + +end diff --git a/vendor/plugins/liquid/lib/liquid/errors.rb b/vendor/plugins/liquid/lib/liquid/errors.rb new file mode 100644 index 0000000..4e493b8 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/errors.rb @@ -0,0 +1,7 @@ +module Liquid + class FilterNotFound < StandardError + end + + class FileSystemError < StandardError + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/extensions.rb b/vendor/plugins/liquid/lib/liquid/extensions.rb new file mode 100644 index 0000000..2752ba3 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/extensions.rb @@ -0,0 +1,56 @@ +require 'time' +require 'date' + +class String # :nodoc: + def to_liquid + self + end +end + +class Array # :nodoc: + def to_liquid + self + end +end + +class Hash # :nodoc: + def to_liquid + self + end +end + +class Numeric # :nodoc: + def to_liquid + self + end +end + +class Time # :nodoc: + def to_liquid + self + end +end + +class DateTime < Date # :nodoc: + def to_liquid + self + end +end + +class Date # :nodoc: + def to_liquid + self + end +end + +def true.to_liquid # :nodoc: + self +end + +def false.to_liquid # :nodoc: + self +end + +def nil.to_liquid # :nodoc: + self +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/file_system.rb b/vendor/plugins/liquid/lib/liquid/file_system.rb new file mode 100644 index 0000000..8c6b76d --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/file_system.rb @@ -0,0 +1,62 @@ +module Liquid + # A Liquid file system is way to let your templates retrieve other templates for use with the include tag. + # + # You can implement subclasses that retrieve templates from the database, from the file system using a different + # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit. + # + # You can add additional instance variables, arguments, or methods as needed. + # + # Example: + # + # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) + # liquid = Liquid::Template.parse(template) + # + # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. + class BlankFileSystem + # Called by Liquid to retrieve a template file + def read_template_file(template_path) + raise FileSystemError, "This liquid context does not allow includes." + end + end + + # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials, + # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added. + # + # For security reasons, template paths are only allowed to contain letters, numbers, and underscore. + # + # Example: + # + # file_system = Liquid::LocalFileSystem.new("/some/path") + # + # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" + # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" + # + class LocalFileSystem + attr_accessor :root + + def initialize(root) + @root = root + end + + def read_template_file(template_path) + full_path = full_path(template_path) + raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path) + + File.read(full_path) + end + + def full_path(template_path) + raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/ + + full_path = if template_path.include?('/') + File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid") + else + File.join(root, "_#{template_path}.liquid") + end + + raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/ + + full_path + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/htmltags.rb b/vendor/plugins/liquid/lib/liquid/htmltags.rb new file mode 100644 index 0000000..2fb8e4e --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/htmltags.rb @@ -0,0 +1,64 @@ +module Liquid + class TableRow < Block + Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/ + + def initialize(tag_name, markup, tokens) + if markup =~ Syntax + @variable_name = $1 + @collection_name = $2 + @attributes = {} + markup.scan(TagAttributes) do |key, value| + @attributes[key] = value + end + else + raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3") + end + + super + end + + def render(context) + collection = context[@collection_name] or return '' + + if @attributes['limit'] or @attributes['offset'] + limit = context[@attributes['limit']] || -1 + offset = context[@attributes['offset']] || 0 + collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)] + end + + length = collection.length + + cols = context[@attributes['cols']].to_i + + row = 1 + col = 0 + + result = ["\n"] + context.stack do + + collection.each_with_index do |item, index| + context[@variable_name] = item + context['tablerowloop'] = { + 'length' => length, + 'index' => index + 1, + 'index0' => index, + 'rindex' => length - index, + 'rindex0' => length - index -1, + 'first' => (index == 0), + 'last' => (index == length - 1) } + + result << [""] + render_all(@nodelist, context) + [''] + + if col == cols and not (index == length - 1) + col = 0 + result << ["\n"] + end + + end + end + result + ["\n"] + end + end + + Template.register_tag('tablerow', TableRow) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/standardfilters.rb b/vendor/plugins/liquid/lib/liquid/standardfilters.rb new file mode 100644 index 0000000..720f9be --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/standardfilters.rb @@ -0,0 +1,133 @@ +require 'cgi' + +module Liquid + + module StandardFilters + + # Return the size of an array or of an string + def size(input) + + input.respond_to?(:size) ? input.size : 0 + end + + # convert a input string to DOWNCASE + def downcase(input) + input.to_s.downcase + end + + # convert a input string to UPCASE + def upcase(input) + input.to_s.upcase + end + + # capitalize words in the input centence + def capitalize(input) + input.to_s.capitalize + end + + def escape(input) + CGI.escapeHTML(input) rescue input + end + + alias_method :h, :escape + + # Truncate a string down to x characters + def truncate(input, length = 50, truncate_string = "...") + if input.nil? then return end + l = length.to_i - truncate_string.length + l = 0 if l < 0 + input.length > length.to_i ? input[0...l] + truncate_string : input + end + + def truncatewords(input, words = 15, truncate_string = "...") + if input.nil? then return end + wordlist = input.to_s.split + l = words.to_i - 1 + l = 0 if l < 0 + wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input + end + + def strip_html(input) + input.to_s.gsub(/<.*?>/, '') + end + + # Join elements of the array with certain character between them + def join(input, glue = ' ') + [input].flatten.join(glue) + end + + # Sort elements of the array + def sort(input) + [input].flatten.sort + end + + # Reformat a date + # + # %a - The abbreviated weekday name (``Sun'') + # %A - The full weekday name (``Sunday'') + # %b - The abbreviated month name (``Jan'') + # %B - The full month name (``January'') + # %c - The preferred local date and time representation + # %d - Day of the month (01..31) + # %H - Hour of the day, 24-hour clock (00..23) + # %I - Hour of the day, 12-hour clock (01..12) + # %j - Day of the year (001..366) + # %m - Month of the year (01..12) + # %M - Minute of the hour (00..59) + # %p - Meridian indicator (``AM'' or ``PM'') + # %S - Second of the minute (00..60) + # %U - Week number of the current year, + # starting with the first Sunday as the first + # day of the first week (00..53) + # %W - Week number of the current year, + # starting with the first Monday as the first + # day of the first week (00..53) + # %w - Day of the week (Sunday is 0, 0..6) + # %x - Preferred representation for the date alone, no time + # %X - Preferred representation for the time alone, no date + # %y - Year without a century (00..99) + # %Y - Year with century + # %Z - Time zone name + # %% - Literal ``%'' character + def date(input, format) + + if format.to_s.empty? + return input.to_s + end + + date = case input + when String + Time.parse(input) + when Date, Time, DateTime + input + else + return input + end + + date.strftime(format.to_s) + rescue => e + input + end + + # Get the first element of the passed in array + # + # Example: + # {{ product.images | first | to_img }} + # + def first(array) + array.first if array.respond_to?(:first) + end + + # Get the last element of the passed in array + # + # Example: + # {{ product.images | last | to_img }} + # + def last(array) + array.last if array.respond_to?(:last) + end + + end + + Template.register_filter(StandardFilters) +end diff --git a/vendor/plugins/liquid/lib/liquid/strainer.rb b/vendor/plugins/liquid/lib/liquid/strainer.rb new file mode 100644 index 0000000..285f2f7 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/strainer.rb @@ -0,0 +1,43 @@ +module Liquid + + # Strainer is the parent class for the filters system. + # New filters are mixed into the strainer class which is then instanciated for each liquid template render run. + # + # One of the strainer's responsibilities is to keep malicious method calls out + class Strainer #:nodoc: + + @@required_methods = ["__send__", "__id__", "respond_to?", "extend", "methods", "class"] + + @@filters = [] + + def initialize(context) + @context = context + end + + def self.global_filter(filter) + raise StandardError, "Passed filter is not a module" unless filter.is_a?(Module) + @@filters << filter + end + + def self.create(context) + strainer = Strainer.new(context) + @@filters.each { |m| strainer.extend(m) } + strainer + end + + def respond_to?(method) + method_name = method.to_s + return false if method_name =~ /^__/ + return false if @@required_methods.include?(method_name) + super + end + + # remove all standard methods from the bucket so circumvent security + # problems + instance_methods.each do |m| + unless @@required_methods.include?(m) + undef_method m + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tag.rb b/vendor/plugins/liquid/lib/liquid/tag.rb new file mode 100644 index 0000000..e0bf35d --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tag.rb @@ -0,0 +1,26 @@ +module Liquid + + class Tag + attr_accessor :nodelist + + def initialize(tag_name, markup, tokens) + @tag_name = tag_name + @markup = markup + parse(tokens) + end + + def parse(tokens) + end + + def name + self.class.name.downcase + end + + def render(context) + '' + end + end + + +end + \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/assign.rb b/vendor/plugins/liquid/lib/liquid/tags/assign.rb new file mode 100644 index 0000000..a68cf30 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/assign.rb @@ -0,0 +1,33 @@ +module Liquid + + # Assign sets a variable in your template. + # + # {% assign foo = 'monkey' %} + # + # You can then use the variable later in the page. + # + # {{ monkey }} + # + class Assign < Tag + Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/ + + def initialize(tag_name, markup, tokens) + if markup =~ Syntax + @to = $1 + @from = $2 + else + raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]") + end + + super + end + + def render(context) + context.scopes.last[@to.to_s] = context[@from] + '' + end + + end + + Template.register_tag('assign', Assign) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/capture.rb b/vendor/plugins/liquid/lib/liquid/tags/capture.rb new file mode 100644 index 0000000..f4f6f3c --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/capture.rb @@ -0,0 +1,35 @@ +module Liquid + + # Capture stores the result of a block into a variable without rendering it inplace. + # + # {% capture heading %} + # Monkeys! + # {% endcapture %} + # ... + #

    {{ monkeys }}

    + # + # Capture is useful for saving content for use later in your template, such as + # in a sidebar or footer. + # + class Capture < Block + Syntax = /(\w+)/ + + def initialize(tag_name, markup, tokens) + if markup =~ Syntax + @to = $1 + else + raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]") + end + + super + end + + def render(context) + output = super + context[@to] = output.to_s + '' + end + end + + Template.register_tag('capture', Capture) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/case.rb b/vendor/plugins/liquid/lib/liquid/tags/case.rb new file mode 100644 index 0000000..0733c51 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/case.rb @@ -0,0 +1,83 @@ +module Liquid + class Case < Block + Syntax = /(#{QuotedFragment})/ + WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/ + + def initialize(tag_name, markup, tokens) + @blocks = [] + + if markup =~ Syntax + @left = $1 + else + raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]") + end + + super + end + + def unknown_tag(tag, markup, tokens) + @nodelist = [] + case tag + when 'when' + record_when_condition(markup) + when 'else' + record_else_condition(markup) + else + super + end + end + + def render(context) + context.stack do + execute_else_block = true + + @blocks.inject([]) do |output, block| + + if block.else? + + return render_all(block.attachment, context) if execute_else_block + + elsif block.evaluate(context) + + execute_else_block = false + output += render_all(block.attachment, context) + end + + output + end + end + end + + private + + def record_when_condition(markup) + while markup + # Create a new nodelist and assign it to the new block + if not markup =~ WhenSyntax + raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ") + end + + markup = $2 + + block = Condition.new(@left, '==', $1) + block.attach(@nodelist) + @blocks.push(block) + end + end + + def record_else_condition(markup) + + if not markup.strip.empty? + raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ") + end + + block = ElseCondition.new + block.attach(@nodelist) + @blocks << block + end + + + end + + Template.register_tag('case', Case) +end diff --git a/vendor/plugins/liquid/lib/liquid/tags/comment.rb b/vendor/plugins/liquid/lib/liquid/tags/comment.rb new file mode 100644 index 0000000..8ce7e0e --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/comment.rb @@ -0,0 +1,9 @@ +module Liquid + class Comment < Block + def render(context) + '' + end + end + + Template.register_tag('comment', Comment) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/cycle.rb b/vendor/plugins/liquid/lib/liquid/tags/cycle.rb new file mode 100644 index 0000000..a34f29a --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/cycle.rb @@ -0,0 +1,60 @@ +module Liquid + + # Cycle is usually used within a loop to alternate between values, like colors or DOM classes. + # + # {% for item in items %} + #
    {{ item }}
    + # {% end %} + # + #
    Item one
    + #
    Item two
    + #
    Item three
    + #
    Item four
    + #
    Item five
    + # + class Cycle < Tag + SimpleSyntax = /#{QuotedFragment}/ + NamedSyntax = /(#{QuotedFragment})\s*\:\s*(.*)/ + + def initialize(tag_name, markup, tokens) + case markup + when NamedSyntax + @variables = variables_from_string($2) + @name = $1 + when SimpleSyntax + @variables = variables_from_string(markup) + @name = "'#{@variables.to_s}'" + else + raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]") + end + + super + end + + def render(context) + context.registers[:cycle] ||= Hash.new(0) + + context.stack do + key = context[@name] + iteration = context.registers[:cycle][key] + result = context[@variables[iteration]] + iteration += 1 + iteration = 0 if iteration >= @variables.size + context.registers[:cycle][key] = iteration + result + end + end + + private + + def variables_from_string(markup) + markup.split(',').collect do |var| + var =~ /\s*(#{QuotedFragment})\s*/ + $1 ? $1 : nil + end.compact + end + + end + + Template.register_tag('cycle', Cycle) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/for.rb b/vendor/plugins/liquid/lib/liquid/tags/for.rb new file mode 100644 index 0000000..80c135f --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/for.rb @@ -0,0 +1,118 @@ +module Liquid + + # "For" iterates over an array or collection. + # Several useful variables are available to you within the loop. + # + # == Basic usage: + # {% for item in collection %} + # {{ forloop.index }}: {{ item.name }} + # {% endfor %} + # + # == Advanced usage: + # {% for item in collection %} + #
    + # Item {{ forloop.index }}: {{ item.name }} + #
    + # {% endfor %} + # + # You can also define a limit and offset much like SQL. Remember + # that offset starts at 0 for the first item. + # + # {% for item in collection limit:5 offset:10 %} + # {{ item.name }} + # {% end %} + # + # == Available variables: + # + # forloop.name:: 'item-collection' + # forloop.length:: Length of the loop + # forloop.index:: The current item's position in the collection; + # forloop.index starts at 1. + # This is helpful for non-programmers who start believe + # the first item in an array is 1, not 0. + # forloop.index0:: The current item's position in the collection + # where the first item is 0 + # forloop.rindex:: Number of items remaining in the loop + # (length - index) where 1 is the last item. + # forloop.rindex0:: Number of items remaining in the loop + # where 0 is the last item. + # forloop.first:: Returns true if the item is the first item. + # forloop.last:: Returns true if the item is the last item. + # + class For < Block + Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/ + + def initialize(tag_name, markup, tokens) + if markup =~ Syntax + @variable_name = $1 + @collection_name = $2 + @name = "#{$1}-#{$2}" + @attributes = {} + markup.scan(TagAttributes) do |key, value| + @attributes[key] = value + end + else + raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]") + end + + super + end + + def render(context) + context.registers[:for] ||= Hash.new(0) + + collection = context[@collection_name] + collection = collection.to_a if collection.is_a?(Range) + + return '' if collection.nil? or collection.empty? + + range = (0..collection.length) + + if @attributes['limit'] or @attributes['offset'] + offset = 0 + if @attributes['offset'] == 'continue' + offset = context.registers[:for][@name] + else + offset = context[@attributes['offset']] || 0 + end + limit = context[@attributes['limit']] + + range_end = limit ? offset + limit : collection.length + range = (offset..range_end-1) + + # Save the range end in the registers so that future calls to + # offset:continue have something to pick up + context.registers[:for][@name] = range_end + end + + result = [] + segment = collection[range] + return '' if segment.nil? + + context.stack do + length = segment.length + + segment.each_with_index do |item, index| + context[@variable_name] = item + context['forloop'] = { + 'name' => @name, + 'length' => length, + 'index' => index + 1, + 'index0' => index, + 'rindex' => length - index, + 'rindex0' => length - index -1, + 'first' => (index == 0), + 'last' => (index == length - 1) } + + result << render_all(@nodelist, context) + end + end + + # Store position of last element we rendered. This allows us to do + + result + end + end + + Template.register_tag('for', For) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/if.rb b/vendor/plugins/liquid/lib/liquid/tags/if.rb new file mode 100644 index 0000000..9e7c8bc --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/if.rb @@ -0,0 +1,66 @@ +module Liquid + + # If is the conditional block + # + # {% if user.admin %} + # Admin user! + # {% else %} + # Not admin user + # {% endif %} + # + # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. + # + # Note you can't use "and" within the If block. You should wrap complex logic in + # the Drop class or in a helper method. + # + class If < Block + Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/ + + def initialize(tag_name, markup, tokens) + + @blocks = [] + + push_block('if', markup) + + super + end + + def unknown_tag(tag, markup, tokens) + if ['elsif', 'else'].include?(tag) + push_block(tag, markup) + else + super + end + end + + def render(context) + context.stack do + @blocks.each do |block| + if block.evaluate(context) + return render_all(block.attachment, context) + end + end + '' + end + end + + private + + def push_block(tag, markup) + + block = if tag == 'else' + ElseCondition.new + elsif markup =~ Syntax + Condition.new($1, $2, $3) + else + raise SyntaxError.new("Syntax Error in tag '#{tag}' - Valid syntax: #{tag} [condition]") + end + + @blocks.push(block) + @nodelist = block.attach(Array.new) + end + + end + + Template.register_tag('if', If) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb b/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb new file mode 100644 index 0000000..a4406c6 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/ifchanged.rb @@ -0,0 +1,20 @@ +module Liquid + class Ifchanged < Block + + def render(context) + context.stack do + + output = render_all(@nodelist, context) + + if output != context.registers[:ifchanged] + context.registers[:ifchanged] = output + output + else + '' + end + end + end + end + + Template.register_tag('ifchanged', Ifchanged) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/include.rb b/vendor/plugins/liquid/lib/liquid/tags/include.rb new file mode 100644 index 0000000..2f9439f --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/include.rb @@ -0,0 +1,55 @@ +module Liquid + class Include < Tag + Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/ + + def initialize(tag_name, markup, tokens) + if markup =~ Syntax + + @template_name = $1 + @variable_name = $3 + @attributes = {} + + markup.scan(TagAttributes) do |key, value| + @attributes[key] = value + end + + else + raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]") + end + + super + end + + def parse(tokens) + end + + def render(context) + source = Liquid::Template.file_system.read_template_file(context[@template_name]) + partial = Liquid::Template.parse(source) + + variable = context[@variable_name || @template_name[1..-2]] + + context.stack do + @attributes.each do |key, value| + context[key] = context[value] + end + + if variable.is_a?(Array) + + variable.collect do |variable| + context[@template_name[1..-2]] = variable + partial.render(context) + end + + else + + context[@template_name[1..-2]] = variable + partial.render(context) + + end + end + end + end + + Template.register_tag('include', Include) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/tags/unless.rb b/vendor/plugins/liquid/lib/liquid/tags/unless.rb new file mode 100644 index 0000000..74a76ab --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/tags/unless.rb @@ -0,0 +1,33 @@ +require File.dirname(__FILE__) + '/if' + +module Liquid + + # Unless is a conditional just like 'if' but works on the inverse logic. + # + # {% unless x < 0 %} x is greater than zero {% end %} + # + class Unless < If + def render(context) + context.stack do + + # First condition is interpreted backwards ( if not ) + block = @blocks.first + unless block.evaluate(context) + return render_all(block.attachment, context) + end + + # After the first condition unless works just like if + @blocks[1..-1].each do |block| + if block.evaluate(context) + return render_all(block.attachment, context) + end + end + + '' + end + end + end + + + Template.register_tag('unless', Unless) +end \ No newline at end of file diff --git a/vendor/plugins/liquid/lib/liquid/template.rb b/vendor/plugins/liquid/lib/liquid/template.rb new file mode 100644 index 0000000..ad7bcd9 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/template.rb @@ -0,0 +1,145 @@ +module Liquid + + # Templates are central to liquid. + # Interpretating templates is a two step process. First you compile the + # source code you got. During compile time some extensive error checking is performed. + # your code should expect to get some SyntaxErrors. + # + # After you have a compiled template you can then render it. + # You can use a compiled template over and over again and keep it cached. + # + # Example: + # + # template = Liquid::Template.parse(source) + # template.render('user_name' => 'bob') + # + class Template + attr_accessor :root + @@file_system = BlankFileSystem.new + + class <Template object from liquid source code + def parse(source) + template = Template.new + template.parse(source) + template + end + end + + # creates a new Template from an array of tokens. Use Template.parse instead + def initialize + end + + # Parse source code. + # Returns self for easy chaining + def parse(source) + @root = Document.new(tokenize(source)) + self + end + + def registers + @registers ||= {} + end + + def assigns + @assigns ||= {} + end + + def errors + @errors ||= [] + end + + # Render takes a hash with local variables. + # + # if you use the same filters over and over again consider registering them globally + # with Template.register_filter + # + # Following options can be passed: + # + # * filters : array with local filters + # * registers : hash with register variables. Those can be accessed from + # filters and tags and might be useful to integrate liquid more with its host application + # + def render(*args) + return '' if @root.nil? + + context = case args.first + when Liquid::Context + args.shift + when Hash + self.assigns.merge!(args.shift) + Context.new(assigns, registers, @rethrow_errors) + when nil + Context.new(assigns, registers, @rethrow_errors) + else + raise ArgumentError, "Expect Hash or Liquid::Context as parameter" + end + + case args.last + when Hash + options = args.pop + + if options[:registers].is_a?(Hash) + self.registers.merge!(options[:registers]) + end + + if options[:filters] + context.add_filters(options[:filters]) + end + when Module + context.add_filters(args.pop) + when Array + context.add_filters(args.pop) + end + + + # render the nodelist. + # for performance reasons we get a array back here. to_s will make a string out of it + begin + @root.render(context).to_s + ensure + @errors = context.errors + end + end + + def render!(*args) + @rethrow_errors = true; render(*args) + end + + private + + # Uses the Liquid::TemplateParser regexp to tokenize the passed source + def tokenize(source) + return [] if source.to_s.empty? + tokens = source.split(TemplateParser) + + # removes the rogue empty element at the beginning of the array + tokens.shift if tokens[0] and tokens[0].empty? + + tokens + end + + end +end diff --git a/vendor/plugins/liquid/lib/liquid/variable.rb b/vendor/plugins/liquid/lib/liquid/variable.rb new file mode 100644 index 0000000..16d7f99 --- /dev/null +++ b/vendor/plugins/liquid/lib/liquid/variable.rb @@ -0,0 +1,52 @@ +module Liquid + + # Holds variables. Variables are only loaded "just in time" + # and are not evaluated as part of the render stage + # + # {{ monkey }} + # {{ user.name }} + # + # Variables can be combined with filters: + # + # {{ user | link }} + # + class Variable + attr_accessor :filters, :name + + def initialize(markup) + @markup = markup + @name = nil + @filters = [] + if match = markup.match(/\s*(#{QuotedFragment})/) + @name = match[1] + if markup.match(/#{FilterSperator}\s*(.*)/) + filters = Regexp.last_match(1).split(/#{FilterSperator}/) + + filters.each do |f| + if matches = f.match(/\s*(\w+)/) + filtername = matches[1] + filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*(#{QuotedFragment})/).flatten + @filters << [filtername.to_sym, filterargs] + end + end + end + end + end + + def render(context) + return '' if @name.nil? + output = context[@name] + @filters.inject(output) do |output, filter| + filterargs = filter[1].to_a.collect do |a| + context[a] + end + begin + output = context.invoke(filter[0], output, *filterargs) + rescue FilterNotFound + raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found." + end + end + output + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/block_test.rb b/vendor/plugins/liquid/test/block_test.rb new file mode 100644 index 0000000..270938e --- /dev/null +++ b/vendor/plugins/liquid/test/block_test.rb @@ -0,0 +1,58 @@ +require File.dirname(__FILE__) + '/helper' + +class VariableTest < Test::Unit::TestCase + include Liquid + + def test_blankspace + template = Liquid::Template.parse(" ") + assert_equal [" "], template.root.nodelist + end + + def test_variable_beginning + template = Liquid::Template.parse("{{funk}} ") + assert_equal 2, template.root.nodelist.size + assert_equal Variable, template.root.nodelist[0].class + assert_equal String, template.root.nodelist[1].class + end + + def test_variable_end + template = Liquid::Template.parse(" {{funk}}") + assert_equal 2, template.root.nodelist.size + assert_equal String, template.root.nodelist[0].class + assert_equal Variable, template.root.nodelist[1].class + end + + def test_variable_middle + template = Liquid::Template.parse(" {{funk}} ") + assert_equal 3, template.root.nodelist.size + assert_equal String, template.root.nodelist[0].class + assert_equal Variable, template.root.nodelist[1].class + assert_equal String, template.root.nodelist[2].class + end + + def test_variable_many_embedded_fragments + template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") + assert_equal 7, template.root.nodelist.size + assert_equal [String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist) + end + + def test_with_block + template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") + assert_equal [String, Comment, String], block_types(template.root.nodelist) + assert_equal 3, template.root.nodelist.size + end + + def test_with_custom_tag + Liquid::Template.register_tag("testtag", Block) + + assert_nothing_thrown do + template = Liquid::Template.parse( "{% testtag %} {% endtesttag %}") + end + end + + private + + def block_types(nodelist) + nodelist.collect { |node| node.class } + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/condition_test.rb b/vendor/plugins/liquid/test/condition_test.rb new file mode 100644 index 0000000..3d2627b --- /dev/null +++ b/vendor/plugins/liquid/test/condition_test.rb @@ -0,0 +1,75 @@ +require File.dirname(__FILE__) + '/helper' + +class ConditionTest < Test::Unit::TestCase + include Liquid + + def test_default_operators_evalute_true + assert_evalutes_true '1', '==', '1' + assert_evalutes_true '1', '!=', '2' + assert_evalutes_true '1', '<>', '2' + assert_evalutes_true '1', '<', '2' + assert_evalutes_true '2', '>', '1' + assert_evalutes_true '1', '>=', '1' + assert_evalutes_true '2', '>=', '1' + assert_evalutes_true '1', '<=', '2' + assert_evalutes_true '1', '<=', '1' + end + + def test_default_operators_evalute_false + assert_evalutes_false '1', '==', '2' + assert_evalutes_false '1', '!=', '1' + assert_evalutes_false '1', '<>', '1' + assert_evalutes_false '1', '<', '0' + assert_evalutes_false '2', '>', '4' + assert_evalutes_false '1', '>=', '3' + assert_evalutes_false '2', '>=', '4' + assert_evalutes_false '1', '<=', '0' + assert_evalutes_false '1', '<=', '0' + end + + def test_contains_works_on_strings + assert_evalutes_true "'bob'", 'contains', "'o'" + assert_evalutes_true "'bob'", 'contains', "'b'" + assert_evalutes_true "'bob'", 'contains', "'bo'" + assert_evalutes_true "'bob'", 'contains', "'ob'" + assert_evalutes_true "'bob'", 'contains', "'bob'" + + assert_evalutes_false "'bob'", 'contains', "'bob2'" + assert_evalutes_false "'bob'", 'contains', "'a'" + assert_evalutes_false "'bob'", 'contains', "'---'" + end + + def test_contains_works_on_arrays + @context = Liquid::Context.new + @context['array'] = [1,2,3,4,5] + + assert_evalutes_false "array", 'contains', '0' + assert_evalutes_true "array", 'contains', '1' + assert_evalutes_true "array", 'contains', '2' + assert_evalutes_true "array", 'contains', '3' + assert_evalutes_true "array", 'contains', '4' + assert_evalutes_true "array", 'contains', '5' + assert_evalutes_false "array", 'contains', '6' + + assert_evalutes_false "array", 'contains', '"1"' + + end + + def test_should_allow_custom_proc_operator + Condition.operators['starts_with'] = Proc.new { |cond, left, right| left =~ %r{^#{right}}} + + assert_evalutes_true "'bob'", 'starts_with', "'b'" + assert_evalutes_false "'bob'", 'starts_with', "'o'" + ensure + Condition.operators.delete 'starts_with' + end + + private + def assert_evalutes_true(left, op, right) + assert Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated false: #{left} #{op} #{right}" + end + + def assert_evalutes_false(left, op, right) + assert !Condition.new(left, op, right).evaluate(@context || Liquid::Context.new), "Evaluated true: #{left} #{op} #{right}" + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/context_test.rb b/vendor/plugins/liquid/test/context_test.rb new file mode 100644 index 0000000..9819c0a --- /dev/null +++ b/vendor/plugins/liquid/test/context_test.rb @@ -0,0 +1,411 @@ +require File.dirname(__FILE__) + '/helper' +class HundredCentes + def to_liquid + 100 + end +end + +class CentsDrop < Liquid::Drop + def amount + HundredCentes.new + end + + def non_zero? + true + end +end + +class ContextSensitiveDrop < Liquid::Drop + def test + @context['test'] + end +end + +class Category < Liquid::Drop + attr_accessor :name + + def initialize(name) + @name = name + end + + def to_liquid + CategoryDrop.new(self) + end +end + +class CategoryDrop + attr_accessor :category, :context + def initialize(category) + @category = category + end +end + + +class ContextTest < Test::Unit::TestCase + include Liquid + + def setup + @template = Liquid::Template.new + @context = Liquid::Context.new(@template.assigns, @template.registers) + end + + def test_variables + @context['string'] = 'string' + assert_equal 'string', @context['string'] + + @context['num'] = 5 + assert_equal 5, @context['num'] + + @context['time'] = Time.parse('2006-06-06 12:00:00') + assert_equal Time.parse('2006-06-06 12:00:00'), @context['time'] + + @context['date'] = Date.today + assert_equal Date.today, @context['date'] + + now = DateTime.now + @context['datetime'] = now + assert_equal now, @context['datetime'] + + @context['bool'] = true + assert_equal true, @context['bool'] + + @context['bool'] = false + assert_equal false, @context['bool'] + + @context['nil'] = nil + assert_equal nil, @context['nil'] + assert_equal nil, @context['nil'] + end + + def test_variables_not_existing + assert_equal nil, @context['does_not_exist'] + end + + def test_scoping + assert_nothing_raised do + @context.push + @context.pop + end + + assert_raise(Liquid::ContextError) do + @context.pop + end + + assert_raise(Liquid::ContextError) do + @context.push + @context.pop + @context.pop + end + end + + def test_length_query + + @context['numbers'] = [1,2,3,4] + + assert_equal 4, @context['numbers.size'] + + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4} + + assert_equal 4, @context['numbers.size'] + + @context['numbers'] = {1 => 1,2 => 2,3 => 3,4 => 4, 'size' => 1000} + + assert_equal 1000, @context['numbers.size'] + + end + + def test_add_filter + + filter = Module.new do + def hi(output) + output + ' hi!' + end + end + + context = Context.new(@template) + context.add_filters(filter) + assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') + + context = Context.new(@template) + assert_equal 'hi?', context.invoke(:hi, 'hi?') + + context.add_filters(filter) + assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') + + end + + def test_override_global_filter + global = Module.new do + def notice(output) + "Global #{output}" + end + end + + local = Module.new do + def notice(output) + "Local #{output}" + end + end + + Template.register_filter(global) + assert_equal 'Global test', Template.parse("{{'test' | notice }}").render + assert_equal 'Local test', Template.parse("{{'test' | notice }}").render({}, :filters => [local]) + end + + def test_only_intended_filters_make_it_there + + filter = Module.new do + def hi(output) + output + ' hi!' + end + end + + context = Context.new(@template) + methods = context.strainer.methods + context.add_filters(filter) + assert_equal (methods + ['hi']).sort, context.strainer.methods.sort + end + + def test_add_item_in_outer_scope + @context['test'] = 'test' + @context.push + assert_equal 'test', @context['test'] + @context.pop + assert_equal 'test', @context['test'] + end + + def test_add_item_in_inner_scope + @context.push + @context['test'] = 'test' + assert_equal 'test', @context['test'] + @context.pop + assert_equal nil, @context['test'] + end + + def test_hierachical_data + @context['hash'] = {"name" => 'tobi'} + assert_equal 'tobi', @context['hash.name'] + end + + def test_keywords + assert_equal true, @context['true'] + assert_equal false, @context['false'] + end + + def test_digits + assert_equal 100, @context['100'] + assert_equal 100.00, @context['100.00'] + end + + def test_strings + assert_equal "hello!", @context['"hello!"'] + assert_equal "hello!", @context["'hello!'"] + end + + def test_merge + @context.merge({ "test" => "test" }) + assert_equal 'test', @context['test'] + @context.merge({ "test" => "newvalue", "foo" => "bar" }) + assert_equal 'newvalue', @context['test'] + assert_equal 'bar', @context['foo'] + end + + def test_array_notation + @context['test'] = [1,2,3,4,5] + + assert_equal 1, @context['test[0]'] + assert_equal 2, @context['test[1]'] + assert_equal 3, @context['test[2]'] + assert_equal 4, @context['test[3]'] + assert_equal 5, @context['test[4]'] + end + + def test_recoursive_array_notation + @context['test'] = {'test' => [1,2,3,4,5]} + + assert_equal 1, @context['test.test[0]'] + + @context['test'] = [{'test' => 'worked'}] + + assert_equal 'worked', @context['test[0].test'] + end + + def test_hash_to_array_transition + @context['colors'] = { + 'Blue' => ['003366','336699', '6699CC', '99CCFF'], + 'Green' => ['003300','336633', '669966', '99CC99'], + 'Yellow' => ['CC9900','FFCC00', 'FFFF99', 'FFFFCC'], + 'Red' => ['660000','993333', 'CC6666', 'FF9999'] + } + + assert_equal '003366', @context['colors.Blue[0]'] + assert_equal 'FF9999', @context['colors.Red[3]'] + end + + def test_try_first + @context['test'] = [1,2,3,4,5] + + assert_equal 1, @context['test.first'] + assert_equal 5, @context['test.last'] + + @context['test'] = {'test' => [1,2,3,4,5]} + + assert_equal 1, @context['test.test.first'] + assert_equal 5, @context['test.test.last'] + + @context['test'] = [1] + assert_equal 1, @context['test.first'] + assert_equal 1, @context['test.last'] + end + + def test_access_hashes_with_hash_notation + + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + + + assert_equal 5, @context['products["count"]'] + assert_equal 'deepsnow', @context['products["tags"][0]'] + assert_equal 'deepsnow', @context['products["tags"].first'] + assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] + assert_equal 'element151cm', @context['product["variants"][1]["title"]'] + assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] + assert_equal 'element151cm', @context['product["variants"].last["title"]'] + end + + def test_access_variable_with_hash_notation + @context['foo'] = 'baz' + @context['bar'] = 'foo' + + assert_equal 'baz', @context['["foo"]'] + assert_equal 'baz', @context['[bar]'] + end + + def test_access_hashes_with_hash_access_variables + + @context['var'] = 'tags' + @context['nested'] = {'var' => 'tags'} + @context['products'] = {'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } + + assert_equal 'deepsnow', @context['products[var].first'] + assert_equal 'freestyle', @context['products[nested.var].last'] + end + + + def test_first_can_appear_in_middle_of_callchain + + @context['product'] = {'variants' => [ {'title' => 'draft151cm'}, {'title' => 'element151cm'} ]} + + assert_equal 'draft151cm', @context['product.variants[0].title'] + assert_equal 'element151cm', @context['product.variants[1].title'] + assert_equal 'draft151cm', @context['product.variants.first.title'] + assert_equal 'element151cm', @context['product.variants.last.title'] + + end + + def test_cents + @context.merge( "cents" => HundredCentes.new ) + assert_equal 100, @context['cents'] + end + + def test_nested_cents + @context.merge( "cents" => { 'amount' => HundredCentes.new} ) + assert_equal 100, @context['cents.amount'] + + @context.merge( "cents" => { 'cents' => { 'amount' => HundredCentes.new} } ) + assert_equal 100, @context['cents.cents.amount'] + end + + def test_cents_through_drop + @context.merge( "cents" => CentsDrop.new ) + assert_equal 100, @context['cents.amount'] + end + + def test_nested_cents_through_drop + @context.merge( "vars" => {"cents" => CentsDrop.new} ) + assert_equal 100, @context['vars.cents.amount'] + end + + def test_drop_methods_with_question_marks + @context.merge( "cents" => CentsDrop.new ) + assert @context['cents.non_zero?'] + end + + def test_context_from_within_drop + @context.merge( "test" => '123', "vars" => ContextSensitiveDrop.new ) + assert_equal '123', @context['vars.test'] + end + + def test_nested_context_from_within_drop + @context.merge( "test" => '123', "vars" => {"local" => ContextSensitiveDrop.new } ) + assert_equal '123', @context['vars.local.test'] + end + + def test_ranges + @context.merge( "test" => '5' ) + assert_equal (1..5), @context['(1..5)'] + assert_equal (1..5), @context['(1..test)'] + assert_equal (5..5), @context['(test..test)'] + end + + def test_cents_through_drop_nestedly + @context.merge( "cents" => {"cents" => CentsDrop.new} ) + assert_equal 100, @context['cents.cents.amount'] + + @context.merge( "cents" => { "cents" => {"cents" => CentsDrop.new}} ) + assert_equal 100, @context['cents.cents.cents.amount'] + end + + def test_proc_as_variable + @context['dynamic'] = Proc.new { 'Hello' } + + assert_equal 'Hello', @context['dynamic'] + end + + def test_lambda_as_variable + @context['dynamic'] = lambda { 'Hello' } + + assert_equal 'Hello', @context['dynamic'] + end + + def test_nested_lambda_as_variable + @context['dynamic'] = { "lambda" => lambda { 'Hello' } } + + assert_equal 'Hello', @context['dynamic.lambda'] + end + + def test_lambda_is_called_once + @context['callcount'] = lambda { @global ||= 0; @global += 1; @global.to_s } + + assert_equal '1', @context['callcount'] + assert_equal '1', @context['callcount'] + assert_equal '1', @context['callcount'] + + @global = nil + end + + def test_nested_lambda_is_called_once + @context['callcount'] = { "lambda" => lambda { @global ||= 0; @global += 1; @global.to_s } } + + assert_equal '1', @context['callcount.lambda'] + assert_equal '1', @context['callcount.lambda'] + assert_equal '1', @context['callcount.lambda'] + + @global = nil + end + + def test_access_to_context_from_proc + @context.registers[:magic] = 345392 + + @context['magic'] = lambda { @context.registers[:magic] } + + assert_equal 345392, @context['magic'] + end + + def test_to_liquid_and_context_at_first_level + @context['category'] = Category.new("foobar") + assert_kind_of CategoryDrop, @context['category'] + assert_equal @context, @context['category'].context + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/drop_test.rb b/vendor/plugins/liquid/test/drop_test.rb new file mode 100644 index 0000000..8a6921e --- /dev/null +++ b/vendor/plugins/liquid/test/drop_test.rb @@ -0,0 +1,141 @@ + +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class ContextDrop < Liquid::Drop + def scopes + @context.scopes.size + end + + def scopes_as_array + (1..@context.scopes.size).to_a + end + + def loop_pos + @context['forloop.index'] + end + + def break + Breakpoint.breakpoint + end + + def before_method(method) + return @context[method] + end +end + + +class ProductDrop < Liquid::Drop + + class TextDrop < Liquid::Drop + def array + ['text1', 'text2'] + end + + def text + 'text1' + end + end + + class CatchallDrop < Liquid::Drop + def before_method(method) + return 'method: ' << method + end + end + + def texts + TextDrop.new + end + + def catchall + CatchallDrop.new + end + + def context + ContextDrop.new + end + + protected + def callmenot + "protected" + end +end + + +class DropsTest < Test::Unit::TestCase + include Liquid + + def test_product_drop + + assert_nothing_raised do + tpl = Liquid::Template.parse( ' ' ) + tpl.render('product' => ProductDrop.new) + end + end + + def test_text_drop + output = Liquid::Template.parse( ' {{ product.texts.text }} ' ).render('product' => ProductDrop.new) + assert_equal ' text1 ', output + + end + + def test_text_drop + output = Liquid::Template.parse( ' {{ product.catchall.unknown }} ' ).render('product' => ProductDrop.new) + assert_equal ' method: unknown ', output + + end + + def test_text_array_drop + output = Liquid::Template.parse( '{% for text in product.texts.array %} {{text}} {% endfor %}' ).render('product' => ProductDrop.new) + assert_equal ' text1 text2 ', output + end + + def test_context_drop + output = Liquid::Template.parse( ' {{ context.bar }} ' ).render('context' => ContextDrop.new, 'bar' => "carrot") + assert_equal ' carrot ', output + end + + def test_nested_context_drop + output = Liquid::Template.parse( ' {{ product.context.foo }} ' ).render('product' => ProductDrop.new, 'foo' => "monkey") + assert_equal ' monkey ', output + end + + def test_protected + output = Liquid::Template.parse( ' {{ product.callmenot }} ' ).render('product' => ProductDrop.new) + assert_equal ' ', output + end + + def test_scope + assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new) + assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + end + + def test_scope_though_proc + assert_equal '1', Liquid::Template.parse( '{{ s }}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }) + assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ s }}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) + assert_equal '3', Liquid::Template.parse( '{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}' ).render('context' => ContextDrop.new, 's' => Proc.new{|c| c['context.scopes'] }, 'dummy' => [1]) + end + + def test_scope_with_assigns + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render('context' => ContextDrop.new) + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal 'test', Liquid::Template.parse( '{% assign header_gif = "test"%}{{header_gif}}' ).render('context' => ContextDrop.new) + assert_equal 'test', Liquid::Template.parse( "{% assign header_gif = 'test'%}{{header_gif}}" ).render('context' => ContextDrop.new) + end + + def test_scope_from_tags + assert_equal '1', Liquid::Template.parse( '{% for i in context.scopes_as_array %}{{i}}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '12', Liquid::Template.parse( '{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1]) + end + + def test_access_context_from_drop + assert_equal '123', Liquid::Template.parse( '{%for a in dummy%}{{ context.loop_pos }}{% endfor %}' ).render('context' => ContextDrop.new, 'dummy' => [1,2,3]) + end + + + +end + + diff --git a/vendor/plugins/liquid/test/error_handling_test.rb b/vendor/plugins/liquid/test/error_handling_test.rb new file mode 100644 index 0000000..3fbb27f --- /dev/null +++ b/vendor/plugins/liquid/test/error_handling_test.rb @@ -0,0 +1,65 @@ + +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class ErrorDrop < Liquid::Drop + def standard_error + raise StandardError, 'standard error' + end + + def argument_error + raise ArgumentError, 'argument error' + end + + def syntax_error + raise SyntaxError, 'syntax error' + end + +end + + +class ErrorHandlingTest < Test::Unit::TestCase + include Liquid + + def test_standard_error + assert_nothing_raised do + template = Liquid::Template.parse( ' {{ errors.standard_error }} ' ) + assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) + + assert_equal 1, template.errors.size + assert_equal StandardError, template.errors.first.class + end + end + + def test_syntax + + assert_nothing_raised do + + template = Liquid::Template.parse( ' {{ errors.syntax_error }} ' ) + assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) + + assert_equal 1, template.errors.size + assert_equal SyntaxError, template.errors.first.class + + end + + end + + def test_argument + + assert_nothing_raised do + + template = Liquid::Template.parse( ' {{ errors.argument_error }} ' ) + assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) + + assert_equal 1, template.errors.size + assert_equal ArgumentError, template.errors.first.class + + end + + end + + +end + + diff --git a/vendor/plugins/liquid/test/extra/breakpoint.rb b/vendor/plugins/liquid/test/extra/breakpoint.rb new file mode 100755 index 0000000..c118e22 --- /dev/null +++ b/vendor/plugins/liquid/test/extra/breakpoint.rb @@ -0,0 +1,547 @@ +# The Breakpoint library provides the convenience of +# being able to inspect and modify state, diagnose +# bugs all via IRB by simply setting breakpoints in +# your applications by the call of a method. +# +# This library was written and is supported by me, +# Florian Gross. I can be reached at flgr@ccan.de +# and enjoy getting feedback about my libraries. +# +# The whole library (including breakpoint_client.rb +# and binding_of_caller.rb) is licensed under the +# same license that Ruby uses. (Which is currently +# either the GNU General Public License or a custom +# one that allows for commercial usage.) If you for +# some good reason need to use this under another +# license please contact me. + +require 'irb' +require 'caller' +require 'drb' +require 'drb/acl' +require 'thread' + +module Breakpoint + id = %q$Id: breakpoint.rb 52 2005-02-26 19:43:19Z flgr $ + current_version = id.split(" ")[2] + unless defined?(Version) + # The Version of ruby-breakpoint you are using as String of the + # 1.2.3 form where the digits stand for release, major and minor + # version respectively. + Version = "0.5.0" + end + + extend self + + # This will pop up an interactive ruby session at a + # pre-defined break point in a Ruby application. In + # this session you can examine the environment of + # the break point. + # + # You can get a list of variables in the context using + # local_variables via +local_variables+. You can then + # examine their values by typing their names. + # + # You can have a look at the call stack via +caller+. + # + # The source code around the location where the breakpoint + # was executed can be examined via +source_lines+. Its + # argument specifies how much lines of context to display. + # The default amount of context is 5 lines. Note that + # the call to +source_lines+ can raise an exception when + # it isn't able to read in the source code. + # + # breakpoints can also return a value. They will execute + # a supplied block for getting a default return value. + # A custom value can be returned from the session by doing + # +throw(:debug_return, value)+. + # + # You can also give names to break points which will be + # used in the message that is displayed upon execution + # of them. + # + # Here's a sample of how breakpoints should be placed: + # + # class Person + # def initialize(name, age) + # @name, @age = name, age + # breakpoint("Person#initialize") + # end + # + # attr_reader :age + # def name + # breakpoint("Person#name") { @name } + # end + # end + # + # person = Person.new("Random Person", 23) + # puts "Name: #{person.name}" + # + # And here is a sample debug session: + # + # Executing break point "Person#initialize" at file.rb:4 in `initialize' + # irb(#):001:0> local_variables + # => ["name", "age", "_", "__"] + # irb(#):002:0> [name, age] + # => ["Random Person", 23] + # irb(#):003:0> [@name, @age] + # => ["Random Person", 23] + # irb(#):004:0> self + # => # + # irb(#):005:0> @age += 1; self + # => # + # irb(#):006:0> exit + # Executing break point "Person#name" at file.rb:9 in `name' + # irb(#):001:0> throw(:debug_return, "Overriden name") + # Name: Overriden name + # + # Breakpoint sessions will automatically have a few + # convenience methods available. See Breakpoint::CommandBundle + # for a list of them. + # + # Breakpoints can also be used remotely over sockets. + # This is implemented by running part of the IRB session + # in the application and part of it in a special client. + # You have to call Breakpoint.activate_drb to enable + # support for remote breakpoints and then run + # breakpoint_client.rb which is distributed with this + # library. See the documentation of Breakpoint.activate_drb + # for details. + def breakpoint(id = nil, context = nil, &block) + callstack = caller + callstack.slice!(0, 3) if callstack.first["breakpoint"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Executing break point " + (id ? "#{id.inspect} " : "") + + "at #{file}:#{line}" + (method ? " in `#{method}'" : "") + + if context then + return handle_breakpoint(context, message, file, line, &block) + end + + Binding.of_caller do |binding_context| + handle_breakpoint(binding_context, message, file, line, &block) + end + end + + # These commands are automatically available in all breakpoint shells. + module CommandBundle + # Proxy to a Breakpoint client. Lets you directly execute code + # in the context of the client. + class Client + def initialize(eval_handler) # :nodoc: + eval_handler.untaint + @eval_handler = eval_handler + end + + instance_methods.each do |method| + next if method[/^__.+__$/] + undef_method method + end + + # Executes the specified code at the client. + def eval(code) + @eval_handler.call(code) + end + + # Will execute the specified statement at the client. + def method_missing(method, *args, &block) + if args.empty? and not block + result = eval "#{method}" + else + # This is a bit ugly. The alternative would be using an + # eval context instead of an eval handler for executing + # the code at the client. The problem with that approach + # is that we would have to handle special expressions + # like "self", "nil" or constants ourself which is hard. + remote = eval %{ + result = lambda { |block, *args| #{method}(*args, &block) } + def result.call_with_block(*args, &block) + call(block, *args) + end + result + } + remote.call_with_block(*args, &block) + end + + return result + end + end + + # Returns the source code surrounding the location where the + # breakpoint was issued. + def source_lines(context = 5, return_line_numbers = false) + lines = File.readlines(@__bp_file).map { |line| line.chomp } + + break_line = @__bp_line + start_line = [break_line - context, 1].max + end_line = break_line + context + + result = lines[(start_line - 1) .. (end_line - 1)] + + if return_line_numbers then + return [start_line, break_line, result] + else + return result + end + end + + # Lets an object that will forward method calls to the breakpoint + # client. This is useful for outputting longer things at the client + # and so on. You can for example do these things: + # + # client.puts "Hello" # outputs "Hello" at client console + # # outputs "Hello" into the file temp.txt at the client + # client.File.open("temp.txt", "w") { |f| f.puts "Hello" } + def client() + if Breakpoint.use_drb? then + sleep(0.5) until Breakpoint.drb_service.eval_handler + Client.new(Breakpoint.drb_service.eval_handler) + else + Client.new(lambda { |code| eval(code, TOPLEVEL_BINDING) }) + end + end + end + + def handle_breakpoint(context, message, file = "", line = "", &block) # :nodoc: + catch(:debug_return) do |value| + eval(%{ + @__bp_file = #{file.inspect} + @__bp_line = #{line} + extend Breakpoint::CommandBundle + extend DRbUndumped if self + }, context) rescue nil + + if not use_drb? then + puts message + IRB.start(nil, IRB::WorkSpace.new(context)) + else + @drb_service.add_breakpoint(context, message) + end + + block.call if block + end + end + + # These exceptions will be raised on failed asserts + # if Breakpoint.asserts_cause_exceptions is set to + # true. + class FailedAssertError < RuntimeError + end + + # This asserts that the block evaluates to true. + # If it doesn't evaluate to true a breakpoint will + # automatically be created at that execution point. + # + # You can disable assert checking in production + # code by setting Breakpoint.optimize_asserts to + # true. (It will still be enabled when Ruby is run + # via the -d argument.) + # + # Example: + # person_name = "Foobar" + # assert { not person_name.nil? } + # + # Note: If you want to use this method from an + # unit test, you will have to call it by its full + # name, Breakpoint.assert. + def assert(context = nil, &condition) + return if Breakpoint.optimize_asserts and not $DEBUG + return if yield + + callstack = caller + callstack.slice!(0, 3) if callstack.first["assert"] + file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures + + message = "Assert failed at #{file}:#{line}#{" in `#{method}'" if method}." + + if Breakpoint.asserts_cause_exceptions and not $DEBUG then + raise(Breakpoint::FailedAssertError, message) + end + + message += " Executing implicit breakpoint." + + if context then + return handle_breakpoint(context, message, file, line) + end + + Binding.of_caller do |context| + handle_breakpoint(context, message, file, line) + end + end + + # Whether asserts should be ignored if not in debug mode. + # Debug mode can be enabled by running ruby with the -d + # switch or by setting $DEBUG to true. + attr_accessor :optimize_asserts + self.optimize_asserts = false + + # Whether an Exception should be raised on failed asserts + # in non-$DEBUG code or not. By default this is disabled. + attr_accessor :asserts_cause_exceptions + self.asserts_cause_exceptions = false + @use_drb = false + + attr_reader :drb_service # :nodoc: + + class DRbService # :nodoc: + include DRbUndumped + + def initialize + @handler = @eval_handler = @collision_handler = nil + + IRB.instance_eval { @CONF[:RC] = true } + IRB.run_config + end + + def collision + sleep(0.5) until @collision_handler + + @collision_handler.untaint + + @collision_handler.call + end + + def ping() end + + def add_breakpoint(context, message) + workspace = IRB::WorkSpace.new(context) + workspace.extend(DRbUndumped) + + sleep(0.5) until @handler + + @handler.untaint + @handler.call(workspace, message) + rescue Errno::ECONNREFUSED, DRb::DRbConnError + raise if Breakpoint.use_drb? + end + + attr_accessor :handler, :eval_handler, :collision_handler + end + + # Will run Breakpoint in DRb mode. This will spawn a server + # that can be attached to via the breakpoint-client command + # whenever a breakpoint is executed. This is useful when you + # are debugging CGI applications or other applications where + # you can't access debug sessions via the standard input and + # output of your application. + # + # You can specify an URI where the DRb server will run at. + # This way you can specify the port the server runs on. The + # default URI is druby://localhost:42531. + # + # Please note that breakpoints will be skipped silently in + # case the DRb server can not spawned. (This can happen if + # the port is already used by another instance of your + # application on CGI or another application.) + # + # Also note that by default this will only allow access + # from localhost. You can however specify a list of + # allowed hosts or nil (to allow access from everywhere). + # But that will still not protect you from somebody + # reading the data as it goes through the net. + # + # A good approach for getting security and remote access + # is setting up an SSH tunnel between the DRb service + # and the client. This is usually done like this: + # + # $ ssh -L20000:127.0.0.1:20000 -R10000:127.0.0.1:10000 example.com + # (This will connect port 20000 at the client side to port + # 20000 at the server side, and port 10000 at the server + # side to port 10000 at the client side.) + # + # After that do this on the server side: (the code being debugged) + # Breakpoint.activate_drb("druby://127.0.0.1:20000", "localhost") + # + # And at the client side: + # ruby breakpoint_client.rb -c druby://127.0.0.1:10000 -s druby://127.0.0.1:20000 + # + # Running through such a SSH proxy will also let you use + # breakpoint.rb in case you are behind a firewall. + # + # Detailed information about running DRb through firewalls is + # available at http://www.rubygarden.org/ruby?DrbTutorial + # + # == Security considerations + # Usually you will be fine when using the default druby:// URI and the default + # access control list. However, if you are sitting on a machine where there are + # local users that you likely can not trust (this is the case for example on + # most web hosts which have multiple users sitting on the same physical machine) + # you will be better off by doing client/server communication through a unix + # socket. This can be accomplished by calling with a drbunix:/ style URI, e.g. + # Breakpoint.activate_drb('drbunix:/tmp/breakpoint_server'). This + # will only work on Unix based platforms. + def activate_drb(uri = nil, allowed_hosts = ['localhost', '127.0.0.1', '::1'], + ignore_collisions = false) + + return false if @use_drb + + uri ||= 'druby://localhost:42531' + + if allowed_hosts then + acl = ["deny", "all"] + + Array(allowed_hosts).each do |host| + acl += ["allow", host] + end + + DRb.install_acl(ACL.new(acl)) + end + + @use_drb = true + @drb_service = DRbService.new + did_collision = false + begin + @service = DRb.start_service(uri, @drb_service) + rescue Errno::EADDRINUSE + if ignore_collisions then + nil + else + # The port is already occupied by another + # Breakpoint service. We will try to tell + # the old service that we want its port. + # It will then forward that request to the + # user and retry. + unless did_collision then + DRbObject.new(nil, uri).collision + did_collision = true + end + sleep(10) + retry + end + end + + return true + end + + # Deactivates a running Breakpoint service. + def deactivate_drb + Thread.exclusive do + @service.stop_service unless @service.nil? + @service = nil + @use_drb = false + @drb_service = nil + end + end + + # Returns true when Breakpoints are used over DRb. + # Breakpoint.activate_drb causes this to be true. + def use_drb? + @use_drb == true + end +end + +module IRB # :nodoc: + class << self; remove_method :start; end + def self.start(ap_path = nil, main_context = nil, workspace = nil) + $0 = File::basename(ap_path, ".rb") if ap_path + + # suppress some warnings about redefined constants + old_verbose, $VERBOSE = $VERBOSE, nil + IRB.setup(ap_path) + $VERBOSE = old_verbose + + if @CONF[:SCRIPT] then + irb = Irb.new(main_context, @CONF[:SCRIPT]) + else + irb = Irb.new(main_context) + end + + if workspace then + irb.context.workspace = workspace + end + + @CONF[:IRB_RC].call(irb.context) if @CONF[:IRB_RC] + @CONF[:MAIN_CONTEXT] = irb.context + + old_sigint = trap("SIGINT") do + begin + irb.signal_handle + rescue RubyLex::TerminateLineInput + # ignored + end + end + + catch(:IRB_EXIT) do + irb.eval_input + end + ensure + trap("SIGINT", old_sigint) + end + + class << self + alias :old_CurrentContext :CurrentContext + remove_method :CurrentContext + remove_method :parse_opts + end + + def IRB.CurrentContext + if old_CurrentContext.nil? and Breakpoint.use_drb? then + result = Object.new + def result.last_value; end + return result + else + old_CurrentContext + end + end + def IRB.parse_opts() end + + class Context # :nodoc: + alias :old_evaluate :evaluate + def evaluate(line, line_no) + if line.chomp == "exit" then + exit + else + old_evaluate(line, line_no) + end + end + end + + class WorkSpace # :nodoc: + alias :old_evaluate :evaluate + + def evaluate(*args) + if Breakpoint.use_drb? then + result = old_evaluate(*args) + if args[0] != :no_proxy and + not [true, false, nil].include?(result) + then + result.extend(DRbUndumped) rescue nil + end + return result + else + old_evaluate(*args) + end + end + end + + module InputCompletor # :nodoc: + def self.eval(code, context, *more) + # Big hack, this assumes that InputCompletor + # will only call eval() when it wants code + # to be executed in the IRB context. + IRB.conf[:MAIN_CONTEXT].workspace.evaluate(:no_proxy, code, *more) + end + end +end + +module DRb # :nodoc: + class DRbObject # :nodoc: + undef :inspect if method_defined?(:inspect) + undef :clone if method_defined?(:clone) + end +end + +# See Breakpoint.breakpoint +def breakpoint(id = nil, &block) + Binding.of_caller do |context| + Breakpoint.breakpoint(id, context, &block) + end +end + +# See Breakpoint.assert +def assert(&block) + Binding.of_caller do |context| + Breakpoint.assert(context, &block) + end +end diff --git a/vendor/plugins/liquid/test/extra/caller.rb b/vendor/plugins/liquid/test/extra/caller.rb new file mode 100755 index 0000000..14c96eb --- /dev/null +++ b/vendor/plugins/liquid/test/extra/caller.rb @@ -0,0 +1,80 @@ +class Continuation # :nodoc: + def self.create(*args, &block) # :nodoc: + cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} + result ||= args + return *[cc, *result] + end +end + +class Binding; end # for RDoc +# This method returns the binding of the method that called your +# method. It will raise an Exception when you're not inside a method. +# +# It's used like this: +# def inc_counter(amount = 1) +# Binding.of_caller do |binding| +# # Create a lambda that will increase the variable 'counter' +# # in the caller of this method when called. +# inc = eval("lambda { |arg| counter += arg }", binding) +# # We can refer to amount from inside this block safely. +# inc.call(amount) +# end +# # No other statements can go here. Put them inside the block. +# end +# counter = 0 +# 2.times { inc_counter } +# counter # => 2 +# +# Binding.of_caller must be the last statement in the method. +# This means that you will have to put everything you want to +# do after the call to Binding.of_caller into the block of it. +# This should be no problem however, because Ruby has closures. +# If you don't do this an Exception will be raised. Because of +# the way that Binding.of_caller is implemented it has to be +# done this way. +def Binding.of_caller(&block) + old_critical = Thread.critical + Thread.critical = true + count = 0 + cc, result, error, extra_data = Continuation.create(nil, nil) + error.call if error + + tracer = lambda do |*args| + type, context, extra_data = args[0], args[4], args + if type == "return" + count += 1 + # First this method and then calling one will return -- + # the trace event of the second event gets the context + # of the method which called the method that called this + # method. + if count == 2 + # It would be nice if we could restore the trace_func + # that was set before we swapped in our own one, but + # this is impossible without overloading set_trace_func + # in current Ruby. + set_trace_func(nil) + cc.call(eval("binding", context), nil, extra_data) + end + elsif type == "line" then + nil + elsif type == "c-return" and extra_data[3] == :set_trace_func then + nil + else + set_trace_func(nil) + error_msg = "Binding.of_caller used in non-method context or " + + "trailing statements of method using it aren't in the block." + cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil) + end + end + + unless result + set_trace_func(tracer) + return nil + else + Thread.critical = old_critical + case block.arity + when 1 then yield(result) + else yield(result, extra_data) + end + end +end diff --git a/vendor/plugins/liquid/test/file_system_test.rb b/vendor/plugins/liquid/test/file_system_test.rb new file mode 100644 index 0000000..d3ab948 --- /dev/null +++ b/vendor/plugins/liquid/test/file_system_test.rb @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class FileSystemTest < Test::Unit::TestCase + include Liquid + + def test_default + assert_raise(FileSystemError) do + BlankFileSystem.new.read_template_file("dummy") + end + end + + def test_local + file_system = Liquid::LocalFileSystem.new("/some/path") + assert_equal "/some/path/_mypartial.liquid" , file_system.full_path("mypartial") + assert_equal "/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial") + + assert_raise(FileSystemError) do + file_system.full_path("../dir/mypartial") + end + + assert_raise(FileSystemError) do + file_system.full_path("/dir/../../dir/mypartial") + end + + assert_raise(FileSystemError) do + file_system.full_path("/etc/passwd") + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/filter_test.rb b/vendor/plugins/liquid/test/filter_test.rb new file mode 100644 index 0000000..6cc5221 --- /dev/null +++ b/vendor/plugins/liquid/test/filter_test.rb @@ -0,0 +1,98 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + + +module MoneyFilter + def money(input) + sprintf(' %d$ ', input) + end + + def money_with_underscore(input) + sprintf(' %d$ ', input) + end +end + +module CanadianMoneyFilter + def money(input) + sprintf(' %d$ CAD ', input) + end +end + + +class FiltersTest < Test::Unit::TestCase + include Liquid + + def setup + @context = Context.new + end + + def test_local_filter + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + assert_equal ' 1000$ ', Variable.new("var | money").render(@context) + end + + def test_underscore_in_filter_name + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context) + end + + def test_second_filter_overwrites_first + @context['var'] = 1000 + @context.add_filters(MoneyFilter) + @context.add_filters(CanadianMoneyFilter) + assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context) + end + + def test_size + @context['var'] = 'abcd' + @context.add_filters(MoneyFilter) + assert_equal 4, Variable.new("var | size").render(@context) + end + + def test_join + @context['var'] = [1,2,3,4] + assert_equal "1 2 3 4", Variable.new("var | join").render(@context) + end + + def test_sort + @context['value'] = 3 + @context['numbers'] = [2,1,4,3] + @context['words'] = ['expected', 'as', 'alphabetic'] + @context['arrays'] = [['flattened'], ['are']] + assert_equal [1,2,3,4], Variable.new("numbers | sort").render(@context) + assert_equal ['alphabetic', 'as', 'expected'], + Variable.new("words | sort").render(@context) + assert_equal [3], Variable.new("value | sort").render(@context) + assert_equal ['are', 'flattened'], Variable.new("arrays | sort").render(@context) + end + + def test_strip_html + @context['var'] = "bla blub" + assert_equal "bla blub", Variable.new("var | strip_html").render(@context) + end + + def test_capitalize + @context['var'] = "blub" + assert_equal "Blub", Variable.new("var | capitalize").render(@context) + end +end + +class FiltersInTemplate < Test::Unit::TestCase + include Liquid + + def test_local_global + Template.register_filter(MoneyFilter) + + assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render(nil, nil) + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => CanadianMoneyFilter) + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, :filters => [CanadianMoneyFilter]) + end + + def test_local_filter_with_deprecated_syntax + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, CanadianMoneyFilter) + assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, [CanadianMoneyFilter]) + end + +end diff --git a/vendor/plugins/liquid/test/helper.rb b/vendor/plugins/liquid/test/helper.rb new file mode 100644 index 0000000..deb8185 --- /dev/null +++ b/vendor/plugins/liquid/test/helper.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.dirname(__FILE__)+ '/extra') + +require 'test/unit' +require 'test/unit/assertions' +require 'caller' +require 'breakpoint' +require File.dirname(__FILE__) + '/../lib/liquid' + + +module Test + module Unit + module Assertions + include Liquid + def assert_template_result(expected, template, assigns={}, message=nil) + assert_equal expected, Template.parse(template).render(assigns) + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/html_tag_test.rb b/vendor/plugins/liquid/test/html_tag_test.rb new file mode 100644 index 0000000..bd15b6f --- /dev/null +++ b/vendor/plugins/liquid/test/html_tag_test.rb @@ -0,0 +1,24 @@ +require File.dirname(__FILE__) + '/helper' + +class HtmlTagTest < Test::Unit::TestCase + include Liquid + + def test_html_table + + assert_template_result("\n 1 2 3 \n 4 5 6 \n", + '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', + 'numbers' => [1,2,3,4,5,6]) + + assert_template_result("\n\n", + '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', + 'numbers' => []) + end + + def test_html_table_with_different_cols + assert_template_result("\n 1 2 3 4 5 \n 6 \n", + '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', + 'numbers' => [1,2,3,4,5,6]) + + end + +end diff --git a/vendor/plugins/liquid/test/if_else_test.rb b/vendor/plugins/liquid/test/if_else_test.rb new file mode 100644 index 0000000..874dabc --- /dev/null +++ b/vendor/plugins/liquid/test/if_else_test.rb @@ -0,0 +1,104 @@ +require File.dirname(__FILE__) + '/helper' + +class IfElseTest < Test::Unit::TestCase + include Liquid + + def test_if + assert_template_result(' ',' {% if false %} this text should not go into the output {% endif %} ') + assert_template_result(' this text should go into the output ', + ' {% if true %} this text should go into the output {% endif %} ') + assert_template_result(' you rock ?','{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') + end + + def test_if_else + assert_template_result(' YES ','{% if false %} NO {% else %} YES {% endif %}') + assert_template_result(' YES ','{% if true %} YES {% else %} NO {% endif %}') + assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}') + end + + def test_if_boolean + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) + end + + def test_hash_miss_generates_false + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) + end + + def test_if_from_variable + assert_template_result('','{% if var %} NO {% endif %}', 'var' => false) + assert_template_result('','{% if var %} NO {% endif %}', 'var' => nil) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {'bar' => false}) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => {}) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => nil) + assert_template_result('','{% if foo.bar %} NO {% endif %}', 'foo' => true) + + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => "text") + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => true) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => 1) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => {}) + assert_template_result(' YES ','{% if var %} YES {% endif %}', 'var' => []) + assert_template_result(' YES ','{% if "foo" %} YES {% endif %}') + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => true}) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => "text"}) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => 1 }) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => {} }) + assert_template_result(' YES ','{% if foo.bar %} YES {% endif %}', 'foo' => {'bar' => [] }) + + assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => false) + assert_template_result(' YES ','{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) + assert_template_result(' YES ','{% if var %} YES {% else %} NO {% endif %}', 'var' => true) + assert_template_result(' YES ','{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") + + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'bar' => false}) + assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => true}) + assert_template_result(' YES ','{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => {'bar' => "text"}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {'notbar' => true}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) + assert_template_result(' YES ','{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => {'bar' => true}) + end + + def test_nested_if + assert_template_result('', '{% if false %}{% if false %} NO {% endif %}{% endif %}') + assert_template_result('', '{% if false %}{% if true %} NO {% endif %}{% endif %}') + assert_template_result('', '{% if true %}{% if false %} NO {% endif %}{% endif %}') + assert_template_result(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}') + + assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') + assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') + assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') + + end + + def test_comparisons_on_null + assert_template_result('','{% if null < 10 %} NO {% endif %}') + assert_template_result('','{% if null <= 10 %} NO {% endif %}') + assert_template_result('','{% if null >= 10 %} NO {% endif %}') + assert_template_result('','{% if null > 10 %} NO {% endif %}') + + assert_template_result('','{% if 10 < null %} NO {% endif %}') + assert_template_result('','{% if 10 <= null %} NO {% endif %}') + assert_template_result('','{% if 10 >= null %} NO {% endif %}') + assert_template_result('','{% if 10 > null %} NO {% endif %}') + end + + def test_else_if + assert_template_result('0','{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') + assert_template_result('1','{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') + assert_template_result('2','{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') + + assert_template_result('elsif','{% if false %}if{% elsif true %}elsif{% endif %}') + end + + def test_syntax_error_no_variable + assert_raise(SyntaxError){ assert_template_result('', '{% if jerry == 1 %}')} + end + + def test_if_with_custom_condition + Condition.operators['contains'] = :[] + + assert_template_result('yes', %({% if 'bob' contains 'o' %}yes{% endif %})) + assert_template_result('no', %({% if 'bob' contains 'f' %}yes{% else %}no{% endif %})) + ensure + Condition.operators.delete 'contains' + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/include_tag_test.rb b/vendor/plugins/liquid/test/include_tag_test.rb new file mode 100644 index 0000000..77ff26a --- /dev/null +++ b/vendor/plugins/liquid/test/include_tag_test.rb @@ -0,0 +1,96 @@ +require File.dirname(__FILE__) + '/helper' + +class TestFileSystem + def read_template_file(template_path) + case template_path + when "product" + "Product: {{ product.title }} " + + when "locale_variables" + "Locale: {{echo1}} {{echo2}}" + + when "variant" + "Variant: {{ variant.title }}" + + when "nested_template" + "{% include 'header' %} {% include 'body' %} {% include 'footer' %}" + + when "body" + "body {% include 'body_detail' %}" + + when "nested_product_template" + "Product: {{ nested_product_template.title }} {%include 'details'%} " + + else + template_path + end + end +end + +class IncludeTagTest < Test::Unit::TestCase + include Liquid + + def setup + Liquid::Template.file_system = TestFileSystem.new + end + + + def test_include_tag_with + assert_equal "Product: Draft 151cm ", + Template.parse("{% include 'product' with products[0] %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) + end + + def test_include_tag_with_default_name + assert_equal "Product: Draft 151cm ", + Template.parse("{% include 'product' %}").render( "product" => {'title' => 'Draft 151cm'} ) + end + + def test_include_tag_for + + assert_equal "Product: Draft 151cm Product: Element 155cm ", + Template.parse("{% include 'product' for products %}").render( "products" => [ {'title' => 'Draft 151cm'}, {'title' => 'Element 155cm'} ] ) + end + + def test_include_tag_with_local_variables + assert_equal "Locale: test123 ", + Template.parse("{% include 'locale_variables' echo1: 'test123' %}").render + end + + def test_include_tag_with_multiple_local_variables + assert_equal "Locale: test123 test321", + Template.parse("{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}").render + end + + def test_include_tag_with_multiple_local_variables_from_context + assert_equal "Locale: test123 test321", + Template.parse("{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}").render('echo1' => 'test123', 'more_echos' => { "echo2" => 'test321'}) + end + + def test_nested_include_tag + assert_equal "body body_detail", + Template.parse("{% include 'body' %}").render + + assert_equal "header body body_detail footer", + Template.parse("{% include 'nested_template' %}").render + end + + def test_nested_include_with_variable + + assert_equal "Product: Draft 151cm details ", + Template.parse("{% include 'nested_product_template' with product %}").render("product" => {"title" => 'Draft 151cm'}) + + assert_equal "Product: Draft 151cm details Product: Element 155cm details ", + Template.parse("{% include 'nested_product_template' for products %}").render("products" => [{"title" => 'Draft 151cm'}, {"title" => 'Element 155cm'}]) + + end + + def test_dynamically_choosen_template + + assert_equal "Test123", Template.parse("{% include template %}").render("template" => 'Test123') + assert_equal "Test321", Template.parse("{% include template %}").render("template" => 'Test321') + + assert_equal "Product: Draft 151cm ", Template.parse("{% include template for product %}").render("template" => 'product', 'product' => { 'title' => 'Draft 151cm'}) + + end + +end diff --git a/vendor/plugins/liquid/test/output_test.rb b/vendor/plugins/liquid/test/output_test.rb new file mode 100644 index 0000000..96d9090 --- /dev/null +++ b/vendor/plugins/liquid/test/output_test.rb @@ -0,0 +1,121 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +module FunnyFilter + + def make_funny(input) + 'LOL' + end + + def cite_funny(input) + "LOL: #{input}" + end + + def add_smiley(input, smiley = ":-)") + "#{input} #{smiley}" + end + + def add_tag(input, tag = "p", id = "foo") + %|<#{tag} id="#{id}">#{input}| + end + + def paragraph(input) + "

    #{input}

    " + end + + def link_to(name, url) + %|#{name}| + end +end + + +class OutputTest < Test::Unit::TestCase + include Liquid + + def setup + @assigns = { + 'best_cars' => 'bmw', + 'car' => {'bmw' => 'good', 'gm' => 'bad'} + } + + end + + def test_variable + text = %| {{best_cars}} | + + expected = %| bmw | + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_variable_traversing + text = %| {{car.bmw}} {{car.gm}} {{car.bmw}} | + + expected = %| good bad good | + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_variable_piping + text = %( {{ car.gm | make_funny }} ) + expected = %| LOL | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_input + text = %( {{ car.gm | cite_funny }} ) + expected = %| LOL: bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_args + text = %! {{ car.gm | add_smiley : ':-(' }} ! + expected = %| bad :-( | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_no_args + text = %! {{ car.gm | add_smiley }} ! + expected = %| bad :-) | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_multiple_variable_piping_with_args + text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! + expected = %| bad :-( :-( | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_args + text = %! {{ car.gm | add_tag : 'span', 'bar'}} ! + expected = %| bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_variable_piping_with_variable_args + text = %! {{ car.gm | add_tag : 'span', car.bmw}} ! + expected = %| bad | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_multiple_pipings + text = %( {{ best_cars | cite_funny | paragraph }} ) + expected = %|

    LOL: bmw

    | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + def test_link_to + text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) + expected = %| Typo | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => [FunnyFilter]) + end + + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/parsing_quirks_test.rb b/vendor/plugins/liquid/test/parsing_quirks_test.rb new file mode 100644 index 0000000..e8c0f0e --- /dev/null +++ b/vendor/plugins/liquid/test/parsing_quirks_test.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class ParsingQuirksTest < Test::Unit::TestCase + include Liquid + + def test_error_with_css + text = %| div { font-weight: bold; } | + template = Template.parse(text) + + assert_equal text, template.render + assert_equal [String], template.root.nodelist.collect {|i| i.class} + end + + def test_error_on_empty_filter + assert_nothing_raised do + Template.parse("{{test |a|b|}}") + Template.parse("{{test}}") + Template.parse("{{|test|}}") + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/regexp_test.rb b/vendor/plugins/liquid/test/regexp_test.rb new file mode 100644 index 0000000..719324c --- /dev/null +++ b/vendor/plugins/liquid/test/regexp_test.rb @@ -0,0 +1,40 @@ +require File.dirname(__FILE__) + '/helper' + +class RegexpTest < Test::Unit::TestCase + include Liquid + + def test_empty + assert_equal [], ''.scan(QuotedFragment) + end + + def test_quote + assert_equal ['"arg 1"'], '"arg 1"'.scan(QuotedFragment) + end + + + def test_words + assert_equal ['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment) + end + + def test_quoted_words + assert_equal ['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment) + end + + def test_quoted_words + assert_equal ['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment) + end + + def test_quoted_words_in_the_middle + assert_equal ['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment) + end + + def test_variable_parser + assert_equal ['var'], 'var'.scan(VariableParser) + assert_equal ['var', 'method'], 'var.method'.scan(VariableParser) + assert_equal ['var', '[method]'], 'var[method]'.scan(VariableParser) + assert_equal ['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser) + assert_equal ['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser) + assert_equal ['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser) + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/security_test.rb b/vendor/plugins/liquid/test/security_test.rb new file mode 100644 index 0000000..1ab0d6f --- /dev/null +++ b/vendor/plugins/liquid/test/security_test.rb @@ -0,0 +1,41 @@ +require File.dirname(__FILE__) + '/helper' + +module SecurityFilter + def add_one(input) + "#{input} + 1" + end +end + +class SecurityTest < Test::Unit::TestCase + include Liquid + + def test_no_instance_eval + text = %( {{ '1+1' | instance_eval }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + def test_no_existing_instance_eval + text = %( {{ '1+1' | __instance_eval__ }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + + def test_no_instance_eval_after_mixing_in_new_filter + text = %( {{ '1+1' | instance_eval }} ) + expected = %| 1+1 | + + assert_equal expected, Template.parse(text).render(@assigns) + end + + + def test_no_instance_eval_later_in_chain + text = %( {{ '1+1' | add_one | instance_eval }} ) + expected = %| 1+1 + 1 | + + assert_equal expected, Template.parse(text).render(@assigns, :filters => SecurityFilter) + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/standard_filter_test.rb b/vendor/plugins/liquid/test/standard_filter_test.rb new file mode 100644 index 0000000..ee78fd8 --- /dev/null +++ b/vendor/plugins/liquid/test/standard_filter_test.rb @@ -0,0 +1,107 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + + +class Filters + include Liquid::StandardFilters +end + + +class StandardFiltersTest < Test::Unit::TestCase + include Liquid + + def setup + @filters = Filters.new + end + + def test_size + assert_equal 3, @filters.size([1,2,3]) + assert_equal 0, @filters.size([]) + assert_equal 0, @filters.size(nil) + end + + def test_downcase + assert_equal 'testing', @filters.downcase("Testing") + assert_equal '', @filters.downcase(nil) + end + + def test_upcase + assert_equal 'TESTING', @filters.upcase("Testing") + assert_equal '', @filters.upcase(nil) + end + + def test_upcase + assert_equal 'TESTING', @filters.upcase("Testing") + assert_equal '', @filters.upcase(nil) + end + + def test_truncate + assert_equal '1234...', @filters.truncate('1234567890', 7) + assert_equal '1234567890', @filters.truncate('1234567890', 20) + assert_equal '...', @filters.truncate('1234567890', 0) + assert_equal '1234567890', @filters.truncate('1234567890') + end + + def test_escape + assert_equal '<strong>', @filters.escape('') + assert_equal '<strong>', @filters.h('') + end + + def test_truncatewords + assert_equal 'one two three', @filters.truncatewords('one two three', 4) + assert_equal 'one two...', @filters.truncatewords('one two three', 2) + assert_equal 'one two three', @filters.truncatewords('one two three') + assert_equal 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15) + end + + def test_strip_html + assert_equal 'test', @filters.strip_html("
    test
    ") + assert_equal 'test', @filters.strip_html("
    test
    ") + assert_equal '', @filters.strip_html(nil) + end + + def test_join + assert_equal '1 2 3 4', @filters.join([1,2,3,4]) + assert_equal '1 - 2 - 3 - 4', @filters.join([1,2,3,4], ' - ') + end + + def test_sort + assert_equal [1,2,3,4], @filters.sort([4,3,2,1]) + end + + def test_date + assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") + assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") + assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") + + assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") + assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") + assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") + + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") + assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) + + assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") + assert_equal nil, @filters.date(nil, "%B") + end + + + def test_first_last + assert_equal 1, @filters.first([1,2,3]) + assert_equal 3, @filters.last([1,2,3]) + assert_equal nil, @filters.first([]) + assert_equal nil, @filters.last([]) + end + + + + + + + + + +end + diff --git a/vendor/plugins/liquid/test/standard_tag_test.rb b/vendor/plugins/liquid/test/standard_tag_test.rb new file mode 100644 index 0000000..2e49173 --- /dev/null +++ b/vendor/plugins/liquid/test/standard_tag_test.rb @@ -0,0 +1,385 @@ +require File.dirname(__FILE__) + '/helper' + + +class StandardTagTest < Test::Unit::TestCase + include Liquid + + + def test_tag + tag = Tag.new('tag', [], []) + assert_equal 'liquid::tag', tag.name + assert_equal '', tag.render(Context.new) + end + + def test_no_transform + assert_template_result('this text should come out of the template without change...', + 'this text should come out of the template without change...') + assert_template_result('blah','blah') + assert_template_result('','') + assert_template_result('|,.:','|,.:') + assert_template_result('','') + + text = %|this shouldnt see any transformation either but has multiple lines + as you can clearly see here ...| + assert_template_result(text,text) + end + + def test_has_a_block_which_does_nothing + assert_template_result(%|the comment block should be removed .. right?|, + %|the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?|) + + assert_template_result('','{%comment%}{%endcomment%}') + assert_template_result('','{%comment%}{% endcomment %}') + assert_template_result('','{% comment %}{%endcomment%}') + assert_template_result('','{% comment %}{% endcomment %}') + assert_template_result('','{%comment%}comment{%endcomment%}') + assert_template_result('','{% comment %}comment{% endcomment %}') + + assert_template_result('foobar','foo{%comment%}comment{%endcomment%}bar') + assert_template_result('foobar','foo{% comment %}comment{% endcomment %}bar') + assert_template_result('foobar','foo{%comment%} comment {%endcomment%}bar') + assert_template_result('foobar','foo{% comment %} comment {% endcomment %}bar') + + assert_template_result('foo bar','foo {%comment%} {%endcomment%} bar') + assert_template_result('foo bar','foo {%comment%}comment{%endcomment%} bar') + assert_template_result('foo bar','foo {%comment%} comment {%endcomment%} bar') + + assert_template_result('foobar','foo{%comment%} + {%endcomment%}bar') + end + + def test_for + assert_template_result(' yo yo yo yo ','{%for item in array%} yo {%endfor%}','array' => [1,2,3,4]) + assert_template_result('yoyo','{%for item in array%}yo{%endfor%}','array' => [1,2]) + assert_template_result(' yo ','{%for item in array%} yo {%endfor%}','array' => [1]) + assert_template_result('','{%for item in array%}{%endfor%}','array' => [1,2]) + expected = < [1,2,3]) + end + + def test_for_with_range + assert_template_result(' 1 2 3 ','{%for item in (1..3) %} {{item}} {%endfor%}') + end + + def test_for_with_variable + assert_template_result(' 1 2 3 ','{%for item in array%} {{item}} {%endfor%}','array' => [1,2,3]) + assert_template_result('123','{%for item in array%}{{item}}{%endfor%}','array' => [1,2,3]) + assert_template_result('123','{% for item in array %}{{item}}{% endfor %}','array' => [1,2,3]) + assert_template_result('abcd','{%for item in array%}{{item}}{%endfor%}','array' => ['a','b','c','d']) + assert_template_result('a b c','{%for item in array%}{{item}}{%endfor%}','array' => ['a',' ','b',' ','c']) + assert_template_result('abc','{%for item in array%}{{item}}{%endfor%}','array' => ['a','','b','','c']) + end + + def test_for_helpers + assigns = {'array' => [1,2,3] } + assert_template_result(' 1/3 2/3 3/3 ','{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',assigns) + assert_template_result(' 1 2 3 ','{%for item in array%} {{forloop.index}} {%endfor%}',assigns) + assert_template_result(' 0 1 2 ','{%for item in array%} {{forloop.index0}} {%endfor%}',assigns) + assert_template_result(' 2 1 0 ','{%for item in array%} {{forloop.rindex0}} {%endfor%}',assigns) + assert_template_result(' 3 2 1 ','{%for item in array%} {{forloop.rindex}} {%endfor%}',assigns) + assert_template_result(' true false false ','{%for item in array%} {{forloop.first}} {%endfor%}',assigns) + assert_template_result(' false false true ','{%for item in array%} {{forloop.last}} {%endfor%}',assigns) + end + + def test_for_and_if + assigns = {'array' => [1,2,3] } + assert_template_result(' yay ', + '{%for item in array%} {% if forloop.first %}yay{% endif %} {%endfor%}', + assigns) + assert_template_result(' yay boo boo ', + '{%for item in array%} {% if forloop.first %}yay{% else %}boo{% endif %} {%endfor%}', + assigns) + assert_template_result(' boo boo ', + '{%for item in array%} {% if forloop.first %}{% else %}boo{% endif %} {%endfor%}', + assigns) + end + + def test_limiting + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} + assert_template_result('12','{%for i in array limit:2 %}{{ i }}{%endfor%}',assigns) + assert_template_result('1234','{%for i in array limit:4 %}{{ i }}{%endfor%}',assigns) + assert_template_result('3456','{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}',assigns) + assert_template_result('3456','{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}',assigns) + + assigns['limit'] = 2 + assigns['offset'] = 2 + assert_template_result('34','{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}',assigns) + end + + def test_nested_for + assigns = {'array' => [[1,2],[3,4],[5,6]] } + assert_template_result('123456','{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}',assigns) + end + + def test_offset_only + assigns = {'array' => [1,2,3,4,5,6,7,8,9,0]} + assert_template_result('890','{%for i in array offset:7 %}{{ i }}{%endfor%}',assigns) + end + + def test_pause_resume + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit: 3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 789 + XPCTD + assert_template_result(expected,markup,assigns) + end + + def test_pause_resume_limit + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 7 + XPCTD + assert_template_result(expected,markup,assigns) + end + + def test_pause_resume_BIG_limit + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = <<-MKUP + {%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} + MKUP + expected = <<-XPCTD + 123 + next + 456 + next + 7890 + XPCTD + assert_template_result(expected,markup,assigns) + end + + + def test_pause_resume_BIG_offset + assigns = {'array' => {'items' => [1,2,3,4,5,6,7,8,9,0]}} + markup = %q({%for i in array.items limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} + next + {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}) + expected = %q(123 + next + 456 + next + ) + assert_template_result(expected,markup,assigns) + end + + def test_assign + assigns = {'var' => 'content' } + assert_template_result('var2: var2:content','var2:{{var2}} {%assign var2 = var%} var2:{{var2}}',assigns) + + end + + def test_capture + assigns = {'var' => 'content' } + assert_template_result('content foo content foo ','{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', assigns) + end + + def test_capture_detects_bad_syntax + assert_raise(SyntaxError) do + assert_template_result('content foo content foo ','{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', {'var' => 'content' }) + end + end + + def test_case + assigns = {'condition' => 2 } + assert_template_result(' its 2 ','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) + + assigns = {'condition' => 1 } + assert_template_result(' its 1 ','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) + + assigns = {'condition' => 3 } + assert_template_result('','{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) + + assigns = {'condition' => "string here" } + assert_template_result(' hit ','{% case condition %}{% when "string here" %} hit {% endcase %}', assigns) + + assigns = {'condition' => "bad string here" } + assert_template_result('','{% case condition %}{% when "string here" %} hit {% endcase %}', assigns) + end + + def test_case_with_else + + assigns = {'condition' => 5 } + assert_template_result(' hit ','{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) + + assigns = {'condition' => 6 } + assert_template_result(' else ','{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) + + assigns = {'condition' => 6 } + assert_template_result(' else ','{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', assigns) + + + end + + def test_case_on_size + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []) + assert_template_result('1' , '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]) + assert_template_result('2' , '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]) + assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]) + end + + def test_case_on_size_with_else + assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []) + assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]) + assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]) + assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]) + assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]) + assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]) + end + + def test_case_on_length_with_else + assert_template_result('else', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) + assert_template_result('false', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) + assert_template_result('true', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) + assert_template_result('else', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) + end + + def test_assign_from_case + # Example from the shopify forums + code = %q({% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}) + template = Liquid::Template.parse(code) + assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-jackets'}) + assert_equal "menswear", template.render("collection" => {'handle' => 'menswear-t-shirts'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'x'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'y'}) + assert_equal "womenswear", template.render("collection" => {'handle' => 'z'}) + end + + def test_case_when_or + code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) + assert_template_result(' its 4 ', code, {'condition' => 4 }) + assert_template_result('', code, {'condition' => 5 }) + + code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) + assert_template_result('', code, {'condition' => 'something else' }) + end + + def test_case_when_comma + code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 2 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 3 }) + assert_template_result(' its 4 ', code, {'condition' => 4 }) + assert_template_result('', code, {'condition' => 5 }) + + code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 1 }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => 'string' }) + assert_template_result(' its 1 or 2 or 3 ', code, {'condition' => nil }) + assert_template_result('', code, {'condition' => 'something else' }) + end + + def test_assign + assert_equal 'variable', Liquid::Template.parse( '{% assign a = "variable"%}{{a}}' ).render + end + + def test_assign_is_global + assert_equal 'variable', Liquid::Template.parse( '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' ).render + end + + def test_case_detects_bad_syntax + assert_raise(SyntaxError) do + assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {}) + end + + assert_raise(SyntaxError) do + assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) + end + + end + + + + def test_cycle + + assert_template_result('one','{%cycle "one", "two"%}') + assert_template_result('one two','{%cycle "one", "two"%} {%cycle "one", "two"%}') + + assert_template_result('one two one','{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') + end + + def test_multiple_cycles + assert_template_result('1 2 1 1 2 3 1','{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}') + end + + def test_multiple_named_cycles + assert_template_result('one one two two one one','{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}') + end + + def test_multiple_named_cycles_with_names_from_context + assigns = {"var1" => 1, "var2" => 2 } + assert_template_result('one one two two one one','{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) + end + + def test_size_of_array + assigns = {"array" => [1,2,3,4]} + assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) + end + + def test_size_of_hash + assigns = {"hash" => {:a => 1, :b => 2, :c=> 3, :d => 4}} + assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) + end + + def test_illegal_symbols + assert_template_result('', '{% if true == empty %}?{% endif %}', {}) + assert_template_result('', '{% if true == null %}?{% endif %}', {}) + assert_template_result('', '{% if empty == true %}?{% endif %}', {}) + assert_template_result('', '{% if null == true %}?{% endif %}', {}) + end + + def test_ifchanged + assigns = {'array' => [ 1, 1, 2, 2, 3, 3] } + assert_template_result('123','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + + assigns = {'array' => [ 1, 1, 1, 1] } + assert_template_result('1','{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}',assigns) + end +end diff --git a/vendor/plugins/liquid/test/statements_test.rb b/vendor/plugins/liquid/test/statements_test.rb new file mode 100644 index 0000000..63a4104 --- /dev/null +++ b/vendor/plugins/liquid/test/statements_test.rb @@ -0,0 +1,137 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class StatementsTest < Test::Unit::TestCase + include Liquid + + + def test_true_eql_true + text = %| {% if true == true %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_true_not_eql_true + text = %| {% if true != true %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_true_lq_true + text = %| {% if 0 > 0 %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_one_lq_zero + text = %| {% if 1 > 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_one + text = %| {% if 0 < 1 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_or_equal_one + text = %| {% if 0 <= 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lq_or_equal_one_involving_nil + text = %| {% if null <= 0 %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + + + text = %| {% if 0 <= null %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_zero_lqq_or_equal_one + text = %| {% if 0 >= 0 %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_strings + text = %| {% if 'test' == 'test' %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render + end + + def test_strings_not_equal + text = %| {% if 'test' != 'test' %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render + end + + def test_var_strings_equal + text = %| {% if var == "hello there!" %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + def test_var_strings_are_not_equal + text = %| {% if "hello there!" == var %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + def test_var_and_long_string_are_equal + text = %| {% if var == 'hello there!' %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + + def test_var_and_long_string_are_equal_backwards + text = %| {% if 'hello there!' == var %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 'hello there!') + end + + #def test_is_nil + # text = %| {% if var != nil %} true {% else %} false {% end %} | + # @template.assigns = { 'var' => 'hello there!'} + # expected = %| true | + # assert_equal expected, @template.parse(text) + #end + + def test_is_collection_empty + text = %| {% if array == empty %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('array' => []) + end + + def test_is_not_collection_empty + text = %| {% if array == empty %} true {% else %} false {% endif %} | + expected = %| false | + assert_equal expected, Template.parse(text).render('array' => [1,2,3]) + end + + def test_nil + text = %| {% if var == nil %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => nil) + + text = %| {% if var == null %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => nil) + end + + def test_not_nil + text = %| {% if var != nil %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 1 ) + + text = %| {% if var != null %} true {% else %} false {% endif %} | + expected = %| true | + assert_equal expected, Template.parse(text).render('var' => 1 ) + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/strainer_test.rb b/vendor/plugins/liquid/test/strainer_test.rb new file mode 100644 index 0000000..45d8f42 --- /dev/null +++ b/vendor/plugins/liquid/test/strainer_test.rb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class StrainerTest < Test::Unit::TestCase + include Liquid + + def test_strainer + strainer = Strainer.create(nil) + assert_equal false, strainer.respond_to?('__test__') + assert_equal false, strainer.respond_to?('test') + assert_equal false, strainer.respond_to?('instance_eval') + assert_equal false, strainer.respond_to?('__send__') + assert_equal true, strainer.respond_to?('size') # from the standard lib + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/template_test.rb b/vendor/plugins/liquid/test/template_test.rb new file mode 100644 index 0000000..d96d274 --- /dev/null +++ b/vendor/plugins/liquid/test/template_test.rb @@ -0,0 +1,26 @@ +require File.dirname(__FILE__) + '/helper' + +class TemplateTest < Test::Unit::TestCase + include Liquid + + def test_tokenize_strings + assert_equal [' '], Template.new.send(:tokenize, ' ') + assert_equal ['hello world'], Template.new.send(:tokenize, 'hello world') + end + + def test_tokenize_variables + assert_equal ['{{funk}}'], Template.new.send(:tokenize, '{{funk}}') + assert_equal [' ', '{{funk}}', ' '], Template.new.send(:tokenize, ' {{funk}} ') + assert_equal [' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], Template.new.send(:tokenize, ' {{funk}} {{so}} {{brother}} ') + assert_equal [' ', '{{ funk }}', ' '], Template.new.send(:tokenize, ' {{ funk }} ') + end + + def test_tokenize_blocks + assert_equal ['{%comment%}'], Template.new.send(:tokenize, '{%comment%}') + assert_equal [' ', '{%comment%}', ' '], Template.new.send(:tokenize, ' {%comment%} ') + + assert_equal [' ', '{%comment%}', ' ', '{%endcomment%}', ' '], Template.new.send(:tokenize, ' {%comment%} {%endcomment%} ') + assert_equal [' ', '{% comment %}', ' ', '{% endcomment %}', ' '], Template.new.send(:tokenize, " {% comment %} {% endcomment %} ") + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/test_helper.rb b/vendor/plugins/liquid/test/test_helper.rb new file mode 100644 index 0000000..deb8185 --- /dev/null +++ b/vendor/plugins/liquid/test/test_helper.rb @@ -0,0 +1,20 @@ +#!/usr/bin/env ruby +$LOAD_PATH.unshift(File.dirname(__FILE__)+ '/extra') + +require 'test/unit' +require 'test/unit/assertions' +require 'caller' +require 'breakpoint' +require File.dirname(__FILE__) + '/../lib/liquid' + + +module Test + module Unit + module Assertions + include Liquid + def assert_template_result(expected, template, assigns={}, message=nil) + assert_equal expected, Template.parse(template).render(assigns) + end + end + end +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/unless_else_test.rb b/vendor/plugins/liquid/test/unless_else_test.rb new file mode 100644 index 0000000..1c420ba --- /dev/null +++ b/vendor/plugins/liquid/test/unless_else_test.rb @@ -0,0 +1,27 @@ +require File.dirname(__FILE__) + '/helper' + +class UnlessElseTest < Test::Unit::TestCase + include Liquid + + def test_unless + assert_template_result(' ',' {% unless true %} this text should not go into the output {% endunless %} ') + assert_template_result(' this text should go into the output ', + ' {% unless false %} this text should go into the output {% endunless %} ') + assert_template_result(' you rock ?','{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') + end + + def test_unless_else + assert_template_result(' YES ','{% unless true %} NO {% else %} YES {% endunless %}') + assert_template_result(' YES ','{% unless false %} YES {% else %} NO {% endunless %}') + assert_template_result(' YES ','{% unless "foo" %} NO {% else %} YES {% endunless %}') + end + + def test_unless_in_loop + assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] + end + + def test_unless_else_in_loop + assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] + end + +end \ No newline at end of file diff --git a/vendor/plugins/liquid/test/variable_test.rb b/vendor/plugins/liquid/test/variable_test.rb new file mode 100644 index 0000000..4cd1f38 --- /dev/null +++ b/vendor/plugins/liquid/test/variable_test.rb @@ -0,0 +1,135 @@ +#!/usr/bin/env ruby +require File.dirname(__FILE__) + '/helper' + +class VariableTest < Test::Unit::TestCase + include Liquid + + def test_variable + var = Variable.new('hello') + assert_equal 'hello', var.name + end + + def test_filters + var = Variable.new('hello | textileze') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]]], var.filters + + var = Variable.new('hello | textileze | paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + + var = Variable.new(%! hello | strftime: '%Y'!) + assert_equal 'hello', var.name + assert_equal [[:strftime,["'%Y'"]]], var.filters + + var = Variable.new(%! 'typo' | link_to: 'Typo', true !) + assert_equal %!'typo'!, var.name + assert_equal [[:link_to,["'Typo'", "true"]]], var.filters + + var = Variable.new(%! 'typo' | link_to: 'Typo', false !) + assert_equal %!'typo'!, var.name + assert_equal [[:link_to,["'Typo'", "false"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3, 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3","3"]]], var.filters + + var = Variable.new(%! 'foo' | repeat: 3, 3, 3 !) + assert_equal %!'foo'!, var.name + assert_equal [[:repeat,["3","3","3"]]], var.filters + + var = Variable.new(%! hello | strftime: '%Y, okay?'!) + assert_equal 'hello', var.name + assert_equal [[:strftime,["'%Y, okay?'"]]], var.filters + + var = Variable.new(%! hello | things: "%Y, okay?", 'the other one'!) + assert_equal 'hello', var.name + assert_equal [[:things,["\"%Y, okay?\"","'the other one'"]]], var.filters + end + + def test_filter_with_date_parameter + + var = Variable.new(%! '2006-06-06' | date: "%m/%d/%Y"!) + assert_equal "'2006-06-06'", var.name + assert_equal [[:date,["\"%m/%d/%Y\""]]], var.filters + + end + + def test_filters_without_whitespace + var = Variable.new('hello | textileze | paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + + var = Variable.new('hello|textileze|paragraph') + assert_equal 'hello', var.name + assert_equal [[:textileze,[]], [:paragraph,[]]], var.filters + end + + def test_symbol + var = Variable.new("http://disney.com/logo.gif | image: 'med' ") + assert_equal 'http://disney.com/logo.gif', var.name + assert_equal [[:image,["'med'"]]], var.filters + end + + def test_string_single_quoted + var = Variable.new(%| "hello" |) + assert_equal '"hello"', var.name + end + + def test_string_double_quoted + var = Variable.new(%| 'hello' |) + assert_equal "'hello'", var.name + end + + def test_integer + var = Variable.new(%| 1000 |) + assert_equal "1000", var.name + end + + def test_float + var = Variable.new(%| 1000.01 |) + assert_equal "1000.01", var.name + end + + def test_string_with_special_chars + var = Variable.new(%| 'hello! $!@.;"ddasd" ' |) + assert_equal %|'hello! $!@.;"ddasd" '|, var.name + end + + def test_string_dot + var = Variable.new(%| test.test |) + assert_equal 'test.test', var.name + end +end + + +class VariableResolutionTest < Test::Unit::TestCase + include Liquid + + def test_simple_variable + template = Template.parse(%|{{test}}|) + assert_equal 'worked', template.render('test' => 'worked') + assert_equal 'worked wonderfully', template.render('test' => 'worked wonderfully') + end + + def test_simple_with_whitespaces + template = Template.parse(%| {{ test }} |) + assert_equal ' worked ', template.render('test' => 'worked') + assert_equal ' worked wonderfully ', template.render('test' => 'worked wonderfully') + end + + def test_ignore_unknown + template = Template.parse(%|{{ test }}|) + assert_equal '', template.render + end + + def test_hash_scoping + template = Template.parse(%|{{ test.test }}|) + assert_equal 'worked', template.render('test' => {'test' => 'worked'}) + end + +end \ No newline at end of file diff --git a/vendor/plugins/ym4r_gm/README b/vendor/plugins/ym4r_gm/README new file mode 100644 index 0000000..b1f0248 --- /dev/null +++ b/vendor/plugins/ym4r_gm/README @@ -0,0 +1,391 @@ +=YM4R/GM plugin for Rails + +This is the latest version of the YM4R/GM plugin for Rails (YM4RGMP4R?). Its aim is to facilitate the use of Google Maps from Rails application. It includes and enhances all the web application-related functionalities found in the YM4R gem as of version 0.4.1. + +==Getting Started +I present here the most common operations you would want to do with YM4R/GM. Read the rest of the documents if you want more details. + +In your controller, here is a typical initialization sequence in action +index+: + def index + @map = GMap.new("map_div") + @map.control_init(:large_map => true,:map_type => true) + @map.center_zoom_init([75.5,-42.56],4) + @map.overlay_init(GMarker.new([75.6,-42.467],:title => "Hello", :info_window => "Info! Info!")) + end + +Here I create a GMap (which will be placed inside a DIV of id +map_div+), add controls (large zoom slider and pan cross + map type selector), set the center and the zoom and add a marker. Of these 4 steps only the creation of the map and the setting of the center and the zoom are absolutely necessary. + +In your view, here is what you would have: + Test + <%= GMap.header %> + <%= @map.to_html %> + + <%= @map.div(:width => 600, :height => 400) %> + + +First you must output the header, used by all the maps of the page: It includes the Google Maps API JS code and the JS helper functions of YM4R/GM. Then we initialize the map by calling @map.to_html. In the body, we need a DIV whose +id+ is the same as the one passed to the GMap constructor in the controller. The call to @map.div outputs such a DIV. We pass to it options to set the width and height of the map in the style attribute of the DIV. + +Note that you need to set a size for the map DIV element at some point or the map will not be displayed. You have a few ways to do this: +- You define it yourself, wherever you want. Usually as part of the layout definition in an external CSS. +- In the head of the document, through a CSS instruction output by @map.header_width_height, to which you pass 2 arguments (width and height). +- When outputting the DIV with @map.div, you can also pass an option hash, with keys :width and :height and a style attribute for the DIV element will be output. + +You can update the map trough RJS. Here is an action you can call from a link_remote_tag which would do this: + def update + @map = Variable.new("map") + @marker = GMarker.new([75.89,-42.767],:title => "Update", :info_window => "I have been placed through RJS") + end + +Here, I first bind the Ruby @map variable to the JS variable map, which already exists in the client browser. +map+ is by default the name given to a map created from YM4R/GM (this could be overriden by passing a second argument to the GMap constructor). Then I create a marker. + +And you would have inside the RJS template for the action: + page << @map.clear_overlays + page << @map.add_overlay(@marker) + +Here I first clear the map of all overlays. Then I add the marker. Note that the +overlay_init+ is not used anymore since, as its name indicates, this method is only for the initialization of the map. + +==Relation between the YM4R gem and the YM4R/GM plugin +They are completely independent from each other. + +With the plugin, you don't need the YM4R gem anymore, unless you want to use the tilers or the Ruby helpers for the Yahoo! Maps Building Block API's and the Google Maps geocoding API. Please refer to the documentation of the YM4R gem to know more about the functionalities in it. + +Conversely, the YM4R gem does not need the plugin to work. + +==Installation +Install like any other Rails plugin: + ruby script/plugin install svn://rubyforge.org/var/svn/ym4r/Plugins/GM/trunk/ym4r_gm + +As part of the installation procedure, the JavaScript files found in the PLUGIN_ROOT/javascript directory will be copied to the RAILS_ROOT/public/javascripts/ directory. + +Moreover a gmaps_api_key.yml file will also be created in the RAILS_ROOT/config/ folder. If this file already exists (installed for example by a previous version of the plugin), nothing will be done to it, so you can keep your configuration data even after updating the plugin. This file is a YAML representation of a hash, similar to the database.yml file in that the primary keys in the file are the different environments (development, test, production), since you will probably need to have different Google Maps API keys depending on that criteria (for example: a key for localhost for development and test; a key for your host for production). If you don't have any special need, there will be only one key associated with each environment. If however, you need to have multiple keys (for example if your app will be accessible from multiple URLs, for which you need different keys), you can also associate a hash to the environment, whose keys will be the different hosts. In this case, you will need to pass a value to the :host key when calling the method GMap.header (usually @request.host). + +==Migration from the YM4R gem versions <= 0.4.1 +Apart from the installation of the plugin detailed above, you will also need to delete the instructions to require the file and include the Ym4r::GoogleMaps module in your controllers: + require 'ym4r' + include Ym4r::GoogleMaps +This lines are now not needed since the plugin is loaded as part of the normal Rails loading procedure and the module is included when the plugin is loaded. + +==Operations +You can use the library to display Google maps easily from Rails. The version of the API used is v2. The library is engineered so updates to the map through RJS/Ajax are possible. I have made available 2 in-depth tutorials to show some of the functionalities of the library and how it can be integrated with GeoRuby and the Spatial Adapter for Rails: +- http://thepochisuperstarmegashow.com/2006/06/02/ym4r-georuby-spatial-adapter-demo/ +- http://thepochisuperstarmegashow.com/2006/06/03/google-maps-yahoo-traffic-mash-up/ +Following is some notes about manipulating Google Maps with the library: + +===Naming conventions +The names of the Ruby class follow the ones in the JavaScript Google Maps API v2, except for GMap2, which in Ruby code is called simply GMap. To know what is possible to do with each class, you should refer to the documentation available on Google website. + +On top of that, you have some convenience methods for initializing the map (in the GMap class). Also, the constructors of some classes accept different types of arguments to be converted later in the correct JavaScript format. For example, the +GMarker+ aclass accepts an array of 2 floats as parameter, in addition of a GLatLng object, to indicate its position. It also facilitates the attribution of an HTML info window, displayed when the user clicks on it, since you can pass to the constructor an options hash with the :info_window key and the text to display as the value, instead of having to wire the response to a click event yourself. + +===Binding JavaScript and Ruby +Since the Google Maps API uses JavaScript to create and manipulate a map, most of what the library does is outputting JavaScript, although some convenience methods are provided to simplify some common operations at initialization time. When you create a YM4R mapping object (a Ruby object which includes the MappingObject module) and call methods on it, these calls are converted by the library into JavaScript code. At initialization time, you can pass arbitrary JavaScript code to the GMap#record_init and GMap#record_global_init.Then, at update time, if you use Ruby-on-Rails as your web framework, you can update your map through RJS by passing the result of the method calls to the page << method to have it then interpreted by the browser. + +For example, here is a typical initialization sequence for a map + @map = GMap.new("map_div") + @map.control_init(:large_map => true,:map_type => true) + @map.center_zoom_init([35.12313,-110.567],12) + @map.overlay_init GMarker.new([35.12878, -110.578],:title => "Hello!") + @map.record_init @map.add_overlay(GMarker.new([35.12878, -110.578],:title => "Hello!")) + +While +center_zoom_init+, +control_init+ or +overlay_init+ (and generally all the GMap methods which end in +init+) are one of the rare convenience methods that do not output JavaScript, the +add_overlay+ does. Actually, if you look at the code of the GMap class, you won't find any +add_overlay+ method, although in the documentation of the GMap2 class from the Google Maps API documentation, you will find something about the +addOverlay+ JavaScript method. In fact, when you call on a mapping object an unknow method, it is converted to a javascriptified version of it, along with its arguments, and a string of JavaScript code is output. So the @map.add_overlay... above is converted to "map.addOverlay(new GMarker(GLatLng.new(35.12878, -110.578),{title:\"Hello!\"}))", which is then passed to the +record_init+ method of a Ruby GMap object to be later output along with the rest of the initialization code. Any arbitrary JavaScript code can be passed to the +record_init+ method. Note that 2 last lines of the previous code sample are strictly equivalent and since the +overlay_init+ version is a bit simpler, it should be preferred. + +===Initialization of the map +The map is represented by a GMap object. You need to pass to the constructor the id of a DIV that will contain the map. You have to place this DIV yourself in your HTML template. You can also optionnally pass to the constructor the JavaScript name of the variable that will reference the map, which by default will be global in JavaScript. You have convenience methods to setup the controls, the center, the zoom, overlays, the interface configuration (continuous zoom, double click zoom, dragging), map types and the icons (which are also global). You can also pass arbitrary JavaScript to +record_init+ and +record_global_init+. Since, by default, the initialization of the map is performed in a callback function, if you want to have a globally accessible variable, you need to use the +global+ version. + +You can also have multiple maps. Just make sure you give them different DIV id's, as well as different global variable names, when constructing them: + @map1 = GMap("map1_div","map1") + @map2 = GMap("map2_div","map2") + +The other absolutely necessary initialization step in the controller is the setting of center and zoom: + @map1.center_zoom_init([49.12,-56.453],3) +Withouth this code the map will display an empty grey rectangle with Google's logo. + +Then in your template, you have 2 necessary calls: +- The static GMap.header: Outputs the inclusion of the JavaScript file from Google to make use of the Google Maps API and by default a style declaration for VML objects, necessary to display polylines under IE. This default can be overriddent by passing :with_vml => false as option to the +header+ method. You can also pass to this method a :host option in order to select the correct key for the location where your app is currently deployed, in case the current environment has multiple possible keys. Usually, in this case, you should pass it @request.host. Finally you can override all the key settings in the configuration by passing a value to the :key key. +- GMap#to_html: Outputs the initialization code of the map. By default, it outputs the +script+ tags and initializes the map in response to the onload event of the JavaScript window object. You can call +to_html+ on each one of your maps to have them all initialized. You can pass the option :full=>true to the method to setup a fullscreen map, which will also be resized when the window size changes. + +You can also use GMap#div to output the div element with the correct +id+. You can pass it options :height and :width to set the size of the div (although, as indicated below, you have other ways to do that). + +So you should have something like the following: + Hello + <%= GMap.header %> + <%= @map.to_html %> + + <%= @map.div(:width => 600,:height => 400) %> + + +Note that you need to set a size for the map DIV element at some point or the map will not be displayed. You have a few ways to do this: +- You define it yourself, wherever you want. Usually as part of the layout definition in an external CSS. +- In the head of the document, through a CSS instruction output by @map.header_width_height, to which you pass 2 arguments (width and height). +- When outputting the DIV with @map.div, you can also pass an option hash, with keys :width and :height and a style attribute for the DIV element will be output. + +===GMarkers +GMarkers are point of interests on a map. You can give a position to a GMarker constructor either by passing it a 2D-array of coordinates, a GLatLng object, an object of type Variable (which evaluates to a GLatLng when interpreted in the browser) or an address, which will be geocoded when the marker is initialized by the map. + +You can pass options to the GMarker to customize the info window (:info_window or :info_window_tabs options), the tooltip (:title option) or the icon used (:icon option). + +For example: + GMarker.new([12.4,65.6],:info_window => "I'm a Popup window",:title => "Tooltip") + GMarker.new(GLatLng.new([12.3,45.6])) + GMarker.new("Rue Clovis Paris France", :title => "geocoded") + + +===Update of the map +You are able to update the map through Ajax. In Ruby-on-Rails, you have something called RJS, which sends JavaScript created on the server as a response to an action, which is later interpreted by the browser. It is usually used for visual effects and replacing or adding elements. It can also accept arbitrary JavaScript and this is what YM4R uses. + +For example, if you want to add a marker to the map, you need to do a few things. First, you have to bind a Ruby mapping object to the global JavaScript map variable. By default its name is +map+, but you could have overriden that at initialization time. You need to do something like this: + @map = Variable.new("map") ++map+ in the Variable constructor is the name of the global JavaScript map variable. Then any method you call on @map will be converted in JavaScript to a method called on +map+. In your RJS code, you would do something like this to add a marker: + page << @map.add_overlay(GMarker.new([12.1,12.76],:title => "Hello again!")) +What is sent to the browser will be the fllowing JavaScript code: + map.addOverlay(new GMarker(new GLatLng(123123.1,12313.76),{title:\"Hello again!\"})) + + +===GPolyline +GPolylines are colored lines on the map. The constructor takes as argument a list of GLatLng or a list of 2-element arrays, which will be transformed into GLatLng for you. It can also take the color (in the #rrggbb form), the weight (an integer) and the opacity. These arguments are optional though. + +For example: + GPolyline.new([[12.4,65.6],[4.5,61.2]],"#ff0000",3,1.0) + +Then you add it like any other overlay: + @map.overlay_init(polyline) + +===GPolygon +GPolygons are colored areas on the map. As of 29/12, this feature is not documented in the official GMaps API, but thanks to Steven Talcott Smith (http://www.talcottsystems.com), it is possible to use it now in ym4r. + +The constructor takes as argument a list of GLatLng or a list of 2-element arrays, which will be transformed into GLatLng for you. Note that for polygons, the last point must be equal to the first, in order to have a closed loop. It can also take the color (in the #rrggbb form) of the stroke, the weight of the stroke, the opacity of the stroke, as well as the color of the fill and the opacity. These arguments are optional though. + +For example: + GPolygon.new([[12.4,6.6],[4.5,1.2],[-5.6,-12.4],[12.4,6.6]],"#ff0000",3,1.0,"#00ff00",1.0) + +Then you add it like any other overlay: + @map.overlay_init(polygon) + +===GMarkerGroup +A new type of GOverlay is available, called GMarkerGroup. + +To use it you would have to include in your HTML template the JavaScript file markerGroup.js after the call to GMap.header (because it extends the GOverlay class). You should have something like that in your template: + <%= javascript_include_tag("markerGroup") %> + +It is useful in 2 situations: +- Display and undisplay a group of markers without referencing all of them. You just declare your marker group globally and call +activate+ and +deactivate+ on this group in response, for example, to clicks on links on your page. +- Keeping an index of markers, for example, in order to show one of these markers in reponse to a click on a link (the way Google Local does with the results of the search). + +Here is how you would use it from your Ruby code: + @map = GMap.new("map_div") + marker1 = GMarker.new([123.55,123.988],:info_window => "Hello from 1!") + marker2 = GMarker.new([87.123,18.9],:info_window =>"Hello from 2!") + @map.overlay_global_init(GMarkerGroup.new(true, + 1 => marker1, + 2 => marker2),"myGroup") +Here I have created an active (ie which is going to be displayed when the map is created) marker group called +myGroup+ with 2 markers, which I want to reference later. If I did not want to reference them later (I just want to be able to display and undisplay the marker group), I could have passed an array instead of the hash. + +Then in your template, you could have that: + Click here to display marker1 + Click here to display marker2 + <%= @map.div %> +When you click on one of the links, the corresponding marker has its info window displayed. + +You can call +activate+ and +deactivate+ to display or undisplay a group of markers. You can add new markers with addMarker(marker,id). Again if you don't care about referencing the marker, you don't need to pass an id. If the marker group is active, the newly added marker will be displayed immediately. Otherwise it will be displayed the next time the group is activated. Finally, since it is an overlay, the group will be removed when calling clearOverlays on the GMap object. + +You can center and zoom on all the markers in the group by calling GMarkerGroup#centerAndZoomOnMarkers() after the group has been added to a map. So for example, if you would want to do that at initalization time, you would do the following (assuming your marker group has been declared as +group+): + @map.record_init group.center_and_zoom_on_markers + +===GMarkerManager +It is a recent (v2.67) GMaps API class that manages the efficient display of potentially thousands of markers. It is similar to the Clusterer (see below) since markers start appearing at specified zoom levels. The clustering behaviour has to be managed explicitly though by specifying the cluster for smaller zoom levels and specify the expanded cluster for larger zoom levels and so on. Note that it is not an overlay and is not added to the map through an overlay_init call. + +Here is an example of use: + @map = GMap.new("map_div") + @map.control_init(:large_map => true, :map_type => true) + @map.center_zoom_init([59.91106, 10.72223],3) + srand 1234 + markers1 = [] + 1.upto(20) do |i| + markers1 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-20-#{i}") + end + managed_markers1 = ManagedMarker.new(markers1,0,7) + + markers2 = [] + 1.upto(200) do |i| + markers2 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-200-#{i}") + end + managed_markers2 = ManagedMarker.new(markers2,8,9) + + markers3 = [] + 1.upto(1000) do |i| + markers3 << GMarker.new([59.91106 + 6 * rand - 3, 10.72223 + 6 * rand - 3],:title => "OY-300-#{i}") + end + managed_markers3= ManagedMarker.new(markers3,10) + + mm = GMarkerManager.new(@map,:managed_markers => [managed_markers1,managed_markers2,managed_markers3]) + @map.declare_init(mm,"mgr") + +===Clusterer +A Clusterer is a type of overlay that contains a potentially very large group of markers. It is engineered so markers close to each other and undistinguishable from each other at some level of zoom, appear as only one marker on the map at that level of zoom. Therefore it is possible to have a very large number of markers on the map at the same time and not having the map crawl to a halt in order to display them. It has been slightly modified from Jef Poskanzer's original Clusterer2 code (see http://www.acme.com/javascript/ for the original version). The major difference with that version is that, in YM4R, the clusterer is a GOverlay and can therefore be added to the map with map.addOverlay(clusterer) and is cleared along with the rest of overlays when map.clearOverlays() is called. + +In order to use a clusterer, you should include in your template page the clusterer.js file after the call to GMap.header (because it extends the GOverlay class). You should have something like that in your template: + <%= javascript_include_tag("clusterer") %> + +To create a clusterer, you should first create an array of all the GMarkers that you want to include in the clusterer (you can still add more after the clusterer has been added to the map though). When GMarkers close together are grouped in one cluster (represented by another marker on the map) and the user clicks on the cluster marker, a list of markers in the cluster is displayed in the info windo with a description: By default it is equal to the +title+ of the markers (which is also displayed as a tooltip when hovering on the marker with the mouse). If you don't want that, you can also pass to the GMarker constructor a key-value pair with key :description to have a description different from the title. For example, here is how you would create a clusterer: + markers = [GMarker.new([37.8,-90.4819],:info_window => "Hello",:title => "HOYOYO"), + GMarker.new([37.844,-90.47819],:info_window => "Namaste",:description => "Chopoto" , :title => "Ay"), + GMarker.new([37.83,-90.456619],:info_window => "Bonjour",:title => "Third"), + ] + clusterer = Clusterer.new(markers,:max_visible_markers => 2) +Of course, with only 3 markers, the whole clusterer thing is totally useless. Usually, using the clusterer starts being interesting with hundreds of markers. The options to pass the clusterer are: +- :max_visible_markers: Below this number of markers, no clustering is performed. Defaults to 150. +- :grid_size: The clusterer divides the visible area into a grid of this size. Defaults to 5. +- :min_markers_per_cluster: Below this number of markers a cluster is not formed. Defaults to 5. +- :max_lines_per_info_box: Number of lines to display in the info window displayed when a cluster is clicked on by the user. Defaults to 10. +- :icon: Icon to be used to mark a cluster. Defaults to G_DEFAULT_ICON (the classic red marker). + +Then to add the clusterer to the map at initialization time, you proceed as with a classic overlay: + @map.overlay_init clusterer + +To add new markers in RJS (if the clusterer has been declared globally with overlay_global_init), you should do this: + page << clusterer.add_marker(marker,description) +In this case, the :description passed to the GMarker contructor will not be taken into account. Only the +description+ passed in the call to +add_marker+ will. + +===GeoRss Overlay +An group of markers taken from a Rss feed containing location information in the W3C basic geo (WGS83 lat/lon) vocabulary and in the Simple GeoRss format. See http://georss.org for more details. The support for GeoRss relies on the MGeoRSS library by Mikel Maron (http://brainoff.com/gmaps/mgeorss.html), although a bit modified, mostly to have the GeoRssOverlay respect the GOverlay API. It has also been enhanced by Andrew Turner who added support for the GeoRss Simple format. + +Note that the geoRssOverlay.js file must be included in the HTML template in order to use this functionality. You should have something like that in your template: + <%= javascript_include_tag("geoRssOverlay") %> + +Here is how you would use it. First create the overlay at initialization: + def index + @map = GMap.new("map_div") + @map.control_init(:large_map => true) + @map.center_zoom_init([38.134557,-95.537109],0) + @map.overlay_init(GeoRssOverlay.new(url_for(:action => "earthquake_rss")) + end +Since it is not possible to make requests outside the domain where the current page comes from, there is a need for a proxy. With the GeoRssOverlay initialization above, the request will be made by the earthquake_rss action, where the address to find the RSS feed will be hardwired: + def earthquake_rss + result = Net::HTTP.get_response("earthquake.usgs.gov","/eqcenter/recenteqsww/catalogs/eqs7day-M5.xml") + render :xml => result.body + end +If you don't want to hardwire the RSS feed location in an action, you can. But you will have to pass the :proxy option to the GeoRssOverlay constructor. When requesting the RSS feed, the browser will in fact call the proxy with the actual URL of the RSS feed in the +q+ parameter of the request. Here is how you would initialize the GeoRssOverlay that way: + @map.overlay_init(GeoRssOverlay.new("http://earthquake.usgs.gov/eqcenter/recenteqsww/catalogs/eqs7day-M5.xml", + :proxy => url_for(:action => "proxy"))) +And here is an example of proxy action: + def proxy + result = Net::HTTP.get_response(URI.parse(@params[:q])) + render :xml => result.body + end +You should probably do some checks to ensure the proxy is not abused but it is of your responsibility. + +Another option can be passed to the GeoRssOverlay constructor to set an icon to be used by the markers of the overlay: :icon. By default it is equal to the default icon (the classic red one). + +In the view, you should have something like the following: + + Testing GeoRss + <%= GMap.header(:with_vml => false) %> + <%= javascript_include_tag("geoRssOverlay") %> + <%= @map.header_width_height(600,400) %> + <%= @map.to_html %> + + + <%= @map.div %> + + +Note the inclusion of the geoRssOverlay.js file. + +Other options to pass to the GeoRssOverlay constructor are the following: +- :list_div: In case you want to make a list of all the markers, with a link on which you can click in order to display the info on the marker, use this option to indicate the ID of the DIV (that you must place yourself on your page). +- :list_item_class: class of the DIV containing each item of the list. Ignored if option :list_div is not set. +- :limit: Maximum number of items to display on the map. +- :content_div: Instead of having an info window appear, indicates the ID of the DIV where this info should be displayed. + +===Adding new map types +It is now possible to easily add new map types, on top of the already existing ones, like G_SATELLITE_MAP or G_NORMAL_MAP. The imagery for these new map types can come from layers of the standard map types or can be taken either from a WMS server or from pretiled images on a server (that can be generated with a tool that comes with the YM4R gem: refer to the README of the gem to know more about it). + +For exemple, here is how you would setup layers from a public WMS server of the DMAP team of the American Navy: + layer = WMSLayer.new("http://columbo.nrlssc.navy.mil/ogcwms/servlet/WMSServlet/AccuWeather_Maps.wms", + "20:3,6:3,0:27,0:29,6:19", + :copyright => {'prefix' => "Map Copyright 2006", 'copyright_texts'=> ["DMAP"]}, + :use_geographic => true, :opacity => 0.8) +This sets up a connection to a WMS service, requesting layers 20:3,6:3,0:27,0:29,6:19 (you would have to look at the GetCapabilities document of the service to know what the valid layers are). The copyright notice attributes the data to DMAP. The images will be 80% opaque. For the rest of the options, the default values are used: default styles (:style option), PNG format (:format option), valid for all zoom levels (:zoom_range option). The option :merc_proj is irrelevant here since the :use_geographic option is true. + +The arguments :use_geographic and :merc_proj warrant some explanation. The Google Maps are in the Simple Mercator projection in the WGS84 datum and currently do not support the display of data in projections other than that (at least if you want to display markers and lines on top of it). Unfortunately, different WMS servers do not identify this projection the same way. So you can give to the WMSLayer constructor your server type through the :merc_proj option and it will figure out what is the correct identifier. Currently, this works only for :mapserver (EPSG:54004) and :geoserver (EPSG:41001). For others you can directly pass a number corresponding to the EPSG definition of the simple Mercator projection of your server. On top of that, some servers just don't support the Simple Mercator projection. This is why there is a :use_geographic option, that can be set to +true+. It is in order to tell the WMSLayer that it should request its tiles using LatLon coordinates in the WGS84 datum (which should be supported by any server in a consistant way). Unfortunately it is not perfect since the deformation is quite important for low zoom levels (< 5 for the US). Higher than that, the deformation is not that visible. However, if you control the WMS server, it is recommended that you don't use :use_geographic and instead use the :merc_proj option and setup the Mercator projection in your server if it is not done by default. + +Note that you need to include the wms-gs.js javascript file in your HTML page in order to make use of the WMSLayer functionality. You should have something like that in your template: + <%= javascript_include_tag("wms-gs") %> +This file uses code by John Deck with the participation of others (read the top of the javascript file to know more). + +Here is how to define a pretiled layer: + layer = PreTiledLayer.new("http://localhost:3000/tiles", + :copyright => {'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]}, + :zoom_range => 13..13, :opacity => 0.7, :format => "gif") +I tell the PreTiledLayer constructor where I can find the tiles, setup the Copyright string, the valid zooms for the map, the opacity and the format. Tiles must have standardized names: tile_#{zoom}_#{x_tile}_#{y_tile}.#{format} (here the format is "gif"). You can use tools found in the YM4R gem to generate tiles in this format either from local maps or from WMS servers (useful to create tiles from geographic data files without having to run a map server or to cache images from slow servers). Again refer to the documentation of the gem for more information on how to do this. + +Instead of having the tiles requested directly, you can also decide to have an action on the server which takes care of it. You can used the class PreTiledLayerFromAction for this. In this case, the first argument of the constructor is an url of an action. The arguments +x+, +y+ and +z+ will be passed to it. + layer = PreTiledLayerFromAction.new(url_for(:action => :get_tile), + :copyright => {'prefix' => "Map C 2006", 'copyright_texts' => ["Open Atlas"]}, + :zoom_range => 13..14, :opacity => 0.7) +The other constructor arguments have the same meaning as PreTiledLayer. Here is an uninteresting example of action that serves tiles: + def get_tile + x = @params[:x] + y = @params[:y] + z = @params[:z] + begin + send_file "#{RAILS_ROOT}/public/tiles/tile_#{z}_#{x}_#{y}.png" , + :type => 'image/png', :disposition => 'inline' + rescue Exception + render :nothing => true + end + end + +You can add a layer to a new map type the following way: + map_type = GMapType.new(layer,"My WMS") +This is actually the simplest configuration possible. Your map type has only one data layer and is called "My WMS". You can add more that one layer: Either one that you have created yourself or existing ones. For example: + map_type = GMapType.new([GMapType::G_SATELLITE_MAP.get_tile_layers[0],layer,GMapType::G_HYBRID_MAP.get_tile_layers[1]], + "Test WMS") +Here for the "Test WMS" map type, we also take the first layer of the "Satellite" map type in the background and overlay the second layer of the "Hybrid" map type (roads, country boundaries, etc... transparently overlaid on top of the preceding layers) so when the "Test WMS" map type is selected in the interface, all three layers will be displayed. + +Finally to add a map type to a GMap: + @map.add_map_type_init(map_type) +If you want to wipe out the existing map types (for example the 3 default ones), you can add a +false+ argument to the +add_map_type_init+ method and the +map_type+ will be the only one. + +If you want to setup the map as the default one when the map is initially displayed, you should first declare the map type then add it to the map as indicated above and finally setting it as the default map type: + @map.declare_init(map_type,"my_map_type") + @map.add_map_type_init(map_type) + @map.set_map_type_init(map_type) +Future versions of the plugin may simplify that. + +===Google Geocoding +A helper to perform geocoding on the server side (in Ruby) is included. Here is an example of request: + results = Geocoding::get("Rue Clovis Paris") +You can also pass to the +get+ method an options hash to manage the various API key options (see the section on GMap.header for details). +results+ is an array of Geocoding::Placemark objects, with 2 additional attributes: +status+ and +name+. You should check if +status+ equals Geocoding::GEO_SUCCESS to know if the request has been successful. You can then access the various data elements. + +Here is an example that will display a marker on Paris: + results = Geocoding::get("Rue Clovis Paris") + if results.status == Geocoding::GEO_SUCCESS + coord = results[0].latlon + @map.overlay_init(GMarker.new(coord,:info_window => "Rue Clovis Paris")) + end + +You could also have performed the geocoding on the client side with the following code, which is functionnality equivalent to the code above: + GMarker.new("Rue Clovis Paris",:info_window => "Rue Clovis Paris") + + +==Recent changes +- GMarker can now be placed by address (in addition to coordinates). Some code to geocode the address when the marker is initialized is added +- Addition of a +center_zoom_on_points_init+ to center and zoom on a group of pixel +- In JS, addition of methods to GMap2 and GMarkerGroup to center and zoom on a group of points or markers (thanks to Glen Barnes) +- Support for easy setup of fullscreen maps + +==TODO +- Add support for easy manipulation of external Google Maps-related libraries: Advanced tooltip manipulation (PdMarker),... +- Addition of all GeoRss vocabularies (with all features: polylines...) to the geoRssOverlay extension +- Tutorials + +==Disclaimer +This software is not endorsed in any way by Google. + +==Acknowledgement +The YM4R/GM plugin bundles JavaScript libraries from John Deck (WMS layers on Google Maps), Jef Poskanzer (Clusterer on Google Maps) and Mikel Maron (GeoRss on Google Maps). + +==License +The YM4R/GM plugin is released under the MIT license. The clusterer.js file is redistributed with a different license (but still compatible with the MIT license). Check the top of the file in PLUGIN_ROOT/javascript to know more. + +==Support +Any questions, enhancement proposals, bug notifications or corrections can be sent to mailto:guilhem.vellut+ym4r@gmail.com. diff --git a/vendor/plugins/ym4r_gm/gmaps_api_key.yml.sample b/vendor/plugins/ym4r_gm/gmaps_api_key.yml.sample new file mode 100644 index 0000000..c4cb457 --- /dev/null +++ b/vendor/plugins/ym4r_gm/gmaps_api_key.yml.sample @@ -0,0 +1,14 @@ +#Fill here the Google Maps API keys for your application +#In this sample: +#For development and test, we have only one possible host (localhost:3000), so there is only a single key associated with the mode. +#In production, the app can be accessed through 2 different hosts: thepochisuperstarmegashow.com and exmaple.com. There then needs a 2-key hash. If you deployed to one host, only the API key would be needed (as in development and test). + +development: + ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ + +test: + ABQIAAAAzMUFFnT9uH0xq39J0Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDKaBR6j135zrztfTGVOm2QlWnkaidDIQ + +production: + thepochisuperstarmegashow.com: ABQIAAAAzMUFFnT9uH0Sfg76Y4kbhTJQa0g3IQ9GZqIMmInSLzwtGDmlRT6e90j135zat56yhJKQlWnkaidDIQ + example.com: ABQIAAAAzMUFFnT9uH0Sfg98Y4kbhGFJQa0g3IQ9GZqIMmInSLrthJKGDmlRT98f4j135zat56yjRKQlWnkmod3TB \ No newline at end of file diff --git a/vendor/plugins/ym4r_gm/init.rb b/vendor/plugins/ym4r_gm/init.rb new file mode 100644 index 0000000..7316aef --- /dev/null +++ b/vendor/plugins/ym4r_gm/init.rb @@ -0,0 +1,3 @@ +require 'ym4r_gm' + + diff --git a/vendor/plugins/ym4r_gm/install.rb b/vendor/plugins/ym4r_gm/install.rb new file mode 100644 index 0000000..68c5e39 --- /dev/null +++ b/vendor/plugins/ym4r_gm/install.rb @@ -0,0 +1,10 @@ +require 'fileutils' + +#Copy the Javascript files +FileUtils.copy(Dir[File.dirname(__FILE__) + '/javascript/*.js'], File.dirname(__FILE__) + '/../../../public/javascripts/') + +#copy the gmaps_api_key file +gmaps_config = File.dirname(__FILE__) + '/../../../config/gmaps_api_key.yml' +unless File.exist?(gmaps_config) + FileUtils.copy(File.dirname(__FILE__) + '/gmaps_api_key.yml.sample',gmaps_config) +end diff --git a/vendor/plugins/ym4r_gm/javascript/clusterer.js b/vendor/plugins/ym4r_gm/javascript/clusterer.js new file mode 100644 index 0000000..eeb6dd9 --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/clusterer.js @@ -0,0 +1,444 @@ +// Clusterer.js - marker clustering routines for Google Maps apps +// +// The original version of this code is available at: +// http://www.acme.com/javascript/ +// +// Copyright © 2005,2006 by Jef Poskanzer . +// All rights reserved. +// +// Modified for inclusion into the YM4R library in accordance with the +// following license: +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// +// For commentary on this license please see http://www.acme.com/license.html + + +// Constructor. +Clusterer = function(markers,icon,maxVisibleMarkers,gridSize,minMarkersPerCluster,maxLinesPerInfoBox) { + this.markers = []; + if(markers){ + for(var i =0 ; i< markers.length ; i++){ + this.addMarker(markers[i]); + } + } + this.clusters = []; + this.timeout = null; + + this.maxVisibleMarkers = maxVisibleMarkers || 150; + this.gridSize = gridSize || 5; + this.minMarkersPerCluster = minMarkersPerCluster || 5; + this.maxLinesPerInfoBox = maxLinesPerInfoBox || 10; + + this.icon = icon || G_DEFAULT_ICON; +} + +Clusterer.prototype = new GOverlay(); + +Clusterer.prototype.initialize = function ( map ){ + this.map = map; + this.currentZoomLevel = map.getZoom(); + + GEvent.addListener( map, 'zoomend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'moveend', Clusterer.makeCaller( Clusterer.display, this ) ); + GEvent.addListener( map, 'infowindowclose', Clusterer.makeCaller( Clusterer.popDown, this ) ); + //Set map for each marker + for(var i = 0,len = this.markers.length ; i < len ; i++){ + this.markers[i].setMap( map ); + } + this.displayLater(); +} + +Clusterer.prototype.remove = function(){ + for ( var i = 0; i < this.markers.length; ++i ){ + this.removeMarker(this.markers[i]); + } +} + +Clusterer.prototype.copy = function(){ + return new Clusterer(this.markers,this.icon,this.maxVisibleMarkers,this.gridSize,this.minMarkersPerCluster,this.maxLinesPerInfoBox); +} + +Clusterer.prototype.redraw = function(force){ + this.displayLater(); +} + +// Call this to change the cluster icon. +Clusterer.prototype.setIcon = function ( icon ){ + this.icon = icon; +} + +// Call this to add a marker. +Clusterer.prototype.addMarker = function ( marker, description){ + marker.onMap = false; + this.markers.push( marker ); + marker.description = marker.description || description; + if(this.map != null){ + marker.setMap(this.map); + this.displayLater(); + } +}; + + +// Call this to remove a marker. +Clusterer.prototype.removeMarker = function ( marker ){ + for ( var i = 0; i < this.markers.length; ++i ) + if ( this.markers[i] == marker ){ + if ( marker.onMap ) + this.map.removeOverlay( marker ); + for ( var j = 0; j < this.clusters.length; ++j ){ + var cluster = this.clusters[j]; + if ( cluster != null ){ + for ( var k = 0; k < cluster.markers.length; ++k ) + if ( cluster.markers[k] == marker ){ + cluster.markers[k] = null; + --cluster.markerCount; + break; + } + if ( cluster.markerCount == 0 ){ + this.clearCluster( cluster ); + this.clusters[j] = null; + } + else if ( cluster == this.poppedUpCluster ) + Clusterer.rePop( this ); + } + } + this.markers[i] = null; + break; + } + this.displayLater(); +}; + +Clusterer.prototype.displayLater = function (){ + if ( this.timeout != null ) + clearTimeout( this.timeout ); + this.timeout = setTimeout( Clusterer.makeCaller( Clusterer.display, this ), 50 ); +}; + +Clusterer.display = function ( clusterer ){ + var i, j, marker, cluster, len, len2; + + clearTimeout( clusterer.timeout ); + + var newZoomLevel = clusterer.map.getZoom(); + if ( newZoomLevel != clusterer.currentZoomLevel ){ + // When the zoom level changes, we have to remove all the clusters. + for ( i = 0 , len = clusterer.clusters.length; i < len; ++i ){ + if ( clusterer.clusters[i] != null ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + } + clusterer.clusters.length = 0; + clusterer.currentZoomLevel = newZoomLevel; + } + + // Get the current bounds of the visible area. + var bounds = clusterer.map.getBounds(); + + // Expand the bounds a little, so things look smoother when scrolling + // by small amounts. + var sw = bounds.getSouthWest(); + var ne = bounds.getNorthEast(); + var dx = ne.lng() - sw.lng(); + var dy = ne.lat() - sw.lat(); + dx *= 0.10; + dy *= 0.10; + bounds = new GLatLngBounds( + new GLatLng( sw.lat() - dy, sw.lng() - dx ), + new GLatLng( ne.lat() + dy, ne.lng() + dx ) + ); + + // Partition the markers into visible and non-visible lists. + var visibleMarkers = []; + var nonvisibleMarkers = []; + for ( i = 0, len = clusterer.markers.length ; i < len; ++i ){ + marker = clusterer.markers[i]; + if ( marker != null ) + if ( bounds.contains( marker.getPoint() ) ) + visibleMarkers.push( marker ); + else + nonvisibleMarkers.push( marker ); + } + + // Take down the non-visible markers. + for ( i = 0, len = nonvisibleMarkers.length ; i < len; ++i ){ + marker = nonvisibleMarkers[i]; + if ( marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + + // Take down the non-visible clusters. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! bounds.contains( cluster.marker.getPoint() ) && cluster.onMap ){ + clusterer.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } + } + + // Clustering! This is some complicated stuff. We have three goals + // here. One, limit the number of markers & clusters displayed, so the + // maps code doesn't slow to a crawl. Two, when possible keep existing + // clusters instead of replacing them with new ones, so that the app pans + // better. And three, of course, be CPU and memory efficient. + if ( visibleMarkers.length > clusterer.maxVisibleMarkers ){ + // Add to the list of clusters by splitting up the current bounds + // into a grid. + var latRange = bounds.getNorthEast().lat() - bounds.getSouthWest().lat(); + var latInc = latRange / clusterer.gridSize; + var lngInc = latInc / Math.cos( ( bounds.getNorthEast().lat() + bounds.getSouthWest().lat() ) / 2.0 * Math.PI / 180.0 ); + for ( var lat = bounds.getSouthWest().lat(); lat <= bounds.getNorthEast().lat(); lat += latInc ) + for ( var lng = bounds.getSouthWest().lng(); lng <= bounds.getNorthEast().lng(); lng += lngInc ){ + cluster = new Object(); + cluster.clusterer = clusterer; + cluster.bounds = new GLatLngBounds( new GLatLng( lat, lng ), new GLatLng( lat + latInc, lng + lngInc ) ); + cluster.markers = []; + cluster.markerCount = 0; + cluster.onMap = false; + cluster.marker = null; + clusterer.clusters.push( cluster ); + } + + // Put all the unclustered visible markers into a cluster - the first + // one it fits in, which favors pre-existing clusters. + for ( i = 0, len = visibleMarkers.length ; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.inCluster ){ + for ( j = 0, len2 = clusterer.clusters.length ; j < len2 ; ++j ){ + cluster = clusterer.clusters[j]; + if ( cluster != null && cluster.bounds.contains( marker.getPoint() ) ){ + cluster.markers.push( marker ); + ++cluster.markerCount; + marker.inCluster = true; + } + } + } + } + + // Get rid of any clusters containing only a few markers. + for ( i = 0, len = clusterer.clusters.length ; i < len ; ++i ) + if ( clusterer.clusters[i] != null && clusterer.clusters[i].markerCount < clusterer.minMarkersPerCluster ){ + clusterer.clearCluster( clusterer.clusters[i] ); + clusterer.clusters[i] = null; + } + + // Shrink the clusters list. + for ( i = clusterer.clusters.length - 1; i >= 0; --i ) + if ( clusterer.clusters[i] != null ) + break; + else + --clusterer.clusters.length; + + // Ok, we have our clusters. Go through the markers in each + // cluster and remove them from the map if they are currently up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null ){ + for ( j = 0 , len2 = cluster.markers.length ; j < len2; ++j ){ + marker = cluster.markers[j]; + if ( marker != null && marker.onMap ){ + clusterer.map.removeOverlay( marker ); + marker.onMap = false; + } + } + } + } + + // Now make cluster-markers for any clusters that need one. + for ( i = 0, len = clusterer.clusters.length; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && cluster.marker == null ){ + // Figure out the average coordinates of the markers in this + // cluster. + var xTotal = 0.0, yTotal = 0.0; + for ( j = 0, len2 = cluster.markers.length; j < len2 ; ++j ){ + marker = cluster.markers[j]; + if ( marker != null ){ + xTotal += ( + marker.getPoint().lng() ); + yTotal += ( + marker.getPoint().lat() ); + } + } + var location = new GLatLng( yTotal / cluster.markerCount, xTotal / cluster.markerCount ); + marker = new GMarker( location, { icon: clusterer.icon } ); + cluster.marker = marker; + GEvent.addListener( marker, 'click', Clusterer.makeCaller( Clusterer.popUp, cluster ) ); + } + } + } + + // Display the visible markers not already up and not in clusters. + for ( i = 0, len = visibleMarkers.length; i < len; ++i ){ + marker = visibleMarkers[i]; + if ( marker != null && ! marker.onMap && ! marker.inCluster ) + { + clusterer.map.addOverlay( marker ); + marker.addedToMap(); + marker.onMap = true; + } + } + + // Display the visible clusters not already up. + for ( i = 0, len = clusterer.clusters.length ; i < len; ++i ){ + cluster = clusterer.clusters[i]; + if ( cluster != null && ! cluster.onMap && bounds.contains( cluster.marker.getPoint() )){ + clusterer.map.addOverlay( cluster.marker ); + cluster.onMap = true; + } + } + + // In case a cluster is currently popped-up, re-pop to get any new + // markers into the infobox. + Clusterer.rePop( clusterer ); +}; + + +Clusterer.popUp = function ( cluster ){ + var clusterer = cluster.clusterer; + var html = ''; + var n = 0; + for ( var i = 0 , len = cluster.markers.length; i < len; ++i ) + { + var marker = cluster.markers[i]; + if ( marker != null ) + { + ++n; + html += ''; + if ( n == clusterer.maxLinesPerInfoBox - 1 && cluster.markerCount > clusterer.maxLinesPerInfoBox ) + { + html += ''; + break; + } + } + } + html += '
    '; + if ( marker.getIcon().smallImage != null ) + html += ''; + else + html += ''; + html += '' + marker.description + '
    ...and ' + ( cluster.markerCount - n ) + ' more
    '; + clusterer.map.closeInfoWindow(); + cluster.marker.openInfoWindowHtml( html ); + clusterer.poppedUpCluster = cluster; +}; + +Clusterer.rePop = function ( clusterer ){ + if ( clusterer.poppedUpCluster != null ) + Clusterer.popUp( clusterer.poppedUpCluster ); +}; + +Clusterer.popDown = function ( clusterer ){ + clusterer.poppedUpCluster = null; +}; + +Clusterer.prototype.clearCluster = function ( cluster ){ + var i, marker; + + for ( i = 0; i < cluster.markers.length; ++i ){ + if ( cluster.markers[i] != null ){ + cluster.markers[i].inCluster = false; + cluster.markers[i] = null; + } + } + + cluster.markers.length = 0; + cluster.markerCount = 0; + + if ( cluster == this.poppedUpCluster ) + this.map.closeInfoWindow(); + + if ( cluster.onMap ) + { + this.map.removeOverlay( cluster.marker ); + cluster.onMap = false; + } +}; + +// This returns a function closure that calls the given routine with the +// specified arg. +Clusterer.makeCaller = function ( func, arg ){ + return function () { func( arg ); }; +}; + + +// Augment GMarker so it handles markers that have been created but +// not yet addOverlayed. +GMarker.prototype.setMap = function ( map ){ + this.map = map; +}; + +GMarker.prototype.getMap = function (){ + return this.map; +} + +GMarker.prototype.addedToMap = function (){ + this.map = null; +}; + + +GMarker.prototype.origOpenInfoWindow = GMarker.prototype.openInfoWindow; +GMarker.prototype.openInfoWindow = function ( node, opts ){ + if ( this.map != null ) + return this.map.openInfoWindow( this.getPoint(), node, opts ); + else + return this.origOpenInfoWindow( node, opts ); +}; + +GMarker.prototype.origOpenInfoWindowHtml = GMarker.prototype.openInfoWindowHtml; +GMarker.prototype.openInfoWindowHtml = function ( html, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowHtml( this.getPoint(), html, opts ); + else + return this.origOpenInfoWindowHtml( html, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabs = GMarker.prototype.openInfoWindowTabs; +GMarker.prototype.openInfoWindowTabs = function ( tabNodes, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabs( this.getPoint(), tabNodes, opts ); + else + return this.origOpenInfoWindowTabs( tabNodes, opts ); +}; + +GMarker.prototype.origOpenInfoWindowTabsHtml = GMarker.prototype.openInfoWindowTabsHtml; +GMarker.prototype.openInfoWindowTabsHtml = function ( tabHtmls, opts ){ + if ( this.map != null ) + return this.map.openInfoWindowTabsHtml( this.getPoint(), tabHtmls, opts ); + else + return this.origOpenInfoWindowTabsHtml( tabHtmls, opts ); +}; + +GMarker.prototype.origShowMapBlowup = GMarker.prototype.showMapBlowup; +GMarker.prototype.showMapBlowup = function ( opts ){ + if ( this.map != null ) + return this.map.showMapBlowup( this.getPoint(), opts ); + else + return this.origShowMapBlowup( opts ); +}; + + +function addDescriptionToMarker(marker, description){ + marker.description = description; + return marker; +} diff --git a/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js b/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js new file mode 100644 index 0000000..315c26d --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/geoRssOverlay.js @@ -0,0 +1,194 @@ +// GeoRssOverlay: GMaps API extension to display a group of markers from +// a RSS feed +// +// Copyright 2006 Mikel Maron (email: mikel_maron yahoo com) +// +// The original version of this code is called MGeoRSS and can be found +// at the following address: +// http://brainoff.com/gmaps/mgeorss.html +// +// Modified by Andrew Turner to add support for the GeoRss Simple vocabulary +// +// Modified and bundled with YM4R in accordance with the following +// license: +// +// This work is public domain + +function GeoRssOverlay(rssurl,icon,proxyurl,options){ + this.rssurl = rssurl; + this.icon = icon; + this.proxyurl = proxyurl; + if(options['visible'] == undefined) + this.visible = true; + else + this.visible = options['visible']; + this.listDiv = options['listDiv']; //ID of the item list DIV + this.contentDiv = options['contentDiv']; //ID of the content DIV + this.listItemClass = options['listItemClass']; //Class of the list item DIV + this.limitItems = options['limit']; //Maximum number of displayed entries + this.request = false; + this.markers = []; +} + +GeoRssOverlay.prototype = new GOverlay(); + +GeoRssOverlay.prototype.initialize=function(map) { + this.map = map; + this.load(); +} + +GeoRssOverlay.prototype.redraw = function(force){ + //nothing to do : the markers are already taken care of +} + +GeoRssOverlay.prototype.remove = function(){ + for(var i= 0, len = this.markers.length ; i< len; i++){ + this.map.removeOverlay(this.markers[i]); + } +} + +GeoRssOverlay.prototype.showHide=function() { + if (this.visible) { + for (var i=0;i" + title + "

    " + description; + + if(this.contentDiv == undefined){ + GEvent.addListener(marker, "click", function() { + marker.openInfoWindowHtml(html); + }); + }else{ + var contentDiv = this.contentDiv; + GEvent.addListener(marker, "click", function() { + document.getElementById(contentDiv).innerHTML = html; + }); + } + + if(this.listDiv != undefined){ + var a = document.createElement('a'); + a.innerHTML = title; + a.setAttribute("href","#"); + var georss = this; + a.onclick = function(){ + georss.showMarker(index); + return false; + }; + var div = document.createElement('div'); + if(this.listItemClass != undefined){ + div.setAttribute("class",this.listItemClass); + } + div.appendChild(a); + document.getElementById(this.listDiv).appendChild(div); + } + + return marker; +} diff --git a/vendor/plugins/ym4r_gm/javascript/markerGroup.js b/vendor/plugins/ym4r_gm/javascript/markerGroup.js new file mode 100644 index 0000000..02fe624 --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/markerGroup.js @@ -0,0 +1,114 @@ +function GMarkerGroup(active, markers, markersById) { + this.active = active; + this.markers = markers || new Array(); + this.markersById = markersById || new Object(); +} + +GMarkerGroup.prototype = new GOverlay(); + +GMarkerGroup.prototype.initialize = function(map) { + this.map = map; + + if(this.active){ + for(var i = 0 , len = this.markers.length; i < len; i++) { + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } +} + +//If not already done (ie if not inactive) remove all the markers from the map +GMarkerGroup.prototype.remove = function() { + this.deactivate(); +} + +GMarkerGroup.prototype.redraw = function(force){ + //Nothing to do : markers are already taken care of +} + +//Copy the data to a new Marker Group +GMarkerGroup.prototype.copy = function() { + var overlay = new GMarkerGroup(this.active); + overlay.markers = this.markers; //Need to do deep copy + overlay.markersById = this.markersById; //Need to do deep copy + return overlay; +} + +//Inactivate the Marker group and clear the internal content +GMarkerGroup.prototype.clear = function(){ + //deactivate the map first (which removes the markers from the map) + this.deactivate(); + //Clear the internal content + this.markers = new Array(); + this.markersById = new Object(); +} + +//Add a marker to the GMarkerGroup ; Adds it now to the map if the GMarkerGroup is active +GMarkerGroup.prototype.addMarker = function(marker,id){ + if(id == undefined){ + this.markers.push(marker); + }else{ + this.markersById[id] = marker; + } + if(this.active && this.map != undefined ){ + this.map.addOverlay(marker); + } +} + +//Open the info window (or info window tabs) of a marker +GMarkerGroup.prototype.showMarker = function(id){ + var marker = this.markersById[id]; + if(marker != undefined){ + GEvent.trigger(marker,"click"); + } +} + +//Activate (or deactivate depending on the argument) the GMarkerGroup +GMarkerGroup.prototype.activate = function(active){ + active = (active == undefined) ? true : active; + if(!active){ + if(this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.removeOverlay(this.markers[i]) + } + for(var id in this.markersById){ + this.map.removeOverlay(this.markersById[id]); + } + } + this.active = false; + } + }else{ + if(!this.active){ + if(this.map != undefined){ + for(var i = 0 , len = this.markers.length; i < len; i++){ + this.map.addOverlay(this.markers[i]); + } + for(var id in this.markersById){ + this.map.addOverlay(this.markersById[id]); + } + } + this.active = true; + } + } +} + +GMarkerGroup.prototype.centerAndZoomOnMarkers = function() { + if(this.map != undefined){ + //merge markers and markersById + var tmpMarkers = this.markers.slice(); + for (var id in this.markersById){ + tmpMarkers.push(this.markersById[id]); + } + if(tmpMarkers.length > 0){ + this.map.centerAndZoomOnMarkers(tmpMarkers); + } + } +} + +//Deactivate the Group Overlay (convenience method) +GMarkerGroup.prototype.deactivate = function(){ + this.activate(false); +} diff --git a/vendor/plugins/ym4r_gm/javascript/wms-gs.js b/vendor/plugins/ym4r_gm/javascript/wms-gs.js new file mode 100644 index 0000000..c67146b --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/wms-gs.js @@ -0,0 +1,69 @@ +/* + * Call generic wms service for GoogleMaps v2 + * John Deck, UC Berkeley + * Inspiration & Code from: + * Mike Williams http://www.econym.demon.co.uk/googlemaps2/ V2 Reference & custommap code + * Brian Flood http://www.spatialdatalogic.com/cs/blogs/brian_flood/archive/2005/07/11/39.aspx V1 WMS code + * Kyle Mulka http://blog.kylemulka.com/?p=287 V1 WMS code modifications + * http://search.cpan.org/src/RRWO/GPS-Lowrance-0.31/lib/Geo/Coordinates/MercatorMeters.pm + * + * Modified by Chris Holmes, TOPP to work by default with GeoServer. + * + * Bundled with YM4R with John Deck's permission. + * Slightly modified to fit YM4R. + * See johndeck.blogspot.com for the original version and for examples and instructions of how to use it. + */ + +var WGS84_SEMI_MAJOR_AXIS = 6378137.0; //equatorial radius +var WGS84_ECCENTRICITY = 0.0818191913108718138; +var DEG2RAD=0.0174532922519943; +var PI=3.14159267; + +function dd2MercMetersLng(p_lng) { + return WGS84_SEMI_MAJOR_AXIS * (p_lng*DEG2RAD); +} + +function dd2MercMetersLat(p_lat) { + var lat_rad = p_lat * DEG2RAD; + return WGS84_SEMI_MAJOR_AXIS * Math.log(Math.tan((lat_rad + PI / 2) / 2) * Math.pow( ((1 - WGS84_ECCENTRICITY * Math.sin(lat_rad)) / (1 + WGS84_ECCENTRICITY * Math.sin(lat_rad))), (WGS84_ECCENTRICITY/2))); +} + +function addWMSPropertiesToLayer(tile_layer,base_url,layers,styles,format,merc_proj,use_geo){ + tile_layer.format = format; + tile_layer.baseURL = base_url; + tile_layer.styles = styles; + tile_layer.layers = layers; + tile_layer.mercatorEpsg = merc_proj; + tile_layer.useGeographic = use_geo; + return tile_layer; +} + +getTileUrlForWMS=function(a,b,c) { + var lULP = new GPoint(a.x*256,(a.y+1)*256); + var lLRP = new GPoint((a.x+1)*256,a.y*256); + var lUL = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lULP,b,c); + var lLR = G_NORMAL_MAP.getProjection().fromPixelToLatLng(lLRP,b,c); + + if (this.useGeographic){ + var lBbox=lUL.x+","+lUL.y+","+lLR.x+","+lLR.y; + var lSRS="EPSG:4326"; + }else{ + var lBbox=dd2MercMetersLng(lUL.x)+","+dd2MercMetersLat(lUL.y)+","+dd2MercMetersLng(lLR.x)+","+dd2MercMetersLat(lLR.y); + var lSRS="EPSG:" + this.mercatorEpsg; + } + var lURL=this.baseURL; + lURL+="?REQUEST=GetMap"; + lURL+="&SERVICE=WMS"; + lURL+="&VERSION=1.1.1"; + lURL+="&LAYERS="+this.layers; + lURL+="&STYLES="+this.styles; + lURL+="&FORMAT=image/"+this.format; + lURL+="&BGCOLOR=0xFFFFFF"; + lURL+="&TRANSPARENT=TRUE"; + lURL+="&SRS="+lSRS; + lURL+="&BBOX="+lBbox; + lURL+="&WIDTH=256"; + lURL+="&HEIGHT=256"; + lURL+="&reaspect=false"; + return lURL; +} diff --git a/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js b/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js new file mode 100644 index 0000000..1c768df --- /dev/null +++ b/vendor/plugins/ym4r_gm/javascript/ym4r-gm.js @@ -0,0 +1,117 @@ +// JS helper functions for YM4R + +function addInfoWindowToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowHtml(info,options);}); + return marker; +} + +function addInfoWindowTabsToMarker(marker,info,options){ + GEvent.addListener(marker, "click", function() {marker.openInfoWindowTabsHtml(info,options);}); + return marker; +} + +function addPropertiesToLayer(layer,getTile,copyright,opacity,isPng){ + layer.getTileUrl = getTile; + layer.getCopyright = copyright; + layer.getOpacity = opacity; + layer.isPng = isPng; + return layer; +} + +function addOptionsToIcon(icon,options){ + for(var k in options){ + icon[k] = options[k]; + } + return icon; +} + +function addCodeToFunction(func,code){ + if(func == undefined) + return code; + else{ + return function(){ + func(); + code(); + } + } +} + +function addGeocodingToMarker(marker,address){ + marker.orig_initialize = marker.initialize; + orig_redraw = marker.redraw; + marker.redraw = function(force){}; //empty the redraw method so no error when called by addOverlay. + marker.initialize = function(map){ + new GClientGeocoder().getLatLng(address, + function(latlng){ + if(latlng){ + marker.redraw = orig_redraw; + marker.orig_initialize(map); //init before setting point + marker.setPoint(latlng); + }//do nothing + }); + }; + return marker; +} + + + +GMap2.prototype.centerAndZoomOnMarkers = function(markers) { + var bounds = new GLatLngBounds(markers[0].getPoint(), + markers[0].getPoint()); + for (var i=1, len = markers.length ; i:key) or a host, (:host). + def self.get(request,options = {}) + api_key = ApiKey.get(options) + output = options[:output] || "kml" + url = "http://maps.google.com/maps/geo?q=#{URI.encode(request)}&key=#{api_key}&output=#{output}" + + res = open(url).read + + case output.to_sym + when :json + res = eval(res.gsub(":","=>")) #!!!EVAL EVAL EVAL EVAL!!! hopefully we can trust google... + placemarks = Placemarks.new(res['name'],res['Status']['code']) + if res['Placemark'] + placemark = res['Placemark'] + + placemark.each do |data| + + data_country = data['Country']['CountryNameCode'] rescue "" + data_administrative = data['Country']['AdministrativeArea']['AdministrativeAreaName'] rescue "" + data_sub_administrative = data['Country']['AdministrativeArea']['SubAdministrativeArea']['SubAdministrativeAreaName'] rescue "" + data_locality = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['LocalityName'] rescue "" + data_dependent_locality = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['DependentLocalityName'] rescue "" + data_thoroughfare = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['Thoroughfare']['ThoroughfareName'] rescue "" + data_postal_code = data['Country']['AdministrativeArea']['SubAdministrativeArea']['Locality']['DependentLocality']['Thoroughfare']['PostalCode']['PostalCodeNumber'] rescue "" + lon, lat = data['Point']['coordinates'][0,2] + data_accuracy = data['Accuracy'] + unless data_accuracy.nil? + data_accuracy = data_accuracy.to_i + end + + placemarks << Geocoding::Placemark.new(data['address'], + data_country, + data_administrative, + data_sub_administrative, + data_locality, + data_dependent_locality, + data_thoroughfare, + data_postal_code, + lon, lat, data_accuracy) + + end + end + when :kml, :xml + + doc = REXML::Document.new(res) + + response = doc.elements['//Response'] + placemarks = Placemarks.new(response.elements['name'].text,response.elements['Status/code'].text.to_i) + response.elements.each(".//Placemark") do |placemark| + data = placemark.elements + data_country = data['.//CountryNameCode'] + data_administrative = data['.//AdministrativeAreaName'] + data_sub_administrative = data['.//SubAdministrativeAreaName'] + data_locality = data['.//LocalityName'] + data_dependent_locality = data['.//DependentLocalityName'] + data_thoroughfare = data['.//ThoroughfareName'] + data_postal_code = data['.//PostalCodeNumber'] + lon, lat = data['.//coordinates'].text.split(",")[0..1].collect {|l| l.to_f } + data_accuracy = data['.//*[local-name()="AddressDetails"]'].attributes['Accuracy'] + unless data_accuracy.nil? + data_accuracy = data_accuracy.to_i + end + placemarks << Geocoding::Placemark.new(data['address'].text, + data_country.nil? ? "" : data_country.text, + data_administrative.nil? ? "" : data_administrative.text, + data_sub_administrative.nil? ? "" : data_sub_administrative.text, + data_locality.nil? ? "" : data_locality.text, + data_dependent_locality.nil? ? "" : data_dependent_locality.text, + data_thoroughfare.nil? ? "" : data_thoroughfare.text, + data_postal_code.nil? ? "" : data_postal_code.text, + lon, lat, data_accuracy ) + end + end + + placemarks + end + + #Group of placemarks returned by the Geocoding service. If the result is valid the +status+ attribute should be equal to Geocoding::GE0_SUCCESS + class Placemarks < Array + attr_accessor :name,:status + + def initialize(name,status) + super(0) + @name = name + @status = status + end + end + + #A result from the Geocoding service. + class Placemark < Struct.new(:address,:country_code,:administrative_area,:sub_administrative_area,:locality,:dependent_locality,:thoroughfare,:postal_code,:longitude,:latitude,:accuracy) + def lonlat + [longitude,latitude] + end + + def latlon + [latitude,longitude] + end + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb new file mode 100644 index 0000000..ddaf8bb --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/helper.rb @@ -0,0 +1,41 @@ + +Ym4r::GmPlugin::GPolyline.class_eval do + #Creates a GPolyline object from a georuby line string. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(line_string,color = nil,weight = nil,opacity = nil) + GPolyline.new(line_string.points.collect { |point| GLatLng.new([point.y,point.x])},color,weight,opacity) + end +end + +Ym4r::GmPlugin::GMarker.class_eval do + #Creates a GMarker object from a georuby point. Accepts the same options as the GMarker constructor. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(point,options = {}) + GMarker.new([point.y,point.x],options) + end +end + +Ym4r::GmPlugin::GLatLng.class_eval do + #Creates a GLatLng object from a georuby point. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(point,unbounded = nil) + GLatLng.new([point.y,point.x],unbounded) + end +end + +Ym4r::GmPlugin::GLatLngBounds.class_eval do + #Creates a GLatLng object from a georuby point. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(envelope) + GLatLngBounds.new(GLatLng.from_georuby(envelope.lower_corner), + GLatLng.from_georuby(envelope.upper_corner)) + end +end + +Ym4r::GmPlugin::GPolygon.class_eval do + #Creates a GPolygon object from a georuby polygon or line string. Assumes the points of the line strings are stored in Longitude(x)/Latitude(y) order. + def self.from_georuby(ls_or_p, stroke_color="#000000",stroke_weight=1,stroke_opacity=1.0,color="#ff0000",opacity=1.0) + if ls_or_p.is_a?(GeoRuby::SimpleFeatures::LineString) + GPolygon.new(ls_or_p.collect { |point| GLatLng.new([point.y,point.x])},stroke_color,stroke_weight,stroke_opacity,color,opacity) + else + GPolygon.new(ls_or_p[0].collect { |point| GLatLng.new([point.y,point.x])},stroke_color,stroke_weight,stroke_opacity,color,opacity) + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb new file mode 100644 index 0000000..0de9c18 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/key.rb @@ -0,0 +1,37 @@ +module Ym4r + module GmPlugin + class GMapsAPIKeyConfigFileNotFoundException < StandardError + end + + class AmbiguousGMapsAPIKeyException < StandardError + end + + #Class fo the manipulation of the API key + class ApiKey + #Read the API key config for the current ENV + unless File.exist?(RAILS_ROOT + '/config/gmaps_api_key.yml') + raise GMapsAPIKeyConfigFileNotFoundException.new("File RAILS_ROOT/config/gmaps_api_key.yml not found") + else + env = ENV['RAILS_ENV'] || RAILS_ENV + GMAPS_API_KEY = YAML.load_file(RAILS_ROOT + '/config/gmaps_api_key.yml')[env] + end + + def self.get(options = {}) + if options.has_key?(:key) + options[:key] + elsif GMAPS_API_KEY.is_a?(Hash) + #For this environment, multiple hosts are possible. + #:host must have been passed as option + if options.has_key?(:host) + GMAPS_API_KEY[options[:host]] + else + raise AmbiguousGMapsAPIKeyException.new(GMAPS_API_KEY.keys.join(",")) + end + else + #Only one possible key: take it and ignore the :host option if it is there + GMAPS_API_KEY + end + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb new file mode 100644 index 0000000..bab6625 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/layer.rb @@ -0,0 +1,125 @@ +module Ym4r + module GmPlugin + #Map types of the map + class GMapType + include MappingObject + + G_NORMAL_MAP = Variable.new("G_NORMAL_MAP") + G_SATELLITE_MAP = Variable.new("G_SATELLITE_MAP") + G_HYBRID_MAP = Variable.new("G_HYBRID_MAP") + + attr_accessor :layers, :name, :projection, :options + + #The options can be any of the GMapType options detailed in the documentation + a :projection. + def initialize(layers, name, options = {}) + @layers = layers + @name = name + @projection = options.delete(:projection) || GMercatorProjection.new + @options = options + end + + def create + "new GMapType(#{MappingObject.javascriptify_variable(Array(layers))}, #{MappingObject.javascriptify_variable(projection)}, #{MappingObject.javascriptify_variable(name)}, #{MappingObject.javascriptify_variable(options)})" + end + end + + #Represents a mercator projection for zoom levels 0 to 17 (more than that by passing an argument to the constructor) + class GMercatorProjection + include MappingObject + + attr_accessor :n + + def initialize(n = nil) + @n = n + end + + def create + if n.nil? + return "G_NORMAL_MAP.getProjection()" + else + "new GMercatorProjection(#{@n})" + end + end + end + + #Abstract Tile layer. Subclasses must implement a get_tile_url method. + class GTileLayer + include MappingObject + + attr_accessor :opacity, :zoom_range, :copyright, :format + + #Options are the following, with default values: + #:zoom_range (0..17), :copyright ({'prefix' => '', 'copyright_texts' => [""]}), :opacity (1.0), :format ("png") + def initialize(options = {}) + @opacity = options[:opacity] || 1.0 + @zoom_range = options[:zoom_range] || (0..17) + @copyright = options[:copyright] || {'prefix' => '', 'copyright_texts' => [""]} + @format = (options[:format] || "png").to_s + end + + def create + "addPropertiesToLayer(new GTileLayer(new GCopyrightCollection(\"\"),#{zoom_range.begin},#{zoom_range.end}),#{get_tile_url},function(a,b) {return #{MappingObject.javascriptify_variable(@copyright)};}\n,function() {return #{@opacity};},function(){return #{@format == "png"};})" + end + + #for subclasses to implement + def get_tile_url + end + end + + #Represents a pre tiled layer, taking images directly from a server, without using a server script. + class PreTiledLayer < GTileLayer + attr_accessor :base_url + + #Possible options are the same as for the GTileLayer constructor + def initialize(base_url,options = {}) + super(options) + @base_url = base_url + end + + #Returns the code to determine the url to fetch the tile. Follows the convention adopted by the tiler: {base_url}/tile_{b}_{a.x}_{a.y}.{format} + def get_tile_url + "function(a,b) { return '#{@base_url}/tile_' + b + '_' + a.x + '_' + a.y + '.#{format}';}" + end + end + + #Represents a pretiled layer (it actually does not really matter where the tiles come from). Calls an action on the server to get back the tiles. It passes the action arguments x, y (coordinates of the tile) and z (zoom level). It can be used, for example, to return default tiles when the requested tile is not present. + class PreTiledLayerFromAction < PreTiledLayer + def get_tile_url + "function(a,b) { return '#{base_url}?x=' + a.x + '&y=' + a.y + '&z=' + b ;}" + end + end + + #Represents a TileLayer where the tiles are generated dynamically from a WMS server (MapServer, GeoServer,...) + #You need to include the JavaScript file wms-gs.js for this to work + #see http://docs.codehaus.org/display/GEOSDOC/Google+Maps + class WMSLayer < GTileLayer + attr_accessor :base_url, :layers, :styles, :merc_proj, :use_geographic + + #Options are the same as with GTileLayer + :styles (""), :merc_proj (:mapserver), :use_geographic (false) + def initialize(base_url, layers, options = {}) + super(options) + @base_url = base_url.gsub(/\?$/,"") #standardize the url + @layers = layers + @styles = options[:styles] || "" + merc_proj = options[:merc_proj] || :mapserver + @merc_proj = if merc_proj == :mapserver + "54004" + elsif merc_proj == :geoserver + "41001" + else + merc_proj.to_s + end + @use_geographic = options.has_key?(:use_geographic)? options[:use_geographic] : false + puts format + end + + def get_tile_url + "getTileUrlForWMS" + end + + def create + "addWMSPropertiesToLayer(#{super},#{MappingObject.javascriptify_variable(@base_url)},#{MappingObject.javascriptify_variable(@layers)},#{MappingObject.javascriptify_variable(@styles)},#{MappingObject.javascriptify_variable(format)},#{MappingObject.javascriptify_variable(@merc_proj)},#{MappingObject.javascriptify_variable(@use_geographic)})" + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb new file mode 100644 index 0000000..82d1374 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/map.rb @@ -0,0 +1,268 @@ +module Ym4r + module GmPlugin + #Representing the Google Maps API class GMap2. + class GMap + include MappingObject + + #A constant containing the declaration of the VML namespace, necessary to display polylines under IE. + VML_NAMESPACE = "xmlns:v=\"urn:schemas-microsoft-com:vml\"" + + #The id of the DIV that will contain the map in the HTML page. + attr_reader :container + + #By default the map in the HTML page will be globally accessible with the name +map+. + def initialize(container, variable = "map") + @container = container + @variable = variable + @init = [] + @init_end = [] #for stuff that must be initialized at the end (controls) + @init_begin = [] #for stuff that must be initialized at the beginning (center + zoom) + @global_init = [] + end + + #Deprecated. Use the static version instead. + def header(with_vml = true) + GMap.header(:with_vml => with_vml) + end + + #Outputs the header necessary to use the Google Maps API, by including the JS files of the API, as well as a file containing YM4R/GM helper functions. By default, it also outputs a style declaration for VML elements. This default can be overriddent by passing :with_vml => false as option to the method. You can also pass a :host option in order to select the correct API key for the location where your app is currently running, in case the current environment has multiple possible keys. Usually, in this case, you should pass it @request.host. If you have defined only one API key for the current environment, the :host option is ignored. Finally you can override all the key settings in the configuration by passing a value to the :key key. Finally, you can pass a language for the map type buttons with the :hl option (possible values are: Japanese (ja), French (fr), German (de), Italian (it), Spanish (es), Catalan (ca), Basque (eu) and Galician (gl): no values means english) + def self.header(options = {}) + options[:with_vml] = true unless options.has_key?(:with_vml) + options[:hl] ||= '' + api_key = ApiKey.get(options) + a = "\n" + a << "\n" unless options[:without_js] + a << "" if options[:with_vml] + a + end + + #Outputs the

    which has been configured to contain the map. You can pass :width and :height as options to output this in the style attribute of the DIV element (you could also achieve the same effect by putting the dimension info into a CSS or using the instance method GMap#header_width_height). You can aslo pass :class to set the classname of the div. + def div(options = {}) + attributes = "id=\"#{@container}\" " + if options.has_key?(:height) && options.has_key?(:width) + attributes += "style=\"width:#{options.delete(:width)}px;height:#{options.delete(:height)}px\" " + end + if options.has_key?(:class) + attributes += options.keys.map {|opt| "#{opt}=\"#{options[opt]}\"" }.join(" ") + end + "
    " + end + + #Outputs a style declaration setting the dimensions of the DIV container of the map. This info can also be set manually in a CSS. + def header_width_height(width,height) + "" + end + + #Records arbitrary JavaScript code and outputs it during initialization inside the +load+ function. + def record_init(code) + @init << code + end + + #Initializes the controls: you can pass a hash with keys :small_map, :large_map, :small_zoom, :scale, :map_type and :overview_map and a boolean value as the value (usually true, since the control is not displayed by default) + def control_init(controls = {}) + @init_end << add_control(GSmallMapControl.new) if controls[:small_map] + @init_end << add_control(GLargeMapControl.new) if controls[:large_map] + @init_end << add_control(GSmallZoomControl.new) if controls[:small_zoom] + @init_end << add_control(GScaleControl.new) if controls[:scale] + @init_end << add_control(GMapTypeControl.new) if controls[:map_type] + @init_end << add_control(GOverviewMapControl.new) if controls[:overview_map] + end + + #Initializes the interface configuration: double-click zoom, dragging, continuous zoom,... You can pass a hash with keys :dragging, :info_window, :double_click_zoom, :continuous_zoom and :scroll_wheel_zoom. The values should be true or false. Check the google maps API doc to know what the default values are. + def interface_init(interface = {}) + if !interface[:dragging].nil? + if interface[:dragging] + @init << enableDragging() + else + @init << disableDragging() + end + end + if !interface[:info_window].nil? + if interface[:info_window] + @init << enableInfoWindow() + else + @init << disableInfoWindow() + end + end + if !interface[:double_click_zoom].nil? + if interface[:double_click_zoom] + @init << enableDoubleClickZoom() + else + @init << disableDoubleClickZoom() + end + end + if !interface[:continuous_zoom].nil? + if interface[:continuous_zoom] + @init << enableContinuousZoom() + else + @init << disableContinuousZoom() + end + end + if !interface[:scroll_wheel_zoom].nil? + if interface[:scroll_wheel_zoom] + @init << enableScrollWheelZoom() + else + @init << disableScrollWheelZoom() + end + end + end + + #Initializes the initial center and zoom of the map. +center+ can be both a GLatLng object or a 2-float array. + def center_zoom_init(center, zoom) + if center.is_a?(GLatLng) + @init_begin << set_center(center,zoom) + else + @init_begin << set_center(GLatLng.new(center),zoom) + end + end + + #Center and zoom based on the coordinates passed as argument (either 2D arrays or GLatLng objects) + def center_zoom_on_points_init(*points) + if(points.length > 0) + if(points[0].is_a?(Array)) + points = points.collect { |point| GLatLng.new(point) } + end + @init_begin << center_and_zoom_on_points(points) + end + end + + #Center and zoom based on the bbox corners. Pass a GLatLngBounds object, an array of 2D coordinates (sw and ne) or an array of GLatLng objects (sw and ne). + def center_zoom_on_bounds_init(latlngbounds) + if(latlngbounds.is_a?(Array)) + if latlngbounds[0].is_a?(Array) + latlngbounds = GLatLngBounds.new(GLatLng.new(latlngbounds[0]),GLatLng.new(latlngbounds[1])) + elsif latlngbounds[0].is_a?(GLatLng) + latlngbounds = GLatLngBounds.new(*latlngbounds) + end + end + #else it is already a latlngbounds object + + @init_begin << center_and_zoom_on_bounds(latlngbounds) + end + + #Initializes the map by adding an overlay (marker or polyline). + def overlay_init(overlay) + @init << add_overlay(overlay) + end + + #Sets up a new map type. If +add+ is false, all the other map types of the map are wiped out. If you want to access the map type in other methods, you should declare the map type first (with +declare_init+). + def add_map_type_init(map_type, add = true) + unless add + @init << get_map_types.set_property(:length,0) + end + @init << add_map_type(map_type) + end + #for legacy purpose + alias :map_type_init :add_map_type_init + + #Sets the map type displayed by default after the map is loaded. It should be known from the map (ie either the default map types or a user-defined map type added with add_map_type_init). Use set_map_type_init(GMapType::G_SATELLITE_MAP) or set_map_type_init(GMapType::G_HYBRID_MAP) to initialize the map with repsecitvely the Satellite view and the hybrid view. + def set_map_type_init(map_type) + @init << set_map_type(map_type) + end + + #Locally declare a MappingObject with variable name "name" + def declare_init(variable, name) + @init << variable.declare(name) + end + + #Records arbitrary JavaScript code and outputs it during initialization outside the +load+ function (ie globally). + def record_global_init(code) + @global_init << code + end + + #Deprecated. Use icon_global_init instead. + def icon_init(icon , name) + icon_global_init(icon , name) + end + + #Initializes an icon and makes it globally accessible through the JavaScript variable of name +variable+. + def icon_global_init(icon , name, options = {}) + declare_global_init(icon,name,options) + end + + #Registers an event + def event_init(object,event,callback) + @init << "GEvent.addListener(#{object.to_javascript},\"#{MappingObject.javascriptify_method(event.to_s)}\",#{callback});" + end + + #Registers an event globally + def event_global_init(object,event,callback) + @global_init << "GEvent.addListener(#{object.to_javascript},\"#{MappingObject.javascriptify_method(event.to_s)}\",#{callback});" + end + + #Declares the overlay globally with name +name+ + def overlay_global_init(overlay,name, options = {}) + declare_global_init(overlay,name, options) + @init << add_overlay(overlay) + end + + #Globally declare a MappingObject with variable name "name". Option :local_construction should be passed if the construction has to be done inside the onload callback method (for exsample if it depends on the GMap to be initialized) + def declare_global_init(variable,name, options = {}) + unless options[:local_construction] + @global_init << "var #{variable.assign_to(name)}" + else + @global_init << "var #{name};" + @init << variable.assign_to(name) + end + end + + #Outputs the initialization code for the map. By default, it outputs the script tags, performs the initialization in response to the onload event of the window and makes the map globally available. If you pass +true+ to the option key :full, the map will be setup in full screen, in which case it is not necessary (but not harmful) to set a size for the map div. + def to_html(options = {}) + no_load = options[:no_load] + no_script_tag = options[:no_script_tag] + no_declare = options[:no_declare] + no_global = options[:no_global] + fullscreen = options[:full] + load_pr = options[:proto_load] #to prevent some problems when the onload event callback from Prototype is used + + html = "" + html << "" if !no_script_tag + + if fullscreen + #setting up the style in case of full screen + html << "" + end + + html + end + + #Outputs in JavaScript the creation of a GMap2 object + def create + "new GMap2(document.getElementById(\"#{@container}\"))" + end + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb new file mode 100644 index 0000000..66d825e --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/mapping.rb @@ -0,0 +1,128 @@ +module Ym4r + module GmPlugin + #The module where all the Ruby-to-JavaScript conversion takes place. It is included by all the classes in the YM4R library. + module MappingObject + #The name of the variable in JavaScript space. + attr_reader :variable + + #Creates javascript code for missing methods + takes care of listeners + def method_missing(name,*args) + str_name = name.to_s + if str_name =~ /^on_(.*)/ + if args.length != 1 + raise ArgumentError("Only 1 argument is allowed on on_ methods"); + else + Variable.new("GEvent.addListener(#{to_javascript},\"#{MappingObject.javascriptify_method($1)}\",#{args[0]})") + end + else + args.collect! do |arg| + MappingObject.javascriptify_variable(arg) + end + Variable.new("#{to_javascript}.#{MappingObject.javascriptify_method(str_name)}(#{args.join(",")})") + end + end + + #Creates javascript code for array or hash indexing + def [](index) #index could be an integer or string + return Variable.new("#{to_javascript}[#{MappingObject.javascriptify_variable(index)}]") + end + + #Transforms a Ruby object into a JavaScript string : MAppingObject, String, Array, Hash and general case (using to_s) + def self.javascriptify_variable(arg) + if arg.is_a?(MappingObject) + arg.to_javascript + elsif arg.is_a?(String) + "\"#{MappingObject.escape_javascript(arg)}\"" + elsif arg.is_a?(Array) + "[" + arg.collect{ |a| MappingObject.javascriptify_variable(a)}.join(",") + "]" + elsif arg.is_a?(Hash) + "{" + arg.to_a.collect do |v| + "#{MappingObject.javascriptify_method(v[0].to_s)} : #{MappingObject.javascriptify_variable(v[1])}" + end.join(",") + "}" + elsif arg.nil? + "undefined" + else + arg.to_s + end + end + + #Escape string to be used in JavaScript. Lifted from rails. + def self.escape_javascript(javascript) + javascript.gsub(/\r\n|\n|\r/, "\\n").gsub("\"") { |m| "\\#{m}" } + end + + #Transform a ruby-type method name (like add_overlay) to a JavaScript-style one (like addOverlay). + def self.javascriptify_method(method_name) + method_name.gsub(/_(\w)/){|s| $1.upcase} + end + + #Declares a Mapping Object bound to a JavaScript variable of name +variable+. + def declare(variable) + @variable = variable + "var #{@variable} = #{create};" + end + + #declare with a random variable name + def declare_random(init,size = 8) + s = init.clone + 6.times { s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr } + declare(s) + end + + #Checks if the MappinObject has been declared + def declared? + !@variable.nil? + end + + #Binds a Mapping object to a previously declared JavaScript variable of name +variable+. + def assign_to(variable) + @variable = variable + "#{@variable} = #{create};" + end + + #Assign the +value+ to the +property+ of the MappingObject + def set_property(property, value) + "#{to_javascript}.#{MappingObject.javascriptify_method(property.to_s)} = #{MappingObject.javascriptify_variable(value)}" + end + + #Returns the code to get a +property+ from the MappingObject + def get_property(property) + Variable.new("#{to_javascript}.#{MappingObject.javascriptify_method(property.to_s)}") + end + + #Returns a Javascript code representing the object + def to_javascript + unless @variable.nil? + @variable + else + create + end + end + + #Creates a Mapping Object in JavaScript. + #To be implemented by subclasses if needed + def create + end + end + + #Used to bind a ruby variable to an already existing JavaScript one. It doesn't have to be a variable in the sense "var variable" but it can be any valid JavaScript expression that has a value. + class Variable + include MappingObject + + def initialize(variable) + @variable = variable + end + #Returns the javascript expression contained in the object. + def create + @variable + end + #Returns the expression inside the Variable followed by a ";" + def to_s + @variable + ";" + end + + UNDEFINED = Variable.new("undefined") + end + end +end + diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb new file mode 100644 index 0000000..5e0953a --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/overlay.rb @@ -0,0 +1,386 @@ +module Ym4r + module GmPlugin + #A graphical marker positionned through geographic coordinates (in the WGS84 datum). An HTML info window can be set to be displayed when the marker is clicked on. + class GMarker + include MappingObject + attr_accessor :point, :options, :info_window, :info_window_tabs, :address + #The +points+ argument can be either a GLatLng object or an array of 2 floats. The +options+ keys can be: :icon, :clickable, :title, :info_window and :info_window_tabs, as well as :max_width. The value of the +info_window+ key is a string of HTML code that will be displayed when the markers is clicked on. The value of the +info_window_tabs+ key is an array of GInfoWindowTab objects or a hash directly, in which case it will be transformed to an array of GInfoWindowTabs, with the keys as the tab headers and the values as the content. + def initialize(position, options = {}) + if position.is_a?(Array) + @point = GLatLng.new(position) + elsif position.is_a?(String) + @point = Variable.new("INVISIBLE") #default coordinates: won't appear anyway + @address = position + else + @point = position + end + @info_window = options.delete(:info_window) + @info_window_tabs = options.delete(:info_window_tabs) + if options.has_key?(:max_url) + @info_window_options = {:max_url => options.delete(:max_url) } + else + @info_window_options = {} + end + @options = options + end + #Creates a marker: If an info_window or info_window_tabs is present, the response to the click action from the user is setup here. + def create + if @options.empty? + creation = "new GMarker(#{MappingObject.javascriptify_variable(@point)})" + else + creation = "new GMarker(#{MappingObject.javascriptify_variable(@point)},#{MappingObject.javascriptify_variable(@options)})" + end + if @info_window && @info_window.is_a?(String) + creation = "addInfoWindowToMarker(#{creation},#{MappingObject.javascriptify_variable(@info_window)},#{MappingObject.javascriptify_variable(@info_window_options)})" + elsif @info_window_tabs && @info_window_tabs.is_a?(Hash) + creation = "addInfoWindowTabsToMarker(#{creation},#{MappingObject.javascriptify_variable(@info_window_tabs.to_a.collect{|kv| GInfoWindowTab.new(kv[0],kv[1] ) })},#{MappingObject.javascriptify_variable(@info_window_options)})" + elsif @info_window_tabs + creation = "addInfoWindowTabsToMarker(#{creation},#{MappingObject.javascriptify_variable(Array(@info_window_tabs))},#{MappingObject.javascriptify_variable(@info_window_options)})" + end + if @address.nil? + creation + else + "addGeocodingToMarker(#{creation},#{MappingObject.javascriptify_variable(@address)})" + end + end + end + + #Represents a tab to be displayed in a bubble when a marker is clicked on. + class GInfoWindowTab < Struct.new(:tab,:content) + include MappingObject + def create + "new GInfoWindowTab(#{MappingObject.javascriptify_variable(tab)},#{MappingObject.javascriptify_variable(content)})" + end + end + + #Represents a definition of an icon. You can pass rubyfied versions of the attributes detailed in the Google Maps API documentation. You can initialize global icons to be used in the application by passing a icon object, along with a variable name, to GMap#icon_init. If you want to declare an icon outside this, you will need to declare it first, since the JavaScript constructor does not accept any argument. + class GIcon + include MappingObject + DEFAULT = Variable.new("G_DEFAULT_ICON") + attr_accessor :options, :copy_base + + #Options can contain all the attributes (in rubyfied format) of a GIcon object (see Google's doc), as well as :copy_base, which indicates if the icon is copied from another one. + def initialize(options = {}) + @copy_base = options.delete(:copy_base) + @options = options + end + #Creates a GIcon. + def create + if @copy_base + c = "new GIcon(#{MappingObject.javascriptify_variable(@copy_base)})" + else + c = "new GIcon()" + end + if !options.empty? + "addOptionsToIcon(#{c},#{MappingObject.javascriptify_variable(@options)})" + else + c + end + end + end + + #A polyline. + class GPolyline + include MappingObject + attr_accessor :points,:color,:weight,:opacity + #Can take an array of +GLatLng+ or an array of 2D arrays. A method to directly build a polyline from a GeoRuby linestring is provided in the helper.rb file. + def initialize(points,color = nil,weight = nil,opacity = nil) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GLatLng.new(pt) } + else + @points = points + end + @color = color + @weight = weight + @opacity = opacity + end + #Creates a new polyline. + def create + a = "new GPolyline(#{MappingObject.javascriptify_variable(points)}" + a << ",#{MappingObject.javascriptify_variable(@color)}" if @color + a << ",#{MappingObject.javascriptify_variable(@weight)}" if @weight + a << ",#{MappingObject.javascriptify_variable(@opacity)}" if @opacity + a << ")" + end + end + + #Encoded GPolyline class + class GPolylineEncoded + include MappingObject + attr_accessor :points,:color,:weight,:opacity,:levels,:zoom_factor,:num_levels + + def initialize(options={}) + #points = options[:points] + #if !points.empty? and points[0].is_a?(Array) + # @points = points.collect { |pt| GLatLng.new(pt) } + #else + #@points = points + #end + @points = options[:points] + @color = options[:color] + @weight = options[:weight] + @opacity = options[:opacity] + @levels = options[:levels] || "BBBBBBBBBBBB" + @zoom_factor = options[:zoom_factor] || 32 + @num_levels = options[:num_levels] || 4 + end + def create + a = "new GPolyline.fromEncoded({points: #{MappingObject.javascriptify_variable(points)},\n" + a << "levels: #{MappingObject.javascriptify_variable(@levels)}," + a << "zoomFactor: #{MappingObject.javascriptify_variable(@zoom_factor)}," + a << "numLevels: #{MappingObject.javascriptify_variable(@num_levels)}" + a << ",color: #{MappingObject.javascriptify_variable(@color)}" if @color + a << ",weight: #{MappingObject.javascriptify_variable(@weight)}" if @weight + a << ",opacity: #{MappingObject.javascriptify_variable(@opacity)}" if @opacity + a << "})" + end + end + + #A basic Latitude/longitude point. + class GLatLng + include MappingObject + attr_accessor :lat,:lng,:unbounded + + def initialize(latlng,unbounded = nil) + @lat = latlng[0] + @lng = latlng[1] + @unbounded = unbounded + end + def create + unless @unbounded + "new GLatLng(#{MappingObject.javascriptify_variable(@lat)},#{MappingObject.javascriptify_variable(@lng)})" + else + "new GLatLng(#{MappingObject.javascriptify_variable(@lat)},#{MappingObject.javascriptify_variable(@lng)},#{MappingObject.javascriptify_variable(@unbounded)})" + end + end + end + + #A rectangular bounding box, defined by its south-western and north-eastern corners. + class GLatLngBounds < Struct.new(:sw,:ne) + include MappingObject + def create + "new GLatLngBounds(#{MappingObject.javascriptify_variable(sw)},#{MappingObject.javascriptify_variable(ne)})" + end + end + + #Polygon. Not documented yet in the Google Maps API + class GPolygon + include MappingObject + + attr_accessor :points,:stroke_color,:stroke_weight,:stroke_opacity,:color,:opacity + + #Can take an array of +GLatLng+ or an array of 2D arrays. A method to directly build a polygon from a GeoRuby polygon is provided in the helper.rb file. + def initialize(points,stroke_color="#000000",stroke_weight=1,stroke_opacity=1.0,color="#ff0000",opacity=1.0,encoded=false) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GLatLng.new(pt) } + else + @points = points + end + @stroke_color = stroke_color + @stroke_weight = stroke_weight + @stroke_opacity = stroke_opacity + @color = color + @opacity = opacity + end + + #Creates a new polygon + def create + a = "new GPolygon(#{MappingObject.javascriptify_variable(points)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_color)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_weight)}" + a << ",#{MappingObject.javascriptify_variable(@stroke_opacity)}" + a << ",#{MappingObject.javascriptify_variable(@color)}" + a << ",#{MappingObject.javascriptify_variable(@opacity)}" + a << ")" + end + end + + class GPolygonEncoded + include MappingObject + + attr_accessor :polyline, :color, :opacity, :outline, :fill + + def initialize(polylines,fill=true,color="#000000",opacity=0.5,outline=false) + #force polylines to be an array + if polylines.is_a? Array + @polylines = polylines + else + @polylines = [polylines] + end + @color = color + @fill = fill + @opacity = opacity + @outline = outline + end + + #Creates a new polygon. + def create + polylines_for_polygon= [] + @polylines.each do |p| + x = "{points: #{MappingObject.javascriptify_variable(p.points)}," + x << "levels: #{MappingObject.javascriptify_variable(p.levels)}," + x << "zoomFactor: #{MappingObject.javascriptify_variable(p.zoom_factor)}," + x << "numLevels: #{MappingObject.javascriptify_variable(p.num_levels)} " + x << "}" + polylines_for_polygon << x + end + + polylines_for_polygon = "[" + polylines_for_polygon.join(",") + "]" + + a = "new GPolygon.fromEncoded({polylines: #{polylines_for_polygon}," + a << "fill: #{MappingObject.javascriptify_variable(@fill)}," + a << "color: #{MappingObject.javascriptify_variable(@color)}," + a << "opacity: #{MappingObject.javascriptify_variable(@opacity)}," + a << "outline: #{MappingObject.javascriptify_variable(@outline)}" + a << "})" + end + end + + class ELabel + attr_accessor :point, :text, :style + include MappingObject + + def initialize(point, text=nil, style=nil) + @point = point + @text = text + @style = style + end + + def create + a = "new ELabel(#{MappingObject.javascriptify_variable(@point)}" + a << ",#{MappingObject.javascriptify_variable(@text)}" if @text + a << ",#{MappingObject.javascriptify_variable(@style)}" if @style + a << ")" + end + end + + + #A GGeoXml object gets data from a GeoRSS or KML feed and displays it. Use overlay_init to add it to a map at initialization time. + class GGeoXml + include MappingObject + + attr_accessor :url + + def initialize(url) + @url = url + end + + def create + "new GGeoXml(#{MappingObject.javascriptify_variable(@url)})" + end + + end + + #A GOverlay representing a group of GMarkers. The GMarkers can be identified with an id, which can be used to show the info window of a specific marker, in reponse, for example, to a click on a link. The whole group can be shown on and off at once. It should be declared global at initialization time to be useful. + class GMarkerGroup + include MappingObject + attr_accessor :active, :markers, :markers_by_id + + def initialize(active = true , markers = nil) + @active = active + @markers = [] + @markers_by_id = {} + if markers.is_a?(Array) + @markers = markers + elsif markers.is_a?(Hash) + @markers_by_id = markers + end + end + + def create + "new GMarkerGroup(#{MappingObject.javascriptify_variable(@active)},#{MappingObject.javascriptify_variable(@markers)},#{MappingObject.javascriptify_variable(@markers_by_id)})" + end + end + + #Can be used to implement a clusterer, similar to the clusterer below, except that there is more stuff to manage explicitly byt the programmer (but this is also more flexible). See the README for usage esamples. + class GMarkerManager + include MappingObject + + attr_accessor :map,:options,:managed_markers + + #options can be :border_padding, :max_zoom, :track_markers and :managed_markers: managed_markers must be an array of ManagedMarker objects + def initialize(map, options = {}) + @map = map + @managed_markers = Array(options.delete(:managed_markers)) #[] if nil + @options = options + end + + def create + puts @options.inspect + "addMarkersToManager(new GMarkerManager(#{MappingObject.javascriptify_variable(@map)},#{MappingObject.javascriptify_variable(@options)}),#{MappingObject.javascriptify_variable(@managed_markers)})" + end + + end + + #A set of similarly managed markers: They share the same minZoom and maxZoom. + class ManagedMarker + include MappingObject + + attr_accessor :markers,:min_zoom, :max_zoom + + def initialize(markers,min_zoom,max_zoom = nil) + @markers = markers + @min_zoom = min_zoom + @max_zoom = max_zoom + end + + def create + "new ManagedMarker(#{MappingObject.javascriptify_variable(@markers)},#{MappingObject.javascriptify_variable(@min_zoom)},#{MappingObject.javascriptify_variable(@max_zoom)})" + end + + end + + #Makes the link with the Clusterer2 library by Jef Poskanzer (slightly modified though). Is a GOverlay making clusters out of its GMarkers, so that GMarkers very close to each other appear as one when the zoom is low. When the zoom gets higher, the individual markers are drawn. + class Clusterer + include MappingObject + attr_accessor :markers,:icon, :max_visible_markers, :grid_size, :min_markers_per_cluster , :max_lines_per_info_box + + def initialize(markers = [], options = {}) + @markers = markers + @icon = options[:icon] || GIcon::DEFAULT + @max_visible_markers = options[:max_visible_markers] || 150 + @grid_size = options[:grid_size] || 5 + @min_markers_per_cluster = options[:min_markers_per_cluster] || 5 + @max_lines_per_info_box = options[:max_lines_per_info_box] || 10 + end + + def create + js_marker = '[' + @markers.collect do |marker| + add_description(marker) + end.join(",") + ']' + + "new Clusterer(#{js_marker},#{MappingObject.javascriptify_variable(@icon)},#{MappingObject.javascriptify_variable(@max_visible_markers)},#{MappingObject.javascriptify_variable(@grid_size)},#{MappingObject.javascriptify_variable(@min_markers_per_cluster)},#{MappingObject.javascriptify_variable(@max_lines_per_info_box)})" + end + + private + def add_description(marker) + "addDescriptionToMarker(#{MappingObject.javascriptify_variable(marker)},#{MappingObject.javascriptify_variable(marker.options[:description] || marker.options[:title] || '')})" + end + end + + #Makes the link with the MGeoRSS extension by Mikel Maron (a bit modified though). It lets you overlay on top of Google Maps the items present in a RSS feed that has GeoRss data. This data can be either in W3C Geo vocabulary or in the GeoRss Simple format. See http://georss.org to know more about GeoRss. + class GeoRssOverlay + include MappingObject + attr_accessor :url, :proxy, :icon, :options + + #You can pass the following options: + #- :icon: An icon for the items of the feed. Defaults to the classic red balloon icon. + #- :proxy: An URL on your server where fetching the RSS feed will be taken care of. + #- :list_div: In case you want a list of all the markers, with a link on which you can click in order to display the info on the marker, use this option to indicate the ID of the div (that you must place yourself). + #- :list_item_class: class of the DIV containing each item of the list. Ignored if option :list_div is not set. + #- :limit: Maximum number of items to display on the map. + #- :content_div: Instead of having an info window appear, indicates the ID of the DIV where this info should be displayed. + def initialize(url, options = {}) + @url = url + @icon = options.delete(:icon) || GIcon::DEFAULT + @proxy = options.delete(:proxy) || Variable::UNDEFINED + @options = options + end + + def create + "new GeoRssOverlay(#{MappingObject.javascriptify_variable(@url)},#{MappingObject.javascriptify_variable(@icon)},#{MappingObject.javascriptify_variable(@proxy)},#{MappingObject.javascriptify_variable(@options)})" + end + end + + end +end diff --git a/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb b/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb new file mode 100644 index 0000000..bcde4e2 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/gm_plugin/point.rb @@ -0,0 +1,34 @@ +module Ym4r + module GmPlugin + #A point in pixel coordinates + class GPoint < Struct.new(:x,:y) + include MappingObject + def create + "new GPoint(#{x},#{y})" + end + end + #A rectangular that contains all the pixel points passed as arguments + class GBounds + include MappingObject + attr_accessor :points + #Accepts both an array of GPoint and an array of 2-element arrays + def initialize(points) + if !points.empty? and points[0].is_a?(Array) + @points = points.collect { |pt| GPoint.new(pt[0],pt[1]) } + else + @points = points + end + end + def create + "new GBounds([#{@points.map { |pt| pt.to_javascript}.join(",")}])" + end + end + #A size object, in pixel space + class GSize < Struct.new(:width,:height) + include MappingObject + def create + "new GSize(#{width},#{height})" + end + end + end +end diff --git a/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb b/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb new file mode 100644 index 0000000..24c9068 --- /dev/null +++ b/vendor/plugins/ym4r_gm/lib/ym4r_gm.rb @@ -0,0 +1,11 @@ +require 'gm_plugin/key' +require 'gm_plugin/mapping' +require 'gm_plugin/map' +require 'gm_plugin/control' +require 'gm_plugin/point' +require 'gm_plugin/overlay' +require 'gm_plugin/layer' +require 'gm_plugin/helper' +require 'gm_plugin/geocoding' + +include Ym4r::GmPlugin diff --git a/vendor/plugins/ym4r_gm/rakefile.rb b/vendor/plugins/ym4r_gm/rakefile.rb new file mode 100644 index 0000000..e2d46bf --- /dev/null +++ b/vendor/plugins/ym4r_gm/rakefile.rb @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the gm plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the gm plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'ym4r_gm-doc' + rdoc.title = 'GM' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake b/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake new file mode 100644 index 0000000..40745d0 --- /dev/null +++ b/vendor/plugins/ym4r_gm/tasks/gm_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :gm do +# # Task goes here +# end \ No newline at end of file diff --git a/vendor/plugins/ym4r_gm/test/gm_test.rb b/vendor/plugins/ym4r_gm/test/gm_test.rb new file mode 100644 index 0000000..cea8447 --- /dev/null +++ b/vendor/plugins/ym4r_gm/test/gm_test.rb @@ -0,0 +1,79 @@ +$:.unshift(File.dirname(__FILE__) + '/../lib') + +require File.expand_path(File.dirname(__FILE__) + "/../../../../config/environment") + + +require 'ym4r_gm' +require 'test/unit' + +include Ym4r::GmPlugin + +class TestGoogleMaps< Test::Unit::TestCase + def test_javascriptify_method + assert_equal("addOverlayToHello",MappingObject::javascriptify_method("add_overlay_to_hello")) + end + + def test_javascriptify_variable_mapping_object + map = GMap.new("div") + assert_equal(map.to_javascript,MappingObject::javascriptify_variable(map)) + end + + def test_javascriptify_variable_numeric + assert_equal("123.4",MappingObject::javascriptify_variable(123.4)) + end + + def test_javascriptify_variable_array + map = GMap.new("div") + assert_equal("[123.4,#{map.to_javascript},[123.4,#{map.to_javascript}]]",MappingObject::javascriptify_variable([123.4,map,[123.4,map]])) + end + + def test_javascriptify_variable_hash + map = GMap.new("div") + test_str = MappingObject::javascriptify_variable("hello" => map, "chopotopoto" => [123.55,map]) + assert("{hello : #{map.to_javascript},chopotopoto : [123.55,#{map.to_javascript}]}" == test_str || "{chopotopoto : [123.55,#{map.to_javascript}],hello : #{map.to_javascript}}" == test_str) + end + + def test_method_call_on_mapping_object + map = GMap.new("div","map") + assert_equal("map.addHello(123.4);",map.add_hello(123.4).to_s) + end + + def test_nested_calls_on_mapping_object + gmap = GMap.new("div","map") + assert_equal("map.addHello(map.hoYoYo(123.4),map);",gmap.add_hello(gmap.ho_yo_yo(123.4),gmap).to_s) + end + + def test_declare_variable_latlng + point = GLatLng.new([123.4,123.6]) + assert_equal("var point = new GLatLng(123.4,123.6);",point.declare("point")) + assert_equal("point",point.variable) + end + + def test_array_indexing + obj = Variable.new("obj") + assert_equal("obj[0]",obj[0].variable) + end + + def test_google_maps_geocoding + + + placemarks = Geocoding.get("Rue Clovis Paris") + assert_equal(Geocoding::GEO_SUCCESS,placemarks.status) + assert_equal(1,placemarks.length) + placemark = placemarks[0] + assert_equal("FR",placemark.country_code) + assert_equal("Paris",placemark.locality) + assert_equal("75005",placemark.postal_code) + + #test iwht multiple placemarks + placemarks = Geocoding.get('hoogstraat, nl') + assert_equal(Geocoding::GEO_SUCCESS,placemarks.status) + assert(placemarks.length > 1) + assert(placemarks[0].latitude != placemarks[1].latitude ) + + + end + + +end +