summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJean-Philippe Lang <jp_lang@yahoo.fr>2008-08-30 14:41:07 +0000
committerJean-Philippe Lang <jp_lang@yahoo.fr>2008-08-30 14:41:07 +0000
commita377069fb2f208213c07e949aa20a36d4cd8cdbf (patch)
tree21a642295e9fb5b08cf59c91d1e0814dea0dbaf2
parentab83ed5d8eb9c07f53a5ba3a285d552f8eb34c42 (diff)
downloadredmine-a377069fb2f208213c07e949aa20a36d4cd8cdbf.tar.gz
redmine-a377069fb2f208213c07e949aa20a36d4cd8cdbf.zip
Merged trunk r1773.
git-svn-id: http://redmine.rubyforge.org/svn/branches/work@1774 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r--groups/app/controllers/account_controller.rb79
-rw-r--r--groups/app/controllers/admin_controller.rb1
-rw-r--r--groups/app/controllers/application.rb33
-rw-r--r--groups/app/controllers/attachments_controller.rb28
-rw-r--r--groups/app/controllers/auth_sources_controller.rb1
-rw-r--r--groups/app/controllers/boards_controller.rb1
-rw-r--r--groups/app/controllers/custom_fields_controller.rb27
-rw-r--r--groups/app/controllers/documents_controller.rb10
-rw-r--r--groups/app/controllers/enumerations_controller.rb22
-rw-r--r--groups/app/controllers/issue_categories_controller.rb1
-rw-r--r--groups/app/controllers/issue_relations_controller.rb1
-rw-r--r--groups/app/controllers/issue_statuses_controller.rb1
-rw-r--r--groups/app/controllers/issues_controller.rb79
-rw-r--r--groups/app/controllers/journals_controller.rb1
-rw-r--r--groups/app/controllers/mail_handler_controller.rb44
-rw-r--r--groups/app/controllers/members_controller.rb1
-rw-r--r--groups/app/controllers/messages_controller.rb17
-rw-r--r--groups/app/controllers/my_controller.rb5
-rw-r--r--groups/app/controllers/news_controller.rb3
-rw-r--r--groups/app/controllers/projects_controller.rb137
-rw-r--r--groups/app/controllers/queries_controller.rb1
-rw-r--r--groups/app/controllers/reports_controller.rb1
-rw-r--r--groups/app/controllers/repositories_controller.rb78
-rw-r--r--groups/app/controllers/roles_controller.rb1
-rw-r--r--groups/app/controllers/search_controller.rb83
-rw-r--r--groups/app/controllers/settings_controller.rb4
-rw-r--r--groups/app/controllers/timelog_controller.rb23
-rw-r--r--groups/app/controllers/trackers_controller.rb1
-rw-r--r--groups/app/controllers/users_controller.rb21
-rw-r--r--groups/app/controllers/versions_controller.rb10
-rw-r--r--groups/app/controllers/watchers_controller.rb51
-rw-r--r--groups/app/controllers/welcome_controller.rb1
-rw-r--r--groups/app/controllers/wiki_controller.rb30
-rw-r--r--groups/app/controllers/wikis_controller.rb1
-rw-r--r--groups/app/helpers/application_helper.rb98
-rw-r--r--groups/app/helpers/attachments_helper.rb4
-rw-r--r--groups/app/helpers/custom_fields_helper.rb28
-rw-r--r--groups/app/helpers/issues_helper.rb18
-rw-r--r--groups/app/helpers/journals_helper.rb11
-rw-r--r--groups/app/helpers/mail_handler_helper.rb19
-rw-r--r--groups/app/helpers/projects_helper.rb8
-rw-r--r--groups/app/helpers/repositories_helper.rb30
-rw-r--r--groups/app/helpers/search_helper.rb27
-rw-r--r--groups/app/helpers/settings_helper.rb1
-rw-r--r--groups/app/helpers/sort_helper.rb2
-rw-r--r--groups/app/helpers/timelog_helper.rb17
-rw-r--r--groups/app/helpers/users_helper.rb31
-rw-r--r--groups/app/helpers/watchers_helper.rb7
-rw-r--r--groups/app/helpers/wiki_helper.rb16
-rw-r--r--groups/app/models/attachment.rb41
-rw-r--r--groups/app/models/auth_source.rb5
-rw-r--r--groups/app/models/auth_source_ldap.rb5
-rw-r--r--groups/app/models/change.rb4
-rw-r--r--groups/app/models/changeset.rb31
-rw-r--r--groups/app/models/custom_field.rb6
-rw-r--r--groups/app/models/custom_value.rb5
-rw-r--r--groups/app/models/document.rb5
-rw-r--r--groups/app/models/enumeration.rb38
-rw-r--r--groups/app/models/issue.rb39
-rw-r--r--groups/app/models/issue_relation.rb2
-rw-r--r--groups/app/models/journal.rb15
-rw-r--r--groups/app/models/mail_handler.rb141
-rw-r--r--groups/app/models/mailer.rb33
-rw-r--r--groups/app/models/message.rb11
-rw-r--r--groups/app/models/news.rb5
-rw-r--r--groups/app/models/project.rb48
-rw-r--r--groups/app/models/query.rb91
-rw-r--r--groups/app/models/repository.rb37
-rw-r--r--groups/app/models/repository/bazaar.rb5
-rw-r--r--groups/app/models/repository/cvs.rb18
-rw-r--r--groups/app/models/repository/darcs.rb14
-rw-r--r--groups/app/models/repository/filesystem.rb43
-rw-r--r--groups/app/models/repository/git.rb6
-rw-r--r--groups/app/models/repository/subversion.rb15
-rw-r--r--groups/app/models/setting.rb39
-rw-r--r--groups/app/models/time_entry.rb16
-rw-r--r--groups/app/models/time_entry_custom_field.rb23
-rw-r--r--groups/app/models/user.rb40
-rw-r--r--groups/app/models/user_preference.rb6
-rw-r--r--groups/app/models/watcher.rb7
-rw-r--r--groups/app/models/wiki.rb2
-rw-r--r--groups/app/models/wiki_content.rb11
-rw-r--r--groups/app/models/wiki_page.rb28
-rw-r--r--groups/app/views/account/login.rhtml1
-rw-r--r--groups/app/views/account/register.rhtml8
-rw-r--r--groups/app/views/account/show.rhtml10
-rw-r--r--groups/app/views/attachments/_links.rhtml2
-rw-r--r--groups/app/views/attachments/diff.rhtml15
-rw-r--r--groups/app/views/attachments/file.rhtml15
-rw-r--r--groups/app/views/auth_sources/_form.rhtml8
-rw-r--r--groups/app/views/boards/index.rhtml2
-rw-r--r--groups/app/views/boards/show.rhtml4
-rw-r--r--groups/app/views/common/_diff.rhtml64
-rw-r--r--groups/app/views/common/_file.rhtml11
-rw-r--r--groups/app/views/common/_preview.rhtml2
-rw-r--r--groups/app/views/common/feed.atom.rxml10
-rw-r--r--groups/app/views/custom_fields/_form.rhtml3
-rw-r--r--groups/app/views/enumerations/destroy.rhtml12
-rw-r--r--groups/app/views/enumerations/list.rhtml9
-rw-r--r--groups/app/views/issues/_edit.rhtml8
-rw-r--r--groups/app/views/issues/_form.rhtml2
-rw-r--r--groups/app/views/issues/_form_custom_fields.rhtml15
-rw-r--r--groups/app/views/issues/_history.rhtml3
-rw-r--r--groups/app/views/issues/_list.rhtml2
-rw-r--r--groups/app/views/issues/_pdf.rfpdf118
-rw-r--r--groups/app/views/issues/context_menu.rhtml64
-rw-r--r--groups/app/views/issues/index.rhtml2
-rw-r--r--groups/app/views/issues/show.rfpdf118
-rw-r--r--groups/app/views/issues/show.rhtml41
-rw-r--r--groups/app/views/layouts/base.rhtml2
-rw-r--r--groups/app/views/mailer/layout.text.html.rhtml32
-rw-r--r--groups/app/views/mailer/lost_password.text.html.rhtml2
-rw-r--r--groups/app/views/mailer/lost_password.text.plain.rhtml2
-rw-r--r--groups/app/views/mailer/reminder.text.html.rhtml9
-rw-r--r--groups/app/views/mailer/reminder.text.plain.rhtml7
-rw-r--r--groups/app/views/messages/show.rhtml10
-rw-r--r--groups/app/views/my/blocks/_documents.rhtml5
-rw-r--r--groups/app/views/news/edit.rhtml2
-rw-r--r--groups/app/views/news/index.rhtml2
-rw-r--r--groups/app/views/news/new.rhtml2
-rw-r--r--groups/app/views/news/show.rhtml6
-rw-r--r--groups/app/views/projects/_form.rhtml14
-rw-r--r--groups/app/views/projects/activity.rhtml14
-rw-r--r--groups/app/views/projects/calendar.rhtml2
-rw-r--r--groups/app/views/projects/gantt.rfpdf4
-rw-r--r--groups/app/views/projects/gantt.rhtml9
-rw-r--r--groups/app/views/projects/index.rhtml (renamed from groups/app/views/projects/list.rhtml)10
-rw-r--r--groups/app/views/projects/list_files.rhtml3
-rw-r--r--groups/app/views/projects/roadmap.rhtml8
-rw-r--r--groups/app/views/projects/settings/_repository.rhtml2
-rw-r--r--groups/app/views/projects/show.rhtml4
-rw-r--r--groups/app/views/queries/_filters.rhtml2
-rw-r--r--groups/app/views/repositories/_dir_list_content.rhtml28
-rw-r--r--groups/app/views/repositories/_navigation.rhtml4
-rw-r--r--groups/app/views/repositories/_revisions.rhtml2
-rw-r--r--groups/app/views/repositories/browse.rhtml1
-rw-r--r--groups/app/views/repositories/changes.rhtml13
-rw-r--r--groups/app/views/repositories/diff.rhtml82
-rw-r--r--groups/app/views/repositories/entry.rhtml12
-rw-r--r--groups/app/views/repositories/revision.rhtml14
-rw-r--r--groups/app/views/repositories/show.rhtml7
-rw-r--r--groups/app/views/repositories/stats.rhtml15
-rw-r--r--groups/app/views/roles/_form.rhtml4
-rw-r--r--groups/app/views/roles/report.rhtml18
-rw-r--r--groups/app/views/search/index.rhtml30
-rw-r--r--groups/app/views/settings/_mail_handler.rhtml18
-rw-r--r--groups/app/views/settings/_notifications.rhtml10
-rw-r--r--groups/app/views/settings/_repositories.rhtml10
-rw-r--r--groups/app/views/timelog/_list.rhtml4
-rw-r--r--groups/app/views/timelog/_report_criteria.rhtml2
-rw-r--r--groups/app/views/timelog/details.rhtml5
-rw-r--r--groups/app/views/timelog/edit.rhtml5
-rw-r--r--groups/app/views/users/_form.rhtml9
-rw-r--r--groups/app/views/users/_general.rhtml4
-rw-r--r--groups/app/views/users/_memberships.rhtml53
-rw-r--r--groups/app/views/users/edit.rhtml31
-rw-r--r--groups/app/views/users/list.rhtml10
-rw-r--r--groups/app/views/versions/show.rhtml2
-rw-r--r--groups/app/views/watchers/_watchers.rhtml25
-rw-r--r--groups/app/views/welcome/index.rhtml6
-rw-r--r--groups/app/views/wiki/export.rhtml4
-rw-r--r--groups/app/views/wiki/history.rhtml4
-rw-r--r--groups/app/views/wiki/rename.rhtml3
-rw-r--r--groups/app/views/wiki/show.rhtml10
-rw-r--r--groups/app/views/wiki/special_page_index.rhtml6
-rw-r--r--groups/config/boot.rb118
-rw-r--r--groups/config/database.yml.example3
-rw-r--r--groups/config/email.yml.example21
-rw-r--r--groups/config/environment.rb57
-rw-r--r--groups/config/environments/test.rb1
-rw-r--r--groups/config/environments/test_pgsql.rb3
-rw-r--r--groups/config/environments/test_sqlite3.rb3
-rw-r--r--groups/config/initializers/10-patches.rb17
-rw-r--r--groups/config/initializers/20-mime_types.rb4
-rw-r--r--groups/config/initializers/30-redmine.rb7
-rw-r--r--groups/config/initializers/40-email.rb17
-rw-r--r--groups/config/routes.rb5
-rw-r--r--groups/config/settings.yml18
-rw-r--r--groups/db/migrate/001_setup.rb19
-rw-r--r--groups/db/migrate/072_add_enumerations_position.rb2
-rw-r--r--groups/db/migrate/078_add_custom_fields_position.rb2
-rw-r--r--groups/db/migrate/096_add_wiki_pages_protected.rb9
-rw-r--r--groups/db/migrate/097_change_projects_homepage_limit.rb9
-rw-r--r--groups/db/migrate/098_add_wiki_pages_parent_id.rb9
-rw-r--r--groups/doc/CHANGELOG80
-rw-r--r--groups/doc/INSTALL40
-rw-r--r--groups/doc/README_FOR_APP5
-rw-r--r--groups/doc/RUNNING_TESTS8
-rw-r--r--groups/doc/UPGRADING8
-rw-r--r--groups/extra/mail_handler/rdm-mailhandler.rb125
-rw-r--r--groups/extra/sample_plugin/app/models/meeting.rb11
-rw-r--r--groups/extra/sample_plugin/db/migrate/001_create_meetings.rb15
-rw-r--r--groups/extra/sample_plugin/db/migrate/001_create_some_models.rb13
-rw-r--r--groups/extra/sample_plugin/init.rb5
-rw-r--r--groups/extra/sample_plugin/lang/en.yml1
-rw-r--r--groups/extra/sample_plugin/lang/fr.yml1
-rw-r--r--groups/extra/svn/Redmine.pm137
-rw-r--r--groups/lang/bg.yml18
-rw-r--r--groups/lang/cs.yml18
-rw-r--r--groups/lang/da.yml18
-rw-r--r--groups/lang/de.yml18
-rw-r--r--groups/lang/en.yml20
-rw-r--r--groups/lang/es.yml18
-rw-r--r--groups/lang/fi.yml176
-rw-r--r--groups/lang/fr.yml22
-rw-r--r--groups/lang/he.yml18
-rw-r--r--groups/lang/hu.yml639
-rw-r--r--groups/lang/it.yml430
-rw-r--r--groups/lang/ja.yml18
-rw-r--r--groups/lang/ko.yml18
-rw-r--r--groups/lang/lt.yml100
-rw-r--r--groups/lang/nl.yml18
-rw-r--r--groups/lang/no.yml22
-rw-r--r--groups/lang/pl.yml52
-rw-r--r--groups/lang/pt-br.yml814
-rw-r--r--groups/lang/pt.yml20
-rw-r--r--groups/lang/ro.yml20
-rw-r--r--groups/lang/ru.yml44
-rw-r--r--groups/lang/sr.yml18
-rw-r--r--groups/lang/sv.yml18
-rw-r--r--groups/lang/th.yml641
-rw-r--r--groups/lang/uk.yml18
-rw-r--r--groups/lang/zh-tw.yml66
-rw-r--r--groups/lang/zh.yml32
-rw-r--r--groups/lib/SVG/Graph/Graph.rb2
-rw-r--r--groups/lib/redcloth.rb56
-rw-r--r--groups/lib/redmine.rb35
-rw-r--r--groups/lib/redmine/activity.rb46
-rw-r--r--groups/lib/redmine/activity/fetcher.rb79
-rw-r--r--groups/lib/redmine/core_ext/string/conversions.rb2
-rw-r--r--groups/lib/redmine/imap.rb51
-rw-r--r--groups/lib/redmine/menu_manager.rb64
-rw-r--r--groups/lib/redmine/platform.rb26
-rw-r--r--groups/lib/redmine/plugin.rb26
-rw-r--r--groups/lib/redmine/scm/adapters/abstract_adapter.rb235
-rw-r--r--groups/lib/redmine/scm/adapters/bazaar_adapter.rb4
-rw-r--r--groups/lib/redmine/scm/adapters/cvs_adapter.rb16
-rw-r--r--groups/lib/redmine/scm/adapters/darcs_adapter.rb43
-rw-r--r--groups/lib/redmine/scm/adapters/filesystem_adapter.rb93
-rw-r--r--groups/lib/redmine/scm/adapters/git_adapter.rb47
-rw-r--r--groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl12
-rw-r--r--groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl12
-rw-r--r--groups/lib/redmine/scm/adapters/mercurial_adapter.rb174
-rw-r--r--groups/lib/redmine/scm/adapters/subversion_adapter.rb48
-rw-r--r--groups/lib/redmine/unified_diff.rb178
-rw-r--r--groups/lib/redmine/wiki_formatting.rb46
-rw-r--r--groups/lib/redmine/wiki_formatting/macros.rb6
-rw-r--r--groups/lib/tabular_form_builder.rb2
-rw-r--r--groups/lib/tasks/email.rake105
-rw-r--r--groups/lib/tasks/migrate_from_trac.rake49
-rw-r--r--groups/lib/tasks/reminder.rake39
-rw-r--r--groups/lib/tasks/testing.rake46
-rw-r--r--groups/public/help/wiki_syntax.html26
-rw-r--r--groups/public/images/bullet_toggle_minus.pngbin0 -> 335 bytes
-rw-r--r--groups/public/images/bullet_toggle_plus.pngbin0 -> 335 bytes
-rw-r--r--groups/public/images/comment.pngbin0 -> 413 bytes
-rw-r--r--groups/public/images/expand.pngbin266 -> 0 bytes
-rw-r--r--groups/public/images/jstoolbar/bt_bq.pngbin0 -> 503 bytes
-rw-r--r--groups/public/images/jstoolbar/bt_bq_remove.pngbin0 -> 501 bytes
-rw-r--r--groups/public/images/locked.pngbin566 -> 1127 bytes
-rw-r--r--groups/public/images/projects.pngbin690 -> 811 bytes
-rw-r--r--groups/public/images/ticket_note.pngbin0 -> 784 bytes
-rw-r--r--groups/public/images/unlock.pngbin643 -> 618 bytes
-rw-r--r--groups/public/javascripts/application.js32
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-he.js2
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-hu.js127
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-pt-br.js34
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-th.js127
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-zh-tw.js12
-rw-r--r--groups/public/javascripts/calendar/lang/calendar-zh.js40
-rw-r--r--groups/public/javascripts/context_menu.js11
-rw-r--r--groups/public/javascripts/controls.js838
-rw-r--r--groups/public/javascripts/dragdrop.js146
-rw-r--r--groups/public/javascripts/effects.js760
-rw-r--r--groups/public/javascripts/jstoolbar/jstoolbar.js33
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js16
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js30
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js16
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js2
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js4
-rw-r--r--groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js28
-rw-r--r--groups/public/javascripts/prototype.js3768
-rw-r--r--groups/public/stylesheets/application.css81
-rw-r--r--groups/public/stylesheets/context_menu.css10
-rw-r--r--groups/public/stylesheets/jstoolbar.css6
-rw-r--r--groups/public/stylesheets/scm.css6
-rw-r--r--groups/script/dbconsole3
-rw-r--r--groups/script/performance/request3
-rw-r--r--groups/script/process/inspector3
-rw-r--r--groups/test/fixtures/attachments.yml49
-rw-r--r--groups/test/fixtures/changes.yml7
-rw-r--r--groups/test/fixtures/custom_fields.yml13
-rw-r--r--groups/test/fixtures/enabled_modules.yml4
-rw-r--r--groups/test/fixtures/enumerations.yml6
-rw-r--r--groups/test/fixtures/files/060719210727_archive.zipbin0 -> 157 bytes
-rw-r--r--groups/test/fixtures/files/060719210727_changeset.diff13
-rw-r--r--groups/test/fixtures/files/060719210727_source.rb10
-rw-r--r--groups/test/fixtures/issue_categories.yml11
-rw-r--r--groups/test/fixtures/issues.yml26
-rw-r--r--groups/test/fixtures/mail_handler/add_note_to_issue.txt14
-rw-r--r--groups/test/fixtures/mail_handler/ticket_on_given_project.eml42
-rw-r--r--groups/test/fixtures/mail_handler/ticket_reply.eml73
-rw-r--r--groups/test/fixtures/mail_handler/ticket_reply_with_status.eml75
-rw-r--r--groups/test/fixtures/mail_handler/ticket_with_attachment.eml248
-rw-r--r--groups/test/fixtures/mail_handler/ticket_with_attributes.eml43
-rw-r--r--groups/test/fixtures/members.yml10
-rw-r--r--groups/test/fixtures/projects.yml14
-rw-r--r--groups/test/fixtures/projects_trackers.yml4
-rw-r--r--groups/test/fixtures/repositories/filesystem_repository.tar.gzbin0 -> 265 bytes
-rw-r--r--groups/test/fixtures/roles.yml6
-rw-r--r--groups/test/fixtures/versions.yml2
-rw-r--r--groups/test/fixtures/watchers.yml6
-rw-r--r--groups/test/fixtures/wiki_contents.yml2
-rw-r--r--groups/test/fixtures/wiki_pages.yml8
-rw-r--r--groups/test/functional/account_controller_test.rb11
-rw-r--r--groups/test/functional/attachments_controller_test.rb79
-rw-r--r--groups/test/functional/documents_controller_test.rb2
-rw-r--r--groups/test/functional/enumerations_controller.rb61
-rw-r--r--groups/test/functional/issues_controller_test.rb176
-rw-r--r--groups/test/functional/mail_handler_controller_test.rb53
-rw-r--r--groups/test/functional/messages_controller_test.rb16
-rw-r--r--groups/test/functional/projects_controller_test.rb165
-rw-r--r--groups/test/functional/repositories_controller_test.rb4
-rw-r--r--groups/test/functional/repositories_cvs_controller_test.rb15
-rw-r--r--groups/test/functional/repositories_git_controller_test.rb2
-rw-r--r--groups/test/functional/repositories_subversion_controller_test.rb53
-rw-r--r--groups/test/functional/search_controller_test.rb30
-rw-r--r--groups/test/functional/timelog_controller_test.rb24
-rw-r--r--groups/test/functional/versions_controller_test.rb6
-rw-r--r--groups/test/functional/watchers_controller_test.rb70
-rw-r--r--groups/test/functional/welcome_controller_test.rb14
-rw-r--r--groups/test/functional/wiki_controller_test.rb99
-rw-r--r--groups/test/integration/account_test.rb56
-rw-r--r--groups/test/integration/admin_test.rb5
-rw-r--r--groups/test/integration/issues_test.rb21
-rw-r--r--groups/test/test_helper.rb20
-rw-r--r--groups/test/unit/activity_test.rb71
-rw-r--r--groups/test/unit/attachment_test.rb32
-rw-r--r--groups/test/unit/changeset_test.rb11
-rw-r--r--groups/test/unit/default_data_test.rb45
-rw-r--r--groups/test/unit/enumeration_test.rb45
-rw-r--r--groups/test/unit/filesystem_adapter_test.rb42
-rw-r--r--groups/test/unit/helpers/application_helper_test.rb160
-rw-r--r--groups/test/unit/issue_test.rb123
-rw-r--r--groups/test/unit/mail_handler_test.rb125
-rw-r--r--groups/test/unit/mailer_test.rb11
-rw-r--r--groups/test/unit/mercurial_adapter_test.rb53
-rw-r--r--groups/test/unit/project_test.rb3
-rw-r--r--groups/test/unit/query_test.rb101
-rw-r--r--groups/test/unit/repository_cvs_test.rb2
-rw-r--r--groups/test/unit/repository_darcs_test.rb7
-rw-r--r--groups/test/unit/repository_filesystem_test.rb54
-rw-r--r--groups/test/unit/repository_git_test.rb2
-rw-r--r--groups/test/unit/repository_mercurial_test.rb20
-rw-r--r--groups/test/unit/repository_test.rb20
-rw-r--r--groups/test/unit/role_test.rb2
-rw-r--r--groups/test/unit/search_test.rb143
-rw-r--r--groups/test/unit/subversion_adapter_test.rb33
-rw-r--r--groups/test/unit/tracker_test.rb2
-rw-r--r--groups/test/unit/user_test.rb6
-rw-r--r--groups/test/unit/wiki_page_test.rb44
-rw-r--r--groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb15
-rw-r--r--groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb66
-rw-r--r--groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb7
-rw-r--r--groups/vendor/plugins/acts_as_versioned/Rakefile362
-rw-r--r--groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb195
-rw-r--r--groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb27
-rw-r--r--groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb2
-rw-r--r--groups/vendor/plugins/acts_as_versioned/test/migration_test.rb22
-rw-r--r--groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb172
-rw-r--r--groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb18
-rw-r--r--groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb2
-rw-r--r--groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb4
-rw-r--r--groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb2
-rw-r--r--groups/vendor/plugins/rfpdf/init.rb8
-rw-r--r--groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb946
-rw-r--r--groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb12
398 files changed, 14338 insertions, 5285 deletions
diff --git a/groups/app/controllers/account_controller.rb b/groups/app/controllers/account_controller.rb
index b9224c158..4b2ec8317 100644
--- a/groups/app/controllers/account_controller.rb
+++ b/groups/app/controllers/account_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AccountController < ApplicationController
- layout 'base'
helper :custom_fields
include CustomFieldsHelper
@@ -26,7 +25,7 @@ class AccountController < ApplicationController
# Show user's account
def show
@user = User.find_active(params[:id])
- @custom_values = @user.custom_values.find(:all, :include => :custom_field)
+ @custom_values = @user.custom_values
# show only public projects and private projects that the logged in user is also a member of
@memberships = @user.memberships.select do |membership|
@@ -44,7 +43,16 @@ class AccountController < ApplicationController
else
# Authenticate user
user = User.try_to_login(params[:username], params[:password])
- if user
+ if user.nil?
+ # Invalid credentials
+ flash.now[:error] = l(:notice_account_invalid_creditentials)
+ elsif user.new_record?
+ # Onthefly creation failed, display the registration form to fill/fix attributes
+ @user = user
+ session[:auth_source_registration] = {:login => user.login, :auth_source_id => user.auth_source_id }
+ render :action => 'register'
+ else
+ # Valid user
self.logged_user = user
# generate a key and set cookie if autologin
if params[:autologin] && Setting.autologin?
@@ -52,12 +60,8 @@ class AccountController < ApplicationController
cookies[:autologin] = { :value => token.value, :expires => 1.year.from_now }
end
redirect_back_or_default :controller => 'my', :action => 'page'
- else
- flash.now[:error] = l(:notice_account_invalid_creditentials)
end
end
- rescue User::OnTheFlyCreationFailure
- flash.now[:error] = 'Redmine could not retrieve the required information from the LDAP to create your account. Please, contact your Redmine administrator.'
end
# Log out current user and redirect to welcome page
@@ -107,43 +111,52 @@ class AccountController < ApplicationController
# User self-registration
def register
- redirect_to(home_url) && return unless Setting.self_registration?
+ redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
if request.get?
+ session[:auth_source_registration] = nil
@user = User.new(:language => Setting.default_language)
- @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = false
- @user.login = params[:user][:login]
@user.status = User::STATUS_REGISTERED
- @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
- @custom_values = UserCustomField.find(:all).collect { |x| CustomValue.new(:custom_field => x,
- :customized => @user,
- :value => (params["custom_fields"] ? params["custom_fields"][x.id.to_s] : nil)) }
- @user.custom_values = @custom_values
- case Setting.self_registration
- when '1'
- # Email activation
- token = Token.new(:user => @user, :action => "register")
- if @user.save and token.save
- Mailer.deliver_register(token)
- flash[:notice] = l(:notice_account_register_done)
- redirect_to :action => 'login'
- end
- when '3'
- # Automatic activation
+ if session[:auth_source_registration]
@user.status = User::STATUS_ACTIVE
+ @user.login = session[:auth_source_registration][:login]
+ @user.auth_source_id = session[:auth_source_registration][:auth_source_id]
if @user.save
+ session[:auth_source_registration] = nil
+ self.logged_user = @user
flash[:notice] = l(:notice_account_activated)
- redirect_to :action => 'login'
+ redirect_to :controller => 'my', :action => 'account'
end
else
- # Manual activation by the administrator
- if @user.save
- # Sends an email to the administrators
- Mailer.deliver_account_activation_request(@user)
- flash[:notice] = l(:notice_account_pending)
- redirect_to :action => 'login'
+ @user.login = params[:user][:login]
+ @user.password, @user.password_confirmation = params[:password], params[:password_confirmation]
+ case Setting.self_registration
+ when '1'
+ # Email activation
+ token = Token.new(:user => @user, :action => "register")
+ if @user.save and token.save
+ Mailer.deliver_register(token)
+ flash[:notice] = l(:notice_account_register_done)
+ redirect_to :action => 'login'
+ end
+ when '3'
+ # Automatic activation
+ @user.status = User::STATUS_ACTIVE
+ if @user.save
+ self.logged_user = @user
+ flash[:notice] = l(:notice_account_activated)
+ redirect_to :controller => 'my', :action => 'account'
+ end
+ else
+ # Manual activation by the administrator
+ if @user.save
+ # Sends an email to the administrators
+ Mailer.deliver_account_activation_request(@user)
+ flash[:notice] = l(:notice_account_pending)
+ redirect_to :action => 'login'
+ end
end
end
end
diff --git a/groups/app/controllers/admin_controller.rb b/groups/app/controllers/admin_controller.rb
index e002f3a27..a6df49dcd 100644
--- a/groups/app/controllers/admin_controller.rb
+++ b/groups/app/controllers/admin_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AdminController < ApplicationController
- layout 'base'
before_filter :require_admin
helper :sort
diff --git a/groups/app/controllers/application.rb b/groups/app/controllers/application.rb
index abf621641..7a56e61f0 100644
--- a/groups/app/controllers/application.rb
+++ b/groups/app/controllers/application.rb
@@ -15,7 +15,11 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+require 'uri'
+
class ApplicationController < ActionController::Base
+ layout 'base'
+
before_filter :user_setup, :check_if_login_required, :set_localization
filter_parameter_logging :password
@@ -61,11 +65,11 @@ class ApplicationController < ActionController::Base
def set_localization
User.current.language = nil unless User.current.logged?
lang = begin
- if !User.current.language.blank? and GLoc.valid_languages.include? User.current.language.to_sym
+ if !User.current.language.blank? && GLoc.valid_language?(User.current.language)
User.current.language
elsif request.env['HTTP_ACCEPT_LANGUAGE']
- accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.split('-').first
- if accept_lang and !accept_lang.empty? and GLoc.valid_languages.include? accept_lang.to_sym
+ accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first.downcase
+ if !accept_lang.blank? && (GLoc.valid_language?(accept_lang) || GLoc.valid_language?(accept_lang = accept_lang.split('-').first))
User.current.language = accept_lang
end
end
@@ -77,8 +81,7 @@ class ApplicationController < ActionController::Base
def require_login
if !User.current.logged?
- store_location
- redirect_to :controller => "account", :action => "login"
+ redirect_to :controller => "account", :action => "login", :back_url => request.request_uri
return false
end
true
@@ -115,20 +118,16 @@ class ApplicationController < ActionController::Base
end
end
- # store current uri in session.
- # return to this location by calling redirect_back_or_default
- def store_location
- session[:return_to_params] = params
- end
-
- # move to the last store_location call or to the passed default one
def redirect_back_or_default(default)
- if session[:return_to_params].nil?
- redirect_to default
- else
- redirect_to session[:return_to_params]
- session[:return_to_params] = nil
+ back_url = params[:back_url]
+ if !back_url.blank?
+ uri = URI.parse(back_url)
+ # do not redirect user to another host
+ if uri.relative? || (uri.host == request.host)
+ redirect_to(back_url) and return
+ end
end
+ redirect_to default
end
def render_403
diff --git a/groups/app/controllers/attachments_controller.rb b/groups/app/controllers/attachments_controller.rb
index 4e87e5442..788bab94d 100644
--- a/groups/app/controllers/attachments_controller.rb
+++ b/groups/app/controllers/attachments_controller.rb
@@ -16,24 +16,40 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AttachmentsController < ApplicationController
- layout 'base'
- before_filter :find_project, :check_project_privacy
+ before_filter :find_project
+ def show
+ if @attachment.is_diff?
+ @diff = File.new(@attachment.diskfile, "rb").read
+ render :action => 'diff'
+ elsif @attachment.is_text?
+ @content = File.new(@attachment.diskfile, "rb").read
+ render :action => 'file'
+ elsif
+ download
+ end
+ end
+
def download
+ @attachment.increment_download if @attachment.container.is_a?(Version)
+
# images are sent inline
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
:type => @attachment.content_type,
:disposition => (@attachment.image? ? 'inline' : 'attachment')
- rescue
- # in case the disk file was deleted
- render_404
end
private
def find_project
@attachment = Attachment.find(params[:id])
+ # Show 404 if the filename in the url is wrong
+ raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
+
@project = @attachment.project
- rescue
+ permission = @attachment.container.is_a?(Version) ? :view_files : "view_#{@attachment.container.class.name.underscore.pluralize}".to_sym
+ allowed = User.current.allowed_to?(permission, @project)
+ allowed ? true : (User.current.logged? ? render_403 : require_login)
+ rescue ActiveRecord::RecordNotFound
render_404
end
end
diff --git a/groups/app/controllers/auth_sources_controller.rb b/groups/app/controllers/auth_sources_controller.rb
index b830f1970..981f29f03 100644
--- a/groups/app/controllers/auth_sources_controller.rb
+++ b/groups/app/controllers/auth_sources_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class AuthSourcesController < ApplicationController
- layout 'base'
before_filter :require_admin
def index
diff --git a/groups/app/controllers/boards_controller.rb b/groups/app/controllers/boards_controller.rb
index 5bf4499bd..4532a88fe 100644
--- a/groups/app/controllers/boards_controller.rb
+++ b/groups/app/controllers/boards_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class BoardsController < ApplicationController
- layout 'base'
before_filter :find_project, :authorize
helper :messages
diff --git a/groups/app/controllers/custom_fields_controller.rb b/groups/app/controllers/custom_fields_controller.rb
index 57f700dc6..4ef4e4a90 100644
--- a/groups/app/controllers/custom_fields_controller.rb
+++ b/groups/app/controllers/custom_fields_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class CustomFieldsController < ApplicationController
- layout 'base'
before_filter :require_admin
def index
@@ -32,18 +31,20 @@ class CustomFieldsController < ApplicationController
def new
case params[:type]
- when "IssueCustomField"
- @custom_field = IssueCustomField.new(params[:custom_field])
- @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids]
- when "UserCustomField"
- @custom_field = UserCustomField.new(params[:custom_field])
- when "ProjectCustomField"
- @custom_field = ProjectCustomField.new(params[:custom_field])
- when "GroupCustomField"
- @custom_field = GroupCustomField.new(params[:custom_field])
- else
- render_404
- return
+ when "IssueCustomField"
+ @custom_field = IssueCustomField.new(params[:custom_field])
+ @custom_field.trackers = Tracker.find(params[:tracker_ids]) if params[:tracker_ids]
+ when "UserCustomField"
+ @custom_field = UserCustomField.new(params[:custom_field])
+ when "ProjectCustomField"
+ @custom_field = ProjectCustomField.new(params[:custom_field])
+ when "TimeEntryCustomField"
+ @custom_field = TimeEntryCustomField.new(params[:custom_field])
+ when "GroupCustomField"
+ @custom_field = GroupCustomField.new(params[:custom_field])
+ else
+ redirect_to :action => 'list'
+ return
end
if request.post? and @custom_field.save
flash[:notice] = l(:notice_successful_create)
diff --git a/groups/app/controllers/documents_controller.rb b/groups/app/controllers/documents_controller.rb
index 7e732b9b6..dbf9cd8e5 100644
--- a/groups/app/controllers/documents_controller.rb
+++ b/groups/app/controllers/documents_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class DocumentsController < ApplicationController
- layout 'base'
before_filter :find_project, :only => [:index, :new]
before_filter :find_document, :except => [:index, :new]
before_filter :authorize
@@ -65,15 +64,6 @@ class DocumentsController < ApplicationController
@document.destroy
redirect_to :controller => 'documents', :action => 'index', :project_id => @project
end
-
- def download
- @attachment = @document.attachments.find(params[:attachment_id])
- @attachment.increment_download
- send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
- :type => @attachment.content_type
- rescue
- render_404
- end
def add_attachment
attachments = attach_files(@document, params[:attachments])
diff --git a/groups/app/controllers/enumerations_controller.rb b/groups/app/controllers/enumerations_controller.rb
index 7a7f1685a..50521bab8 100644
--- a/groups/app/controllers/enumerations_controller.rb
+++ b/groups/app/controllers/enumerations_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class EnumerationsController < ApplicationController
- layout 'base'
before_filter :require_admin
def index
@@ -75,11 +74,20 @@ class EnumerationsController < ApplicationController
end
def destroy
- Enumeration.find(params[:id]).destroy
- flash[:notice] = l(:notice_successful_delete)
- redirect_to :action => 'list'
- rescue
- flash[:error] = "Unable to delete enumeration"
- redirect_to :action => 'list'
+ @enumeration = Enumeration.find(params[:id])
+ if !@enumeration.in_use?
+ # No associated objects
+ @enumeration.destroy
+ redirect_to :action => 'index'
+ elsif params[:reassign_to_id]
+ if reassign_to = Enumeration.find_by_opt_and_id(@enumeration.opt, params[:reassign_to_id])
+ @enumeration.destroy(reassign_to)
+ redirect_to :action => 'index'
+ end
+ end
+ @enumerations = Enumeration.get_values(@enumeration.opt) - [@enumeration]
+ #rescue
+ # flash[:error] = 'Unable to delete enumeration'
+ # redirect_to :action => 'index'
end
end
diff --git a/groups/app/controllers/issue_categories_controller.rb b/groups/app/controllers/issue_categories_controller.rb
index a73935b4f..8315d6eb8 100644
--- a/groups/app/controllers/issue_categories_controller.rb
+++ b/groups/app/controllers/issue_categories_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueCategoriesController < ApplicationController
- layout 'base'
menu_item :settings
before_filter :find_project, :authorize
diff --git a/groups/app/controllers/issue_relations_controller.rb b/groups/app/controllers/issue_relations_controller.rb
index cb0ad552a..2ca3f0d68 100644
--- a/groups/app/controllers/issue_relations_controller.rb
+++ b/groups/app/controllers/issue_relations_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueRelationsController < ApplicationController
- layout 'base'
before_filter :find_project, :authorize
def new
diff --git a/groups/app/controllers/issue_statuses_controller.rb b/groups/app/controllers/issue_statuses_controller.rb
index d0712e7c3..69d9db965 100644
--- a/groups/app/controllers/issue_statuses_controller.rb
+++ b/groups/app/controllers/issue_statuses_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssueStatusesController < ApplicationController
- layout 'base'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :create, :update, :move ],
diff --git a/groups/app/controllers/issues_controller.rb b/groups/app/controllers/issues_controller.rb
index 322958b8c..76b851111 100644
--- a/groups/app/controllers/issues_controller.rb
+++ b/groups/app/controllers/issues_controller.rb
@@ -16,10 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class IssuesController < ApplicationController
- layout 'base'
menu_item :new_issue, :only => :new
- before_filter :find_issue, :only => [:show, :edit, :destroy_attachment]
+ before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
before_filter :find_project, :only => [:new, :update_form, :preview]
before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
@@ -43,6 +42,7 @@ class IssuesController < ApplicationController
helper :sort
include SortHelper
include IssuesHelper
+ helper :timelog
def index
sort_init "#{Issue.table_name}.id", "desc"
@@ -65,7 +65,7 @@ class IssuesController < ApplicationController
:offset => @issue_pages.current.offset
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
- format.atom { render_feed(@issues, :title => l(:label_issue_plural)) }
+ format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
end
@@ -94,14 +94,13 @@ class IssuesController < ApplicationController
end
def show
- @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
- @activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
+ @time_entry = TimeEntry.new
respond_to do |format|
format.html { render :template => 'issues/show.rhtml' }
format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
@@ -112,15 +111,18 @@ class IssuesController < ApplicationController
# Add a new issue
# The new issue will be created from an existing one if copy_from parameter is given
def new
- @issue = params[:copy_from] ? Issue.new.copy_from(params[:copy_from]) : Issue.new(params[:issue])
+ @issue = Issue.new
+ @issue.copy_from(params[:copy_from]) if params[:copy_from]
@issue.project = @project
- @issue.author = User.current
- @issue.tracker ||= @project.trackers.find(params[:tracker_id] ? params[:tracker_id] : :first)
+ # Tracker must be set before custom field values
+ @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
if @issue.tracker.nil?
flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
render :nothing => true, :layout => true
return
end
+ @issue.attributes = params[:issue]
+ @issue.author = User.current
default_status = IssueStatus.default
unless default_status
@@ -133,22 +135,15 @@ class IssuesController < ApplicationController
if request.get? || request.xhr?
@issue.start_date ||= Date.today
- @custom_values = @issue.custom_values.empty? ?
- @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue) } :
- @issue.custom_values
else
requested_status = (params[:issue] && params[:issue][:status_id] ? IssueStatus.find_by_id(params[:issue][:status_id]) : default_status)
# Check that the user is allowed to apply the requested status
@issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
- @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x,
- :customized => @issue,
- :value => (params[:custom_fields] ? params[:custom_fields][x.id.to_s] : nil)) }
- @issue.custom_values = @custom_values
if @issue.save
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project
+ redirect_to :controller => 'issues', :action => 'show', :id => @issue
return
end
end
@@ -162,10 +157,9 @@ class IssuesController < ApplicationController
def edit
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
- @activities = Enumeration::get_values('ACTI')
@priorities = Enumeration::get_values('IPRI')
- @custom_values = []
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
+ @time_entry = TimeEntry.new
@notes = params[:notes]
journal = @issue.init_journal(User.current, @notes)
@@ -177,21 +171,14 @@ class IssuesController < ApplicationController
@issue.attributes = attrs
end
- if request.get?
- @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| @issue.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x, :customized => @issue) }
- else
- # Update custom fields if user has :edit permission
- if @edit_allowed && params[:custom_fields]
- @custom_values = @project.custom_fields_for_issues(@issue.tracker).collect { |x| CustomValue.new(:custom_field => x, :customized => @issue, :value => params["custom_fields"][x.id.to_s]) }
- @issue.custom_values = @custom_values
- end
+ if request.post?
+ @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
+ @time_entry.attributes = params[:time_entry]
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
- if @issue.save
+ if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
# Log spend time
if current_role.allowed_to?(:log_time)
- @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
- @time_entry.attributes = params[:time_entry]
@time_entry.save
end
if !journal.new_record?
@@ -207,6 +194,26 @@ class IssuesController < ApplicationController
flash.now[:error] = l(:notice_locking_conflict)
end
+ def reply
+ journal = Journal.find(params[:journal_id]) if params[:journal_id]
+ if journal
+ user = journal.user
+ text = journal.notes
+ else
+ user = @issue.author
+ text = @issue.description
+ end
+ content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
+ content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
+ render(:update) { |page|
+ page.<< "$('notes').value = \"#{content}\";"
+ page.show 'update'
+ page << "Form.Element.focus('notes');"
+ page << "Element.scrollTo('update');"
+ page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
+ }
+ end
+
# Bulk edit a set of issues
def bulk_edit
if request.post?
@@ -240,7 +247,7 @@ class IssuesController < ApplicationController
else
flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
end
- redirect_to :controller => 'issues', :action => 'index', :project_id => @project
+ redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
return
end
# Find potential statuses the user could be allowed to switch issues to
@@ -264,6 +271,7 @@ class IssuesController < ApplicationController
new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
unsaved_issue_ids = []
@issues.each do |issue|
+ issue.init_journal(User.current)
unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
end
if unsaved_issue_ids.empty?
@@ -318,19 +326,22 @@ class IssuesController < ApplicationController
if (@issues.size == 1)
@issue = @issues.first
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
- @assignables = @issue.assignable_users
- @assignables << @issue.assigned_to if @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
end
projects = @issues.collect(&:project).compact.uniq
@project = projects.first if projects.size == 1
@can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
- :update => (@issue && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && !@allowed_statuses.empty?))),
+ :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
+ :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
:delete => (@project && User.current.allowed_to?(:delete_issues, @project))
}
-
+ if @project
+ @assignables = @project.assignable_users
+ @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
+ end
+
@priorities = Enumeration.get_values('IPRI').reverse
@statuses = IssueStatus.find(:all, :order => 'position')
@back = request.env['HTTP_REFERER']
diff --git a/groups/app/controllers/journals_controller.rb b/groups/app/controllers/journals_controller.rb
index 758b8507f..6df54f098 100644
--- a/groups/app/controllers/journals_controller.rb
+++ b/groups/app/controllers/journals_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
- layout 'base'
before_filter :find_journal
def edit
diff --git a/groups/app/controllers/mail_handler_controller.rb b/groups/app/controllers/mail_handler_controller.rb
new file mode 100644
index 000000000..8bcfce630
--- /dev/null
+++ b/groups/app/controllers/mail_handler_controller.rb
@@ -0,0 +1,44 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class MailHandlerController < ActionController::Base
+ before_filter :check_credential
+
+ verify :method => :post,
+ :only => :index,
+ :render => { :nothing => true, :status => 405 }
+
+ # Submits an incoming email to MailHandler
+ def index
+ options = params.dup
+ email = options.delete(:email)
+ if MailHandler.receive(email, options)
+ render :nothing => true, :status => :created
+ else
+ render :nothing => true, :status => :unprocessable_entity
+ end
+ end
+
+ private
+
+ def check_credential
+ User.current = nil
+ unless Setting.mail_handler_api_enabled? && params[:key] == Setting.mail_handler_api_key
+ render :nothing => true, :status => 403
+ end
+ end
+end
diff --git a/groups/app/controllers/members_controller.rb b/groups/app/controllers/members_controller.rb
index 6a42ed05e..fcaede0a5 100644
--- a/groups/app/controllers/members_controller.rb
+++ b/groups/app/controllers/members_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MembersController < ApplicationController
- layout 'base'
before_filter :find_member, :except => :new
before_filter :find_project, :only => :new
before_filter :authorize
diff --git a/groups/app/controllers/messages_controller.rb b/groups/app/controllers/messages_controller.rb
index 97cb2c3bf..554279d21 100644
--- a/groups/app/controllers/messages_controller.rb
+++ b/groups/app/controllers/messages_controller.rb
@@ -16,14 +16,15 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MessagesController < ApplicationController
- layout 'base'
menu_item :boards
before_filter :find_board, :only => [:new, :preview]
before_filter :find_message, :except => [:new, :preview]
before_filter :authorize, :except => :preview
verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
+ verify :xhr => true, :only => :quote
+
helper :attachments
include AttachmentsHelper
@@ -83,6 +84,20 @@ class MessagesController < ApplicationController
{ :action => 'show', :id => @message.parent }
end
+ def quote
+ user = @message.author
+ text = @message.content
+ content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
+ content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
+ render(:update) { |page|
+ page.<< "$('message_content').value = \"#{content}\";"
+ page.show 'reply'
+ page << "Form.Element.focus('message_content');"
+ page << "Element.scrollTo('reply');"
+ page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
+ }
+ end
+
def preview
message = @board.messages.find_by_id(params[:id])
@attachements = message.attachments if message
diff --git a/groups/app/controllers/my_controller.rb b/groups/app/controllers/my_controller.rb
index ff3393e90..1cfa3e531 100644
--- a/groups/app/controllers/my_controller.rb
+++ b/groups/app/controllers/my_controller.rb
@@ -16,11 +16,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MyController < ApplicationController
- helper :issues
-
- layout 'base'
before_filter :require_login
+ helper :issues
+
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
'issuesreportedbyme' => :label_reported_issues,
'issueswatched' => :label_watched_issues,
diff --git a/groups/app/controllers/news_controller.rb b/groups/app/controllers/news_controller.rb
index c9ba6b991..b5f7ca1b2 100644
--- a/groups/app/controllers/news_controller.rb
+++ b/groups/app/controllers/news_controller.rb
@@ -16,9 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class NewsController < ApplicationController
- layout 'base'
before_filter :find_news, :except => [:new, :index, :preview]
- before_filter :find_project, :only => :new
+ before_filter :find_project, :only => [:new, :preview]
before_filter :authorize, :except => [:index, :preview]
before_filter :find_optional_project, :only => :index
accept_key_auth :index
diff --git a/groups/app/controllers/projects_controller.rb b/groups/app/controllers/projects_controller.rb
index ba6955221..747c26bd2 100644
--- a/groups/app/controllers/projects_controller.rb
+++ b/groups/app/controllers/projects_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ProjectsController < ApplicationController
- layout 'base'
menu_item :overview
menu_item :activity, :only => :activity
menu_item :roadmap, :only => :roadmap
@@ -44,37 +43,36 @@ class ProjectsController < ApplicationController
include RepositoriesHelper
include ProjectsHelper
- def index
- list
- render :action => 'list' unless request.xhr?
- end
-
# Lists visible projects
- def list
+ def index
projects = Project.find :all,
:conditions => Project.visible_by(User.current),
:include => :parent
- @project_tree = projects.group_by {|p| p.parent || p}
- @project_tree.each_key {|p| @project_tree[p] -= [p]}
+ respond_to do |format|
+ format.html {
+ @project_tree = projects.group_by {|p| p.parent || p}
+ @project_tree.keys.each {|p| @project_tree[p] -= [p]}
+ }
+ format.atom {
+ render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
+ :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
+ }
+ end
end
# Add a new project
def add
- @custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all
@root_projects = Project.find(:all,
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
:order => 'name')
@project = Project.new(params[:project])
if request.get?
- @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project) }
@project.trackers = Tracker.all
@project.is_public = Setting.default_projects_public?
@project.enabled_module_names = Redmine::AccessControl.available_project_modules
else
- @project.custom_fields = CustomField.find(params[:custom_field_ids]) if params[:custom_field_ids]
- @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
- @project.custom_values = @custom_values
@project.enabled_module_names = params[:enabled_modules]
if @project.save
flash[:notice] = l(:notice_successful_create)
@@ -85,8 +83,7 @@ class ProjectsController < ApplicationController
# Show @project
def show
- @custom_values = @project.custom_values.find(:all, :include => :custom_field, :order => "#{CustomField.table_name}.position")
- @subprojects = @project.active_children
+ @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers
@@ -111,11 +108,10 @@ class ProjectsController < ApplicationController
@root_projects = Project.find(:all,
:conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
:order => 'name')
- @custom_fields = IssueCustomField.find(:all)
+ @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@issue_category ||= IssueCategory.new
@member ||= @project.members.new
@trackers = Tracker.all
- @custom_values ||= ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @project.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
@repository ||= @project.repository
@wiki ||= @project.wiki
end
@@ -123,10 +119,6 @@ class ProjectsController < ApplicationController
# Edit @project
def edit
if request.post?
- if params[:custom_fields]
- @custom_values = ProjectCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @project, :value => params["custom_fields"][x.id.to_s]) }
- @project.custom_values = @custom_values
- end
@project.attributes = params[:project]
if @project.save
flash[:notice] = l(:notice_successful_update)
@@ -232,91 +224,23 @@ class ProjectsController < ApplicationController
@date_to ||= Date.today + 1
@date_from = @date_to - @days
+ @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
- @event_types = %w(issues news files documents changesets wiki_pages messages)
- if @project
- @event_types.delete('wiki_pages') unless @project.wiki
- @event_types.delete('changesets') unless @project.repository
- @event_types.delete('messages') unless @project.boards.any?
- # only show what the user is allowed to view
- @event_types = @event_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
- @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
- end
- @scope = @event_types.select {|t| params["show_#{t}"]}
- # default events if none is specified in parameters
- @scope = (@event_types - %w(wiki_pages messages))if @scope.empty?
-
- @events = []
-
- if @scope.include?('issues')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Issue.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Issue.find(:all, :include => [:project, :author, :tracker], :conditions => cond.conditions)
-
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_issues, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Journal.table_name}.journalized_type = 'Issue' AND #{JournalDetail.table_name}.prop_key = 'status_id' AND #{Journal.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Journal.find(:all, :include => [{:issue => :project}, :details, :user], :conditions => cond.conditions)
- end
-
- if @scope.include?('news')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_news, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{News.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += News.find(:all, :include => [:project, :author], :conditions => cond.conditions)
- end
-
- if @scope.include?('files')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_files, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
- :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
- "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id",
- :conditions => cond.conditions)
- end
-
- if @scope.include?('documents')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Document.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Document.find(:all, :include => :project, :conditions => cond.conditions)
-
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_documents, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Attachment.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Attachment.find(:all, :select => "#{Attachment.table_name}.*",
- :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
- "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id",
- :conditions => cond.conditions)
- end
-
- if @scope.include?('wiki_pages')
- select = "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
- "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
- "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
- "#{WikiContent.versioned_table_name}.id"
- joins = "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
- "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
- "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"
-
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_wiki_pages, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{WikiContent.versioned_table_name}.updated_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += WikiContent.versioned_class.find(:all, :select => select, :joins => joins, :conditions => cond.conditions)
- end
+ @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project, :with_subprojects => @with_subprojects)
+ @activity.scope_select {|t| !params["show_#{t}"].nil?}
+ @activity.default_scope! if @activity.scope.empty?
- if @scope.include?('changesets')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_changesets, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Changeset.table_name}.committed_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Changeset.find(:all, :include => {:repository => :project}, :conditions => cond.conditions)
- end
-
- if @scope.include?('messages')
- cond = ARCondition.new(Project.allowed_to_condition(User.current, :view_messages, :project => @project, :with_subprojects => @with_subprojects))
- cond.add(["#{Message.table_name}.created_on BETWEEN ? AND ?", @date_from, @date_to])
- @events += Message.find(:all, :include => [{:board => :project}, :author], :conditions => cond.conditions)
- end
-
- @events_by_day = @events.group_by(&:event_date)
+ events = @activity.events(@date_from, @date_to)
respond_to do |format|
- format.html { render :layout => false if request.xhr? }
- format.atom { render_feed(@events, :title => "#{@project || Setting.app_title}: #{l(:label_activity)}") }
+ format.html {
+ @events_by_day = events.group_by(&:event_date)
+ render :layout => false if request.xhr?
+ }
+ format.atom {
+ title = (@activity.scope.size == 1) ? l("label_#{@activity.scope.first.singularize}_plural") : l(:label_activity)
+ render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
+ }
end
end
@@ -381,11 +305,18 @@ class ProjectsController < ApplicationController
@events = []
@project.issues_with_subprojects(@with_subprojects) do
+ # Issues that have start and due dates
@events += Issue.find(:all,
:order => "start_date, due_date",
:include => [:tracker, :status, :assigned_to, :priority, :project],
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
) unless @selected_tracker_ids.empty?
+ # Issues that don't have a due date but that are assigned to a version with a date
+ @events += Issue.find(:all,
+ :order => "start_date, effective_date",
+ :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
+ :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null and #{Issue.table_name}.tracker_id in (#{@selected_tracker_ids.join(',')}))", @date_from, @date_to, @date_from, @date_to, @date_from, @date_to]
+ ) unless @selected_tracker_ids.empty?
@events += Version.find(:all, :include => :project,
:conditions => ["effective_date BETWEEN ? AND ?", @date_from, @date_to])
end
diff --git a/groups/app/controllers/queries_controller.rb b/groups/app/controllers/queries_controller.rb
index da2c4a2c8..8500e853a 100644
--- a/groups/app/controllers/queries_controller.rb
+++ b/groups/app/controllers/queries_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class QueriesController < ApplicationController
- layout 'base'
menu_item :issues
before_filter :find_query, :except => :new
before_filter :find_optional_project, :only => :new
diff --git a/groups/app/controllers/reports_controller.rb b/groups/app/controllers/reports_controller.rb
index 338059a50..dd3ece930 100644
--- a/groups/app/controllers/reports_controller.rb
+++ b/groups/app/controllers/reports_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class ReportsController < ApplicationController
- layout 'base'
menu_item :issues
before_filter :find_project, :authorize
diff --git a/groups/app/controllers/repositories_controller.rb b/groups/app/controllers/repositories_controller.rb
index 64eb05793..2f96e2d66 100644
--- a/groups/app/controllers/repositories_controller.rb
+++ b/groups/app/controllers/repositories_controller.rb
@@ -23,20 +23,21 @@ class ChangesetNotFound < Exception; end
class InvalidRevisionParam < Exception; end
class RepositoriesController < ApplicationController
- layout 'base'
menu_item :repository
before_filter :find_repository, :except => :edit
before_filter :find_project, :only => :edit
before_filter :authorize
accept_key_auth :revisions
+ rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
+
def edit
@repository = @project.repository
if !@repository
@repository = Repository.factory(params[:repository_scm])
- @repository.project = @project
+ @repository.project = @project if @repository
end
- if request.post?
+ if request.post? && @repository
@repository.attributes = params[:repository]
@repository.save
end
@@ -56,8 +57,6 @@ class RepositoriesController < ApplicationController
# latest changesets
@changesets = @repository.changesets.find(:all, :limit => 10, :order => "committed_on DESC")
show_error_not_found unless @entries || @changesets.any?
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
end
def browse
@@ -66,18 +65,16 @@ class RepositoriesController < ApplicationController
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
else
show_error_not_found and return unless @entries
+ @properties = @repository.properties(@path, @rev)
render :action => 'browse'
end
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
end
def changes
- @entry = @repository.scm.entry(@path, @rev)
+ @entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
@changesets = @repository.changesets_for_path(@path)
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
+ @properties = @repository.properties(@path, @rev)
end
def revisions
@@ -96,13 +93,13 @@ class RepositoriesController < ApplicationController
end
def entry
- @entry = @repository.scm.entry(@path, @rev)
+ @entry = @repository.entry(@path, @rev)
show_error_not_found and return unless @entry
# If the entry is a dir, show the browser
browse and return if @entry.is_dir?
- @content = @repository.scm.cat(@path, @rev)
+ @content = @repository.cat(@path, @rev)
show_error_not_found and return unless @content
if 'raw' == params[:format] || @content.is_binary_data?
# Force the download if it's a binary file
@@ -110,16 +107,12 @@ class RepositoriesController < ApplicationController
else
# Prevent empty lines when displaying a file with Windows style eol
@content.gsub!("\r\n", "\n")
- end
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
+ end
end
def annotate
@annotate = @repository.scm.annotate(@path, @rev)
render_error l(:error_scm_annotate) and return if @annotate.nil? || @annotate.empty?
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
end
def revision
@@ -137,27 +130,33 @@ class RepositoriesController < ApplicationController
end
rescue ChangesetNotFound
show_error_not_found
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
end
def diff
- @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
- @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
-
- # Save diff type as user preference
- if User.current.logged? && @diff_type != User.current.pref[:diff_type]
- User.current.pref[:diff_type] = @diff_type
- User.current.preference.save
- end
-
- @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
- unless read_fragment(@cache_key)
- @diff = @repository.diff(@path, @rev, @rev_to, @diff_type)
- show_error_not_found unless @diff
+ if params[:format] == 'diff'
+ @diff = @repository.diff(@path, @rev, @rev_to)
+ show_error_not_found and return unless @diff
+ filename = "changeset_r#{@rev}"
+ filename << "_r#{@rev_to}" if @rev_to
+ send_data @diff.join, :filename => "#{filename}.diff",
+ :type => 'text/x-patch',
+ :disposition => 'attachment'
+ else
+ @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
+ @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
+
+ # Save diff type as user preference
+ if User.current.logged? && @diff_type != User.current.pref[:diff_type]
+ User.current.pref[:diff_type] = @diff_type
+ User.current.preference.save
+ end
+
+ @cache_key = "repositories/diff/#{@repository.id}/" + Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}")
+ unless read_fragment(@cache_key)
+ @diff = @repository.diff(@path, @rev, @rev_to)
+ show_error_not_found unless @diff
+ end
end
- rescue Redmine::Scm::Adapters::CommandFailed => e
- show_error_command_failed(e.message)
end
def stats
@@ -207,8 +206,9 @@ private
render_error l(:error_scm_not_found)
end
- def show_error_command_failed(msg)
- render_error l(:error_scm_command_failed, msg)
+ # Handler for Redmine::Scm::Adapters::CommandFailed exception
+ def show_error_command_failed(exception)
+ render_error l(:error_scm_command_failed, exception.message)
end
def graph_commits_per_month(repository)
@@ -229,7 +229,7 @@ private
graph = SVG::Graph::Bar.new(
:height => 300,
- :width => 500,
+ :width => 800,
:fields => fields.reverse,
:stack => :side,
:scale_integers => true,
@@ -271,8 +271,8 @@ private
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
graph = SVG::Graph::BarHorizontal.new(
- :height => 300,
- :width => 500,
+ :height => 400,
+ :width => 800,
:fields => fields,
:stack => :side,
:scale_integers => true,
diff --git a/groups/app/controllers/roles_controller.rb b/groups/app/controllers/roles_controller.rb
index 9fdd9701b..72555e5b0 100644
--- a/groups/app/controllers/roles_controller.rb
+++ b/groups/app/controllers/roles_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class RolesController < ApplicationController
- layout 'base'
before_filter :require_admin
verify :method => :post, :only => [ :destroy, :move ],
diff --git a/groups/app/controllers/search_controller.rb b/groups/app/controllers/search_controller.rb
index f15653b63..e6e66f05c 100644
--- a/groups/app/controllers/search_controller.rb
+++ b/groups/app/controllers/search_controller.rb
@@ -16,8 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SearchController < ApplicationController
- layout 'base'
-
before_filter :find_optional_project
helper :messages
@@ -29,6 +27,18 @@ class SearchController < ApplicationController
@all_words = params[:all_words] || (params[:submit] ? false : true)
@titles_only = !params[:titles_only].nil?
+ projects_to_search =
+ case params[:scope]
+ when 'all'
+ nil
+ when 'my_projects'
+ User.current.memberships.collect(&:project)
+ when 'subprojects'
+ @project ? ([ @project ] + @project.active_children) : nil
+ else
+ @project
+ end
+
offset = nil
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
@@ -38,16 +48,16 @@ class SearchController < ApplicationController
return
end
- if @project
+ @object_types = %w(issues news documents changesets wiki_pages messages projects)
+ if projects_to_search.is_a? Project
+ # don't search projects
+ @object_types.delete('projects')
# only show what the user is allowed to view
- @object_types = %w(issues news documents changesets wiki_pages messages)
- @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
-
- @scope = @object_types.select {|t| params[t]}
- @scope = @object_types if @scope.empty?
- else
- @object_types = @scope = %w(projects)
+ @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
end
+
+ @scope = @object_types.select {|t| params[t]}
+ @scope = @object_types if @scope.empty?
# extract tokens from the question
# eg. hello "bye bye" => ["hello", "bye bye"]
@@ -60,39 +70,34 @@ class SearchController < ApplicationController
@tokens.slice! 5..-1 if @tokens.size > 5
# strings used in sql like statement
like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
+
@results = []
+ @results_by_type = Hash.new {|h,k| h[k] = 0}
+
limit = 10
- if @project
- @scope.each do |s|
- @results += s.singularize.camelcase.constantize.search(like_tokens, @project,
- :all_words => @all_words,
- :titles_only => @titles_only,
- :limit => (limit+1),
- :offset => offset,
- :before => params[:previous].nil?)
- end
- @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
- if params[:previous].nil?
- @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
- if @results.size > limit
- @pagination_next_date = @results[limit-1].event_datetime
- @results = @results[0, limit]
- end
- else
- @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
- if @results.size > limit
- @pagination_previous_date = @results[-(limit)].event_datetime
- @results = @results[-(limit), limit]
- end
+ @scope.each do |s|
+ r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
+ :all_words => @all_words,
+ :titles_only => @titles_only,
+ :limit => (limit+1),
+ :offset => offset,
+ :before => params[:previous].nil?)
+ @results += r
+ @results_by_type[s] += c
+ end
+ @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
+ if params[:previous].nil?
+ @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
+ if @results.size > limit
+ @pagination_next_date = @results[limit-1].event_datetime
+ @results = @results[0, limit]
end
else
- operator = @all_words ? ' AND ' : ' OR '
- @results += Project.find(:all,
- :limit => limit,
- :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
- ) if @scope.include? 'projects'
- # if only one project is found, user is redirected to its overview
- redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
+ @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
+ if @results.size > limit
+ @pagination_previous_date = @results[-(limit)].event_datetime
+ @results = @results[-(limit), limit]
+ end
end
else
@question = ""
diff --git a/groups/app/controllers/settings_controller.rb b/groups/app/controllers/settings_controller.rb
index c7c8751dd..6482a3576 100644
--- a/groups/app/controllers/settings_controller.rb
+++ b/groups/app/controllers/settings_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class SettingsController < ApplicationController
- layout 'base'
before_filter :require_admin
def index
@@ -39,6 +38,7 @@ class SettingsController < ApplicationController
end
@options = {}
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
+ @deliveries = ActionMailer::Base.perform_deliveries
end
def plugin
@@ -49,7 +49,7 @@ class SettingsController < ApplicationController
flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'plugin', :id => params[:id]
end
- @partial = "../../vendor/plugins/#{plugin_id}/app/views/" + @plugin.settings[:partial]
+ @partial = @plugin.settings[:partial]
@settings = Setting["plugin_#{plugin_id}"]
end
end
diff --git a/groups/app/controllers/timelog_controller.rb b/groups/app/controllers/timelog_controller.rb
index 29c2635d6..f331cdbe4 100644
--- a/groups/app/controllers/timelog_controller.rb
+++ b/groups/app/controllers/timelog_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TimelogController < ApplicationController
- layout 'base'
menu_item :issues
before_filter :find_project, :authorize
@@ -54,8 +53,15 @@ class TimelogController < ApplicationController
}
# Add list and boolean custom fields as available criterias
- @project.all_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
- @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM custom_values c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = issues.id)",
+ @project.all_issue_custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
+ :format => cf.field_format,
+ :label => cf.name}
+ end
+
+ # Add list and boolean time entry custom fields
+ TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
+ @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
:format => cf.field_format,
:label => cf.name}
end
@@ -154,6 +160,14 @@ class TimelogController < ApplicationController
render :layout => !request.xhr?
}
+ format.atom {
+ entries = TimeEntry.find(:all,
+ :include => [:project, :activity, :user, {:issue => :tracker}],
+ :conditions => cond.conditions,
+ :order => "#{TimeEntry.table_name}.created_on DESC",
+ :limit => Setting.feeds_limit.to_i)
+ render_feed(entries, :title => l(:label_spent_time))
+ }
format.csv {
# Export all entries
@entries = TimeEntry.find(:all,
@@ -172,10 +186,9 @@ class TimelogController < ApplicationController
@time_entry.attributes = params[:time_entry]
if request.post? and @time_entry.save
flash[:notice] = l(:notice_successful_update)
- redirect_to(params[:back_url] || {:action => 'details', :project_id => @time_entry.project})
+ redirect_to(params[:back_url].blank? ? {:action => 'details', :project_id => @time_entry.project} : params[:back_url])
return
end
- @activities = Enumeration::get_values('ACTI')
end
def destroy
diff --git a/groups/app/controllers/trackers_controller.rb b/groups/app/controllers/trackers_controller.rb
index 3d7dbd5c5..8c02f9474 100644
--- a/groups/app/controllers/trackers_controller.rb
+++ b/groups/app/controllers/trackers_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class TrackersController < ApplicationController
- layout 'base'
before_filter :require_admin
def index
diff --git a/groups/app/controllers/users_controller.rb b/groups/app/controllers/users_controller.rb
index 3cd66d6a4..5c933c7de 100644
--- a/groups/app/controllers/users_controller.rb
+++ b/groups/app/controllers/users_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class UsersController < ApplicationController
- layout 'base'
before_filter :require_admin
helper :sort
@@ -53,15 +52,12 @@ class UsersController < ApplicationController
def add
if request.get?
@user = User.new(:language => Setting.default_language)
- @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user) }
else
@user = User.new(params[:user])
@user.admin = params[:user][:admin] || false
@user.login = params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
@user.group_id = params[:user][:group_id]
- @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => (params[:custom_fields] ? params["custom_fields"][x.id.to_s] : nil)) }
- @user.custom_values = @custom_values
if @user.save
Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
flash[:notice] = l(:notice_successful_create)
@@ -74,17 +70,11 @@ class UsersController < ApplicationController
def edit
@user = User.find(params[:id])
- if request.get?
- @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| @user.custom_values.find_by_custom_field_id(x.id) || CustomValue.new(:custom_field => x) }
- else
+ if request.post?
@user.admin = params[:user][:admin] if params[:user][:admin]
@user.login = params[:user][:login] if params[:user][:login]
@user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
@user.group_id = params[:user][:group_id] if params[:user][:group_id]
- if params[:custom_fields]
- @custom_values = UserCustomField.find(:all, :order => "#{CustomField.table_name}.position").collect { |x| CustomValue.new(:custom_field => x, :customized => @user, :value => params["custom_fields"][x.id.to_s]) }
- @user.custom_values = @custom_values
- end
if @user.update_attributes(params[:user])
flash[:notice] = l(:notice_successful_update)
# Give a string to redirect_to otherwise it would use status param as the response code
@@ -96,16 +86,15 @@ class UsersController < ApplicationController
@roles = Role.find_all_givable
@projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
@membership ||= Member.new
+ @memberships = @user.memberships.select {|m| m.inherited_from.nil? }
end
def edit_membership
@user = User.find(params[:id])
@membership = params[:membership_id] ? Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL') : Member.new(:principal => @user)
@membership.attributes = params[:membership]
- if request.post? and @membership.save
- flash[:notice] = l(:notice_successful_update)
- end
- redirect_to :action => 'edit', :id => @user and return
+ @membership.save if request.post?
+ redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
def destroy_membership
@@ -113,6 +102,6 @@ class UsersController < ApplicationController
if request.post? and Member.find(params[:membership_id], :conditions => 'inherited_from IS NULL').destroy
flash[:notice] = l(:notice_successful_update)
end
- redirect_to :action => 'edit', :id => @user and return
+ redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
end
end
diff --git a/groups/app/controllers/versions_controller.rb b/groups/app/controllers/versions_controller.rb
index aeb802ccb..ab2ccb773 100644
--- a/groups/app/controllers/versions_controller.rb
+++ b/groups/app/controllers/versions_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class VersionsController < ApplicationController
- layout 'base'
menu_item :roadmap
before_filter :find_project, :authorize
@@ -37,15 +36,6 @@ class VersionsController < ApplicationController
flash[:error] = "Unable to delete version"
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
end
-
- def download
- @attachment = @version.attachments.find(params[:attachment_id])
- @attachment.increment_download
- send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
- :type => @attachment.content_type
- rescue
- render_404
- end
def destroy_file
@version.attachments.find(params[:attachment_id]).destroy
diff --git a/groups/app/controllers/watchers_controller.rb b/groups/app/controllers/watchers_controller.rb
index 206dc0843..8e6ee3a9e 100644
--- a/groups/app/controllers/watchers_controller.rb
+++ b/groups/app/controllers/watchers_controller.rb
@@ -16,27 +16,38 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WatchersController < ApplicationController
- layout 'base'
- before_filter :require_login, :find_project, :check_project_privacy
+ before_filter :find_project
+ before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
+ before_filter :authorize, :only => :new
- def add
- user = User.current
- @watched.add_watcher(user)
- respond_to do |format|
- format.html { render :text => 'Watcher added.', :layout => true }
- format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
- end
+ verify :method => :post,
+ :only => [ :watch, :unwatch ],
+ :render => { :nothing => true, :status => :method_not_allowed }
+
+ def watch
+ set_watcher(User.current, true)
end
- def remove
- user = User.current
- @watched.remove_watcher(user)
+ def unwatch
+ set_watcher(User.current, false)
+ end
+
+ def new
+ @watcher = Watcher.new(params[:watcher])
+ @watcher.watchable = @watched
+ @watcher.save if request.post?
respond_to do |format|
- format.html { render :text => 'Watcher removed.', :layout => true }
- format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
+ format.html { redirect_to :back }
+ format.js do
+ render :update do |page|
+ page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
+ end
+ end
end
+ rescue ::ActionController::RedirectBackError
+ render :text => 'Watcher added.', :layout => true
end
-
+
private
def find_project
klass = Object.const_get(params[:object_type].camelcase)
@@ -46,4 +57,14 @@ private
rescue
render_404
end
+
+ def set_watcher(user, watching)
+ @watched.set_watcher(user, watching)
+ respond_to do |format|
+ format.html { redirect_to :back }
+ format.js { render(:update) {|page| page.replace_html 'watcher', watcher_link(@watched, user)} }
+ end
+ rescue ::ActionController::RedirectBackError
+ render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true
+ end
end
diff --git a/groups/app/controllers/welcome_controller.rb b/groups/app/controllers/welcome_controller.rb
index b4be7fb1c..b8108e8ac 100644
--- a/groups/app/controllers/welcome_controller.rb
+++ b/groups/app/controllers/welcome_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WelcomeController < ApplicationController
- layout 'base'
def index
@news = News.latest User.current
diff --git a/groups/app/controllers/wiki_controller.rb b/groups/app/controllers/wiki_controller.rb
index 53c5ec53b..46df2931e 100644
--- a/groups/app/controllers/wiki_controller.rb
+++ b/groups/app/controllers/wiki_controller.rb
@@ -18,10 +18,9 @@
require 'diff'
class WikiController < ApplicationController
- layout 'base'
before_filter :find_wiki, :authorize
- verify :method => :post, :only => [:destroy, :destroy_attachment], :redirect_to => { :action => :index }
+ verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
helper :attachments
include AttachmentsHelper
@@ -48,12 +47,14 @@ class WikiController < ApplicationController
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
return
end
+ @editable = editable?
render :action => 'show'
end
# edit an existing page or a new one
def edit
@page = @wiki.find_or_new_page(params[:page])
+ return render_403 unless editable?
@page.content = WikiContent.new(:page => @page) if @page.new_record?
@content = @page.content_for_version(params[:version])
@@ -82,7 +83,8 @@ class WikiController < ApplicationController
# rename a page
def rename
- @page = @wiki.find_page(params[:page])
+ @page = @wiki.find_page(params[:page])
+ return render_403 unless editable?
@page.redirect_existing_links = true
# used to display the *original* title if some AR validation errors occur
@original_title = @page.pretty_title
@@ -92,6 +94,12 @@ class WikiController < ApplicationController
end
end
+ def protect
+ page = @wiki.find_page(params[:page])
+ page.update_attribute :protected, params[:protected]
+ redirect_to :action => 'index', :id => @project, :page => page.title
+ end
+
# show page history
def history
@page = @wiki.find_page(params[:page])
@@ -122,6 +130,7 @@ class WikiController < ApplicationController
# remove a wiki page and its history
def destroy
@page = @wiki.find_page(params[:page])
+ return render_403 unless editable?
@page.destroy if @page
redirect_to :action => 'special', :id => @project, :page => 'Page_index'
end
@@ -137,6 +146,7 @@ class WikiController < ApplicationController
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
:order => 'title'
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
+ @pages_by_parent_id = @pages.group_by(&:parent_id)
# export wiki to a single html file
when 'export'
@pages = @wiki.pages.find :all, :order => 'title'
@@ -152,19 +162,26 @@ class WikiController < ApplicationController
def preview
page = @wiki.find_page(params[:page])
- @attachements = page.attachments if page
+ # page is nil when previewing a new page
+ return render_403 unless page.nil? || editable?(page)
+ if page
+ @attachements = page.attachments
+ @previewed = page.content
+ end
@text = params[:content][:text]
render :partial => 'common/preview'
end
def add_attachment
@page = @wiki.find_page(params[:page])
+ return render_403 unless editable?
attach_files(@page, params[:attachments])
redirect_to :action => 'index', :page => @page.title
end
def destroy_attachment
@page = @wiki.find_page(params[:page])
+ return render_403 unless editable?
@page.attachments.find(params[:attachment_id]).destroy
redirect_to :action => 'index', :page => @page.title
end
@@ -178,4 +195,9 @@ private
rescue ActiveRecord::RecordNotFound
render_404
end
+
+ # Returns true if the current user is allowed to edit the page, otherwise false
+ def editable?(page = @page)
+ page.editable_by?(User.current)
+ end
end
diff --git a/groups/app/controllers/wikis_controller.rb b/groups/app/controllers/wikis_controller.rb
index 6054abd9a..215d39f4b 100644
--- a/groups/app/controllers/wikis_controller.rb
+++ b/groups/app/controllers/wikis_controller.rb
@@ -16,7 +16,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class WikisController < ApplicationController
- layout 'base'
menu_item :settings
before_filter :find_project, :authorize
diff --git a/groups/app/helpers/application_helper.rb b/groups/app/helpers/application_helper.rb
index 47a251053..78e5bdc65 100644
--- a/groups/app/helpers/application_helper.rb
+++ b/groups/app/helpers/application_helper.rb
@@ -15,6 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+require 'coderay'
+require 'coderay/helpers/file_type'
+
module ApplicationHelper
include Redmine::WikiFormatting::Macros::Definitions
@@ -31,6 +34,12 @@ module ApplicationHelper
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
end
+
+ # Display a link to remote if user is authorized
+ def link_to_remote_if_authorized(name, options = {}, html_options = nil)
+ url = options[:url] || {}
+ link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
+ end
# Display a link to user's account page
def link_to_user(user)
@@ -38,9 +47,23 @@ module ApplicationHelper
end
def link_to_issue(issue, options={})
+ options[:class] ||= ''
+ options[:class] << ' issue'
+ options[:class] << ' closed' if issue.closed?
link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
end
+ # Generates a link to an attachment.
+ # Options:
+ # * :text - Link text (default to attachment filename)
+ # * :download - Force download (default: false)
+ def link_to_attachment(attachment, options={})
+ text = options.delete(:text) || attachment.filename
+ action = options.delete(:download) ? 'download' : 'show'
+
+ link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
+ end
+
def toggle_link(name, id, options={})
onclick = "Element.toggle('#{id}'); "
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
@@ -48,14 +71,6 @@ module ApplicationHelper
link_to(name, "#", :onclick => onclick)
end
- def show_and_goto_link(name, id, options={})
- onclick = "Element.show('#{id}'); "
- onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
- onclick << "Element.scrollTo('#{id}'); "
- onclick << "return false;"
- link_to(name, "#", options.merge(:onclick => onclick))
- end
-
def image_to_function(name, function, html_options = {})
html_options.symbolize_keys!
tag(:input, html_options.merge({
@@ -80,23 +95,25 @@ module ApplicationHelper
return nil unless time
time = time.to_time if time.is_a?(String)
zone = User.current.time_zone
- if time.utc?
- local = zone ? zone.adjust(time) : time.getlocal
- else
- local = zone ? zone.adjust(time.getutc) : time
- end
+ local = zone ? time.in_time_zone(zone) : (time.utc? ? time.utc_to_local : time)
@date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
@time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
end
+ # Truncates and returns the string as a single line
+ def truncate_single_line(string, *args)
+ truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
+ end
+
def html_hours(text)
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
end
def authoring(created, author)
time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
- l(:label_added_time_by, author || 'Anonymous', time_tag)
+ author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
+ l(:label_added_time_by, author_tag, time_tag)
end
def l_or_humanize(s)
@@ -111,6 +128,15 @@ module ApplicationHelper
l(:actionview_datehelper_select_month_names).split(',')[month-1]
end
+ def syntax_highlight(name, content)
+ type = CodeRay::FileType[name]
+ type ? CodeRay.scan(content, type).html : h(content)
+ end
+
+ def to_path_param(path)
+ path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
+ end
+
def pagination_links_full(paginator, count=nil, options={})
page_param = options.delete(:page_param) || :page
url_param = params.dup
@@ -157,7 +183,8 @@ module ApplicationHelper
end
def breadcrumb(*args)
- content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb')
+ elements = args.flatten
+ elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
end
def html_title(*args)
@@ -185,7 +212,7 @@ module ApplicationHelper
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
- obj = nil
+ obj = options[:object]
text = args.shift
when 2
obj = args.shift
@@ -225,12 +252,12 @@ module ApplicationHelper
case options[:wiki_links]
when :local
# used for local links to html files
- format_wiki_link = Proc.new {|project, title| "#{title}.html" }
+ format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
when :anchor
# used for single-file wiki export
- format_wiki_link = Proc.new {|project, title| "##{title}" }
+ format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
else
- format_wiki_link = Proc.new {|project, title| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title) }
+ format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
end
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
@@ -256,9 +283,14 @@ module ApplicationHelper
end
if link_project && link_project.wiki
+ # extract anchor
+ anchor = nil
+ if page =~ /^(.+?)\#(.+)$/
+ page, anchor = $1, $2
+ end
# check if page exists
wiki_page = link_project.wiki.find_page(page)
- link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page)),
+ link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
:class => ('wiki-page' + (wiki_page ? '' : ' new')))
else
# project or wiki doesn't exist
@@ -293,7 +325,9 @@ module ApplicationHelper
# source:some/file#L120 -> Link to line 120 of the file
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
# export:some/file -> Force the download of the file
- text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit|source|export)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
+ # Forum messages:
+ # message#1218 -> Link to message with id 1218
+ text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
link = nil
if esc.nil?
@@ -301,7 +335,7 @@ module ApplicationHelper
if project && (changeset = project.changesets.find_by_revision(oid))
link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
:class => 'changeset',
- :title => truncate(changeset.comments, 100))
+ :title => truncate_single_line(changeset.comments, 100))
end
elsif sep == '#'
oid = oid.to_i
@@ -323,6 +357,16 @@ module ApplicationHelper
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
:class => 'version'
end
+ when 'message'
+ if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
+ link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
+ :controller => 'messages',
+ :action => 'show',
+ :board_id => message.board,
+ :id => message.root,
+ :anchor => (message.parent ? "message-#{message.id}" : nil)},
+ :class => 'message'
+ end
end
elsif sep == ':'
# removes the double quotes if any
@@ -340,13 +384,16 @@ module ApplicationHelper
end
when 'commit'
if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
- link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision}, :class => 'changeset', :title => truncate(changeset.comments, 100)
+ link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
+ :class => 'changeset',
+ :title => truncate_single_line(changeset.comments, 100)
end
when 'source', 'export'
if project && project.repository
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
path, rev, anchor = $1, $3, $5
- link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :path => path,
+ link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
+ :path => to_path_param(path),
:rev => rev,
:anchor => anchor,
:format => (prefix == 'export' ? 'raw' : nil)},
@@ -428,7 +475,8 @@ module ApplicationHelper
end
def back_url_hidden_field_tag
- hidden_field_tag 'back_url', (params[:back_url] || request.env['HTTP_REFERER'])
+ back_url = params[:back_url] || request.env['HTTP_REFERER']
+ hidden_field_tag('back_url', back_url) unless back_url.blank?
end
def check_all_links(form_name)
diff --git a/groups/app/helpers/attachments_helper.rb b/groups/app/helpers/attachments_helper.rb
index 989cd3e66..ebf417bab 100644
--- a/groups/app/helpers/attachments_helper.rb
+++ b/groups/app/helpers/attachments_helper.rb
@@ -22,4 +22,8 @@ module AttachmentsHelper
render :partial => 'attachments/links', :locals => {:attachments => attachments, :options => options}
end
end
+
+ def to_utf8(str)
+ str
+ end
end
diff --git a/groups/app/helpers/custom_fields_helper.rb b/groups/app/helpers/custom_fields_helper.rb
index aae105b72..ed0c4126f 100644
--- a/groups/app/helpers/custom_fields_helper.rb
+++ b/groups/app/helpers/custom_fields_helper.rb
@@ -19,6 +19,7 @@ module CustomFieldsHelper
def custom_fields_tabs
tabs = [{:name => 'IssueCustomField', :label => :label_issue_plural},
+ {:name => 'TimeEntryCustomField', :label => :label_spent_time},
{:name => 'ProjectCustomField', :label => :label_project_plural},
{:name => 'UserCustomField', :label => :label_user_plural},
{:name => 'GroupCustomField', :label => :label_group_plural}
@@ -26,37 +27,40 @@ module CustomFieldsHelper
end
# Return custom field html tag corresponding to its format
- def custom_field_tag(custom_value)
+ def custom_field_tag(name, custom_value)
custom_field = custom_value.custom_field
- field_name = "custom_fields[#{custom_field.id}]"
- field_id = "custom_fields_#{custom_field.id}"
+ field_name = "#{name}[custom_field_values][#{custom_field.id}]"
+ field_id = "#{name}_custom_field_values_#{custom_field.id}"
case custom_field.field_format
when "date"
- text_field('custom_value', 'value', :name => field_name, :id => field_id, :size => 10) +
+ text_field_tag(field_name, custom_value.value, :id => field_id, :size => 10) +
calendar_for(field_id)
when "text"
- text_area 'custom_value', 'value', :name => field_name, :id => field_id, :rows => 3, :style => 'width:99%'
+ text_area_tag(field_name, custom_value.value, :id => field_id, :rows => 3, :style => 'width:90%')
when "bool"
- check_box 'custom_value', 'value', :name => field_name, :id => field_id
+ check_box_tag(field_name, '1', custom_value.true?, :id => field_id) + hidden_field_tag(field_name, '0')
when "list"
- select 'custom_value', 'value', custom_field.possible_values, { :include_blank => true }, :name => field_name, :id => field_id
+ blank_option = custom_field.is_required? ?
+ (custom_field.default_value.blank? ? "<option value=\"\">--- #{l(:actionview_instancetag_blank_option)} ---</option>" : '') :
+ '<option></option>'
+ select_tag(field_name, blank_option + options_for_select(custom_field.possible_values, custom_value.value), :id => field_id)
else
- text_field 'custom_value', 'value', :name => field_name, :id => field_id
+ text_field_tag(field_name, custom_value.value, :id => field_id)
end
end
# Return custom field label tag
- def custom_field_label_tag(custom_value)
+ def custom_field_label_tag(name, custom_value)
content_tag "label", custom_value.custom_field.name +
(custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>" : ""),
- :for => "custom_fields_#{custom_value.custom_field.id}",
+ :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}",
:class => (custom_value.errors.empty? ? nil : "error" )
end
# Return custom field tag with its label tag
- def custom_field_tag_with_label(custom_value)
- custom_field_label_tag(custom_value) + custom_field_tag(custom_value)
+ def custom_field_tag_with_label(name, custom_value)
+ custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end
# Return a string used to display a custom value
diff --git a/groups/app/helpers/issues_helper.rb b/groups/app/helpers/issues_helper.rb
index 6013f1ec8..c6de00c10 100644
--- a/groups/app/helpers/issues_helper.rb
+++ b/groups/app/helpers/issues_helper.rb
@@ -54,9 +54,15 @@ module IssuesHelper
when 'due_date', 'start_date'
value = format_date(detail.value.to_date) if detail.value
old_value = format_date(detail.old_value.to_date) if detail.old_value
+ when 'project_id'
+ p = Project.find_by_id(detail.value) and value = p.name if detail.value
+ p = Project.find_by_id(detail.old_value) and old_value = p.name if detail.old_value
when 'status_id'
s = IssueStatus.find_by_id(detail.value) and value = s.name if detail.value
s = IssueStatus.find_by_id(detail.old_value) and old_value = s.name if detail.old_value
+ when 'tracker_id'
+ t = Tracker.find_by_id(detail.value) and value = t.name if detail.value
+ t = Tracker.find_by_id(detail.old_value) and old_value = t.name if detail.old_value
when 'assigned_to_id'
u = User.find_by_id(detail.value) and value = u.name if detail.value
u = User.find_by_id(detail.old_value) and old_value = u.name if detail.old_value
@@ -69,6 +75,9 @@ module IssuesHelper
when 'fixed_version_id'
v = Version.find_by_id(detail.value) and value = v.name if detail.value
v = Version.find_by_id(detail.old_value) and old_value = v.name if detail.old_value
+ when 'estimated_hours'
+ value = "%0.02f" % detail.value.to_f unless detail.value.blank?
+ old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
end
when 'cf'
custom_field = CustomField.find_by_id(detail.prop_key)
@@ -89,9 +98,9 @@ module IssuesHelper
label = content_tag('strong', label)
old_value = content_tag("i", h(old_value)) if detail.old_value
old_value = content_tag("strike", old_value) if detail.old_value and (!detail.value or detail.value.empty?)
- if detail.property == 'attachment' && !value.blank? && Attachment.find_by_id(detail.prop_key)
+ if detail.property == 'attachment' && !value.blank? && a = Attachment.find_by_id(detail.prop_key)
# Link to the attachment if it has not been removed
- value = link_to(value, :controller => 'attachments', :action => 'download', :id => detail.prop_key)
+ value = link_to_attachment(a)
else
value = content_tag("i", h(value)) if value
end
@@ -120,6 +129,7 @@ module IssuesHelper
def issues_to_csv(issues, project = nil)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ decimal_separator = l(:general_csv_decimal_separator)
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
@@ -142,7 +152,7 @@ module IssuesHelper
]
# Export project custom fields if project is given
# otherwise export custom fields marked as "For all projects"
- custom_fields = project.nil? ? IssueCustomField.for_all : project.all_custom_fields
+ custom_fields = project.nil? ? IssueCustomField.for_all : project.all_issue_custom_fields
custom_fields.each {|f| headers << f.name}
# Description in the last column
headers << l(:field_description)
@@ -162,7 +172,7 @@ module IssuesHelper
format_date(issue.start_date),
format_date(issue.due_date),
issue.done_ratio,
- issue.estimated_hours,
+ issue.estimated_hours.to_s.gsub('.', decimal_separator),
format_time(issue.created_on),
format_time(issue.updated_on)
]
diff --git a/groups/app/helpers/journals_helper.rb b/groups/app/helpers/journals_helper.rb
index 234bfabc0..45579f771 100644
--- a/groups/app/helpers/journals_helper.rb
+++ b/groups/app/helpers/journals_helper.rb
@@ -19,13 +19,16 @@ module JournalsHelper
def render_notes(journal, options={})
content = ''
editable = journal.editable_by?(User.current)
- if editable && !journal.notes.blank?
- links = []
+ links = []
+ if !journal.notes.blank?
+ links << link_to_remote(image_tag('comment.png'),
+ { :url => {:controller => 'issues', :action => 'reply', :id => journal.journalized, :journal_id => journal} },
+ :title => l(:button_quote)) if options[:reply_links]
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
{ :controller => 'journals', :action => 'edit', :id => journal },
- :title => l(:button_edit))
- content << content_tag('div', links.join(' '), :class => 'contextual')
+ :title => l(:button_edit)) if editable
end
+ content << content_tag('div', links.join(' '), :class => 'contextual') unless links.empty?
content << textilizable(journal, :notes)
content_tag('div', content, :id => "journal-#{journal.id}-notes", :class => (editable ? 'wiki editable' : 'wiki'))
end
diff --git a/groups/app/helpers/mail_handler_helper.rb b/groups/app/helpers/mail_handler_helper.rb
new file mode 100644
index 000000000..a29a6dd5a
--- /dev/null
+++ b/groups/app/helpers/mail_handler_helper.rb
@@ -0,0 +1,19 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module MailHandlerHelper
+end
diff --git a/groups/app/helpers/projects_helper.rb b/groups/app/helpers/projects_helper.rb
index 0a076be99..23e4ae6c7 100644
--- a/groups/app/helpers/projects_helper.rb
+++ b/groups/app/helpers/projects_helper.rb
@@ -21,18 +21,22 @@ module ProjectsHelper
link_to h(version.name), { :controller => 'versions', :action => 'show', :id => version }, options
end
+ def format_activity_title(text)
+ h(truncate_single_line(text, 100))
+ end
+
def format_activity_day(date)
date == Date.today ? l(:label_today).titleize : format_date(date)
end
def format_activity_description(text)
- h(truncate(text, 250))
+ h(truncate(text.to_s, 250).gsub(%r{<(pre|code)>.*$}m, '...'))
end
# Renders the member list displayed on project overview
def render_member_list(project)
parts = []
- memberships_by_role = project.memberships.find(:all, :include => :role, :order => 'position').group_by {|m| m.role}
+ memberships_by_role = project.memberships.find(:all, :include => :role, :order => "#{Role.table_name}.position").group_by {|m| m.role}
memberships_by_role.keys.sort.each do |role|
role_parts = []
# Display group name (with its 5 first users) or user name
diff --git a/groups/app/helpers/repositories_helper.rb b/groups/app/helpers/repositories_helper.rb
index 22bdec9df..852ed18d7 100644
--- a/groups/app/helpers/repositories_helper.rb
+++ b/groups/app/helpers/repositories_helper.rb
@@ -15,20 +15,23 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-require 'coderay'
-require 'coderay/helpers/file_type'
require 'iconv'
module RepositoriesHelper
- def syntax_highlight(name, content)
- type = CodeRay::FileType[name]
- type ? CodeRay.scan(content, type).html : h(content)
- end
-
def format_revision(txt)
txt.to_s[0,8]
end
+ def render_properties(properties)
+ unless properties.nil? || properties.empty?
+ content = ''
+ properties.keys.sort.each do |property|
+ content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>")
+ end
+ content_tag('ul', content, :class => 'properties')
+ end
+ end
+
def to_utf8(str)
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
@encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@@ -48,10 +51,13 @@ module RepositoriesHelper
end
def scm_select_tag(repository)
- container = [[]]
- REDMINE_SUPPORTED_SCM.each {|scm| container << ["Repository::#{scm}".constantize.scm_name, scm]}
+ scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
+ REDMINE_SUPPORTED_SCM.each do |scm|
+ scm_options << ["Repository::#{scm}".constantize.scm_name, scm] if Setting.enabled_scm.include?(scm) || (repository && repository.class.name.demodulize == scm)
+ end
+
select_tag('repository_scm',
- options_for_select(container, repository.class.name.demodulize),
+ options_for_select(scm_options, repository.class.name.demodulize),
:disabled => (repository && !repository.new_record?),
:onchange => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")
)
@@ -95,4 +101,8 @@ module RepositoriesHelper
def bazaar_field_tags(form, repository)
content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?)))
end
+
+ def filesystem_field_tags(form, repository)
+ content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+ end
end
diff --git a/groups/app/helpers/search_helper.rb b/groups/app/helpers/search_helper.rb
index ed2f40b69..cd96dbd3f 100644
--- a/groups/app/helpers/search_helper.rb
+++ b/groups/app/helpers/search_helper.rb
@@ -18,7 +18,8 @@
module SearchHelper
def highlight_tokens(text, tokens)
return text unless text && tokens && !tokens.empty?
- regexp = Regexp.new "(#{tokens.join('|')})", Regexp::IGNORECASE
+ re_tokens = tokens.collect {|t| Regexp.escape(t)}
+ regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
result = ''
text.split(regexp).each_with_index do |words, i|
if result.length > 1200
@@ -35,4 +36,28 @@ module SearchHelper
end
result
end
+
+ def type_label(t)
+ l("label_#{t.singularize}_plural")
+ end
+
+ def project_select_tag
+ options = [[l(:label_project_all), 'all']]
+ options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
+ options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty?
+ options << [@project.name, ''] unless @project.nil?
+ select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
+ end
+
+ def render_results_by_type(results_by_type)
+ links = []
+ # Sorts types by results count
+ results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
+ c = results_by_type[t]
+ next if c == 0
+ text = "#{type_label(t)} (#{c})"
+ links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
+ end
+ ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
+ end
end
diff --git a/groups/app/helpers/settings_helper.rb b/groups/app/helpers/settings_helper.rb
index f4ec5a7a7..d88269f7d 100644
--- a/groups/app/helpers/settings_helper.rb
+++ b/groups/app/helpers/settings_helper.rb
@@ -21,6 +21,7 @@ module SettingsHelper
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
{:name => 'notifications', :partial => 'settings/notifications', :label => l(:field_mail_notification)},
+ {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => l(:label_incoming_emails)},
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
]
end
diff --git a/groups/app/helpers/sort_helper.rb b/groups/app/helpers/sort_helper.rb
index f16ff3c7d..9ca5c11bd 100644
--- a/groups/app/helpers/sort_helper.rb
+++ b/groups/app/helpers/sort_helper.rb
@@ -83,7 +83,7 @@ module SortHelper
# Use this to sort the controller's table items collection.
#
def sort_clause()
- session[@sort_name][:key] + ' ' + session[@sort_name][:order]
+ session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
end
# Returns a link which sorts by the named column.
diff --git a/groups/app/helpers/timelog_helper.rb b/groups/app/helpers/timelog_helper.rb
index db13556a1..2c1225a7c 100644
--- a/groups/app/helpers/timelog_helper.rb
+++ b/groups/app/helpers/timelog_helper.rb
@@ -16,6 +16,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module TimelogHelper
+ def activity_collection_for_select_options
+ activities = Enumeration::get_values('ACTI')
+ collection = []
+ collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
+ activities.each { |a| collection << [a.name, a.id] }
+ collection
+ end
+
def select_hours(data, criteria, value)
data.select {|row| row[criteria] == value}
end
@@ -44,6 +52,8 @@ module TimelogHelper
def entries_to_csv(entries)
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
+ decimal_separator = l(:general_csv_decimal_separator)
+ custom_fields = TimeEntryCustomField.find(:all)
export = StringIO.new
CSV::Writer.generate(export, l(:general_csv_separator)) do |csv|
# csv header fields
@@ -57,6 +67,9 @@ module TimelogHelper
l(:field_hours),
l(:field_comments)
]
+ # Export custom fields
+ headers += custom_fields.collect(&:name)
+
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
# csv lines
entries.each do |entry|
@@ -67,9 +80,11 @@ module TimelogHelper
(entry.issue ? entry.issue.id : nil),
(entry.issue ? entry.issue.tracker : nil),
(entry.issue ? entry.issue.subject : nil),
- entry.hours,
+ entry.hours.to_s.gsub('.', decimal_separator),
entry.comments
]
+ fields += custom_fields.collect {|f| show_value(entry.custom_value_for(f)) }
+
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
end
end
diff --git a/groups/app/helpers/users_helper.rb b/groups/app/helpers/users_helper.rb
index 250ed8ce8..5b113e880 100644
--- a/groups/app/helpers/users_helper.rb
+++ b/groups/app/helpers/users_helper.rb
@@ -16,11 +16,26 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module UsersHelper
- def status_options_for_select(selected)
+ def users_status_options_for_select(selected)
+ user_count_by_status = User.count(:group => 'status').to_hash
options_for_select([[l(:label_all), ''],
- [l(:status_active), 1],
- [l(:status_registered), 2],
- [l(:status_locked), 3]], selected)
+ ["#{l(:status_active)} (#{user_count_by_status[1].to_i})", 1],
+ ["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", 2],
+ ["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", 3]], selected)
+ end
+
+ # Options for the new membership projects combo-box
+ def projects_options_for_select(projects)
+ options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
+ projects_by_root = projects.group_by(&:root)
+ projects_by_root.keys.sort.each do |root|
+ options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root)))
+ projects_by_root[root].sort.each do |project|
+ next if project == root
+ options << content_tag('option', '&#187; ' + h(project.name), :value => project.id)
+ end
+ end
+ options
end
def change_status_link(user)
@@ -30,8 +45,14 @@ module UsersHelper
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
elsif user.registered?
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :post, :class => 'icon icon-unlock'
- else
+ elsif user != User.current
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :post, :class => 'icon icon-lock'
end
end
+
+ def user_settings_tabs
+ tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
+ {:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
+ ]
+ end
end
diff --git a/groups/app/helpers/watchers_helper.rb b/groups/app/helpers/watchers_helper.rb
index c83c785fc..f4767ebed 100644
--- a/groups/app/helpers/watchers_helper.rb
+++ b/groups/app/helpers/watchers_helper.rb
@@ -24,7 +24,7 @@ module WatchersHelper
return '' unless user && user.logged? && object.respond_to?('watched_by?')
watched = object.watched_by?(user)
url = {:controller => 'watchers',
- :action => (watched ? 'remove' : 'add'),
+ :action => (watched ? 'unwatch' : 'watch'),
:object_type => object.class.to_s.underscore,
:object_id => object.id}
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
@@ -33,4 +33,9 @@ module WatchersHelper
:class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
end
+
+ # Returns a comma separated list of users watching the given object
+ def watchers_list(object)
+ object.watcher_users.collect {|u| content_tag('span', link_to_user(u), :class => 'user') }.join(",\n")
+ end
end
diff --git a/groups/app/helpers/wiki_helper.rb b/groups/app/helpers/wiki_helper.rb
index 980035bd4..0a6b810de 100644
--- a/groups/app/helpers/wiki_helper.rb
+++ b/groups/app/helpers/wiki_helper.rb
@@ -17,6 +17,22 @@
module WikiHelper
+ def render_page_hierarchy(pages, node=nil)
+ content = ''
+ if pages[node]
+ content << "<ul class=\"pages-hierarchy\">\n"
+ pages[node].each do |page|
+ content << "<li>"
+ content << link_to(h(page.pretty_title), {:action => 'index', :page => page.title},
+ :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
+ content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
+ content << "</li>\n"
+ end
+ content << "</ul>\n"
+ end
+ content
+ end
+
def html_diff(wdiff)
words = wdiff.words.collect{|word| h(word)}
words_add = 0
diff --git a/groups/app/models/attachment.rb b/groups/app/models/attachment.rb
index 08f440816..95ba8491f 100644
--- a/groups/app/models/attachment.rb
+++ b/groups/app/models/attachment.rb
@@ -26,7 +26,19 @@ class Attachment < ActiveRecord::Base
validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename,
- :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id}}
+ :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
+
+ acts_as_activity_provider :type => 'files',
+ :permission => :view_files,
+ :find_options => {:select => "#{Attachment.table_name}.*",
+ :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id"}
+
+ acts_as_activity_provider :type => 'documents',
+ :permission => :view_documents,
+ :find_options => {:select => "#{Attachment.table_name}.*",
+ :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
cattr_accessor :storage_path
@@storage_path = "#{RAILS_ROOT}/files"
@@ -40,7 +52,7 @@ class Attachment < ActiveRecord::Base
@temp_file = incoming_file
if @temp_file.size > 0
self.filename = sanitize_filename(@temp_file.original_filename)
- self.disk_filename = DateTime.now.strftime("%y%m%d%H%M%S") + "_" + self.filename
+ self.disk_filename = Attachment.disk_filename(filename)
self.content_type = @temp_file.content_type.to_s.chomp
self.filesize = @temp_file.size
end
@@ -68,9 +80,7 @@ class Attachment < ActiveRecord::Base
# Deletes file on the disk
def after_destroy
- if self.filename?
- File.delete(diskfile) if File.exist?(diskfile)
- end
+ File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
end
# Returns file's location on disk
@@ -90,6 +100,14 @@ class Attachment < ActiveRecord::Base
self.filename =~ /\.(jpe?g|gif|png)$/i
end
+ def is_text?
+ Redmine::MimeType.is_type?('text', filename)
+ end
+
+ def is_diff?
+ self.filename =~ /\.(patch|diff)$/i
+ end
+
private
def sanitize_filename(value)
# get only the filename, not the whole path
@@ -100,4 +118,17 @@ private
# Finally, replace all non alphanumeric, hyphens or periods with underscore
@filename = just_filename.gsub(/[^\w\.\-]/,'_')
end
+
+ # Returns an ASCII or hashed filename
+ def self.disk_filename(filename)
+ df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
+ if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
+ df << filename
+ else
+ df << Digest::MD5.hexdigest(filename)
+ # keep the extension if any
+ df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
+ end
+ df
+ end
end
diff --git a/groups/app/models/auth_source.rb b/groups/app/models/auth_source.rb
index 47c121a13..a0a2cdc5f 100644
--- a/groups/app/models/auth_source.rb
+++ b/groups/app/models/auth_source.rb
@@ -20,10 +20,7 @@ class AuthSource < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
- validates_length_of :name, :host, :maximum => 60
- validates_length_of :account_password, :maximum => 60, :allow_nil => true
- validates_length_of :account, :base_dn, :maximum => 255
- validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30
+ validates_length_of :name, :maximum => 60
def authenticate(login, password)
end
diff --git a/groups/app/models/auth_source_ldap.rb b/groups/app/models/auth_source_ldap.rb
index a438bd3c7..655ffd6d5 100644
--- a/groups/app/models/auth_source_ldap.rb
+++ b/groups/app/models/auth_source_ldap.rb
@@ -20,7 +20,10 @@ require 'iconv'
class AuthSourceLdap < AuthSource
validates_presence_of :host, :port, :attr_login
- validates_presence_of :attr_firstname, :attr_lastname, :attr_mail, :if => Proc.new { |a| a.onthefly_register? }
+ validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true
+ validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true
+ validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
+ validates_numericality_of :port, :only_integer => true
def after_initialize
self.port = 389 if self.port == 0
diff --git a/groups/app/models/change.rb b/groups/app/models/change.rb
index d14f435a4..385fe5acb 100644
--- a/groups/app/models/change.rb
+++ b/groups/app/models/change.rb
@@ -19,4 +19,8 @@ class Change < ActiveRecord::Base
belongs_to :changeset
validates_presence_of :changeset_id, :action, :path
+
+ def relative_path
+ changeset.repository.relative_path(path)
+ end
end
diff --git a/groups/app/models/changeset.rb b/groups/app/models/changeset.rb
index 3e95ce111..c4258c88b 100644
--- a/groups/app/models/changeset.rb
+++ b/groups/app/models/changeset.rb
@@ -15,6 +15,8 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+require 'iconv'
+
class Changeset < ActiveRecord::Base
belongs_to :repository
has_many :changes, :dependent => :delete_all
@@ -27,9 +29,12 @@ class Changeset < ActiveRecord::Base
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
acts_as_searchable :columns => 'comments',
- :include => :repository,
+ :include => {:repository => :project},
:project_key => "#{Repository.table_name}.project_id",
:date_column => 'committed_on'
+
+ acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
+ :find_options => {:include => {:repository => :project}}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id
@@ -40,7 +45,7 @@ class Changeset < ActiveRecord::Base
end
def comments=(comment)
- write_attribute(:comments, comment.strip)
+ write_attribute(:comments, Changeset.normalize_comments(comment))
end
def committed_on=(date)
@@ -75,7 +80,7 @@ class Changeset < ActiveRecord::Base
if ref_keywords.delete('*')
# find any issue ID in the comments
target_issue_ids = []
- comments.scan(%r{([\s\(,-^])#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
+ comments.scan(%r{([\s\(,-]|^)#(\d+)(?=[[:punct:]]|\s|<|$)}).each { |m| target_issue_ids << m[1] }
referenced_issues += repository.project.issues.find_all_by_id(target_issue_ids)
end
@@ -128,4 +133,24 @@ class Changeset < ActiveRecord::Base
def next
@next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
end
+
+ # Strips and reencodes a commit log before insertion into the database
+ def self.normalize_comments(str)
+ to_utf8(str.to_s.strip)
+ end
+
+ private
+
+ def self.to_utf8(str)
+ return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
+ encoding = Setting.commit_logs_encoding.to_s.strip
+ unless encoding.blank? || encoding == 'UTF-8'
+ begin
+ return Iconv.conv('UTF-8', encoding, str)
+ rescue Iconv::Failure
+ # do nothing here
+ end
+ end
+ str
+ end
end
diff --git a/groups/app/models/custom_field.rb b/groups/app/models/custom_field.rb
index 990adf9e2..4759b714b 100644
--- a/groups/app/models/custom_field.rb
+++ b/groups/app/models/custom_field.rb
@@ -30,9 +30,9 @@ class CustomField < ActiveRecord::Base
}.freeze
validates_presence_of :name, :field_format
- validates_uniqueness_of :name
+ validates_uniqueness_of :name, :scope => :type
validates_length_of :name, :maximum => 30
- validates_format_of :name, :with => /^[\w\s\'\-]*$/i
+ validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
def initialize(attributes = nil)
@@ -66,7 +66,7 @@ class CustomField < ActiveRecord::Base
# to move in project_custom_field
def self.for_all
- find(:all, :conditions => ["is_for_all=?", true])
+ find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
end
def type_name
diff --git a/groups/app/models/custom_value.rb b/groups/app/models/custom_value.rb
index 98ce6b168..1d453baf0 100644
--- a/groups/app/models/custom_value.rb
+++ b/groups/app/models/custom_value.rb
@@ -25,6 +25,11 @@ class CustomValue < ActiveRecord::Base
end
end
+ # Returns true if the boolean custom value is true
+ def true?
+ self.value == '1'
+ end
+
protected
def validate
if value.blank?
diff --git a/groups/app/models/document.rb b/groups/app/models/document.rb
index 7a432b46b..627a2418f 100644
--- a/groups/app/models/document.rb
+++ b/groups/app/models/document.rb
@@ -20,11 +20,12 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
has_many :attachments, :as => :container, :dependent => :destroy
- acts_as_searchable :columns => ['title', 'description']
+ acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
-
+ acts_as_activity_provider :find_options => {:include => :project}
+
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
end
diff --git a/groups/app/models/enumeration.rb b/groups/app/models/enumeration.rb
index 400681a43..d32a0c049 100644
--- a/groups/app/models/enumeration.rb
+++ b/groups/app/models/enumeration.rb
@@ -23,12 +23,12 @@ class Enumeration < ActiveRecord::Base
validates_presence_of :opt, :name
validates_uniqueness_of :name, :scope => [:opt]
validates_length_of :name, :maximum => 30
- validates_format_of :name, :with => /^[\w\s\'\-]*$/i
+ # Single table inheritance would be an option
OPTIONS = {
- "IPRI" => :enumeration_issue_priorities,
- "DCAT" => :enumeration_doc_categories,
- "ACTI" => :enumeration_activities
+ "IPRI" => {:label => :enumeration_issue_priorities, :model => Issue, :foreign_key => :priority_id},
+ "DCAT" => {:label => :enumeration_doc_categories, :model => Document, :foreign_key => :category_id},
+ "ACTI" => {:label => :enumeration_activities, :model => TimeEntry, :foreign_key => :activity_id}
}.freeze
def self.get_values(option)
@@ -40,13 +40,32 @@ class Enumeration < ActiveRecord::Base
end
def option_name
- OPTIONS[self.opt]
+ OPTIONS[self.opt][:label]
end
def before_save
Enumeration.update_all("is_default = #{connection.quoted_false}", {:opt => opt}) if is_default?
end
+ def objects_count
+ OPTIONS[self.opt][:model].count(:conditions => "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
+ end
+
+ def in_use?
+ self.objects_count != 0
+ end
+
+ alias :destroy_without_reassign :destroy
+
+ # Destroy the enumeration
+ # If a enumeration is specified, objects are reassigned
+ def destroy(reassign_to = nil)
+ if reassign_to && reassign_to.is_a?(Enumeration)
+ OPTIONS[self.opt][:model].update_all("#{OPTIONS[self.opt][:foreign_key]} = #{reassign_to.id}", "#{OPTIONS[self.opt][:foreign_key]} = #{id}")
+ end
+ destroy_without_reassign
+ end
+
def <=>(enumeration)
position <=> enumeration.position
end
@@ -55,13 +74,6 @@ class Enumeration < ActiveRecord::Base
private
def check_integrity
- case self.opt
- when "IPRI"
- raise "Can't delete enumeration" if Issue.find(:first, :conditions => ["priority_id=?", self.id])
- when "DCAT"
- raise "Can't delete enumeration" if Document.find(:first, :conditions => ["category_id=?", self.id])
- when "ACTI"
- raise "Can't delete enumeration" if TimeEntry.find(:first, :conditions => ["activity_id=?", self.id])
- end
+ raise "Can't delete enumeration" if self.in_use?
end
end
diff --git a/groups/app/models/issue.rb b/groups/app/models/issue.rb
index 8082e43b7..4701e41f1 100644
--- a/groups/app/models/issue.rb
+++ b/groups/app/models/issue.rb
@@ -28,23 +28,26 @@ class Issue < ActiveRecord::Base
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
- has_many :custom_values, :dependent => :delete_all, :as => :customized
- has_many :custom_fields, :through => :custom_values
- has_and_belongs_to_many :changesets, :order => "revision ASC"
+ has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
+ acts_as_customizable
acts_as_watchable
- acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
+ acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
+ :include => [:project, :journals],
+ # sort by id so that limited eager loading doesn't break with postgresql
+ :order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
+ acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}
+
validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
validates_inclusion_of :done_ratio, :in => 0..100
validates_numericality_of :estimated_hours, :allow_nil => true
- validates_associated :custom_values, :on => :update
def after_initialize
if new_record?
@@ -54,6 +57,11 @@ class Issue < ActiveRecord::Base
end
end
+ # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
+ def available_custom_fields
+ (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
+ end
+
def copy_from(arg)
issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
self.attributes = issue.attributes.dup
@@ -71,7 +79,9 @@ class Issue < ActiveRecord::Base
self.relations_to.clear
end
# issue is moved to another project
- self.category = nil
+ # reassign to the category with same name if any
+ new_category = category.nil? ? nil : new_project.issue_categories.find_by_name(category.name)
+ self.category = new_category
self.fixed_version = nil
self.project = new_project
end
@@ -168,17 +178,14 @@ class Issue < ActiveRecord::Base
end
end
- def custom_value_for(custom_field)
- self.custom_values.each {|v| return v if v.custom_field_id == custom_field.id }
- return nil
- end
-
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
+ # Make sure updated_on is updated when adding a note.
+ updated_on_will_change!
@current_journal
end
@@ -225,9 +232,15 @@ class Issue < ActiveRecord::Base
dependencies
end
- # Returns an array of the duplicate issues
+ # Returns an array of issues that duplicate this one
def duplicates
- relations.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.other_issue(self)}
+ relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
+ end
+
+ # Returns the due date or the target due date if any
+ # Used on gantt chart
+ def due_before
+ due_date || (fixed_version ? fixed_version.effective_date : nil)
end
def duration
diff --git a/groups/app/models/issue_relation.rb b/groups/app/models/issue_relation.rb
index 07e940b85..49329e0bb 100644
--- a/groups/app/models/issue_relation.rb
+++ b/groups/app/models/issue_relation.rb
@@ -25,7 +25,7 @@ class IssueRelation < ActiveRecord::Base
TYPE_PRECEDES = "precedes"
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
- TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicates, :order => 2 },
+ TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
}.freeze
diff --git a/groups/app/models/journal.rb b/groups/app/models/journal.rb
index 1376d349e..71a51290b 100644
--- a/groups/app/models/journal.rb
+++ b/groups/app/models/journal.rb
@@ -25,17 +25,18 @@ class Journal < ActiveRecord::Base
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
- acts_as_searchable :columns => 'notes',
- :include => :issue,
- :project_key => "#{Issue.table_name}.project_id",
- :date_column => "#{Issue.table_name}.created_on"
-
- acts_as_event :title => Proc.new {|o| "#{o.issue.tracker.name} ##{o.issue.id}: #{o.issue.subject}" + ((s = o.new_status) ? " (#{s})" : '') },
+ acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
- :type => Proc.new {|o| (s = o.new_status) && s.is_closed? ? 'issue-closed' : 'issue-edit' },
+ :type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
+ acts_as_activity_provider :type => 'issues',
+ :permission => :view_issues,
+ :find_options => {:include => [{:issue => :project}, :details, :user],
+ :conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
+ " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
+
def save
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
diff --git a/groups/app/models/mail_handler.rb b/groups/app/models/mail_handler.rb
index 7a1d73244..2f1eba3e9 100644
--- a/groups/app/models/mail_handler.rb
+++ b/groups/app/models/mail_handler.rb
@@ -16,25 +16,138 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class MailHandler < ActionMailer::Base
+
+ class UnauthorizedAction < StandardError; end
+ class MissingInformation < StandardError; end
+
+ attr_reader :email, :user
+
+ def self.receive(email, options={})
+ @@handler_options = options.dup
+
+ @@handler_options[:issue] ||= {}
+
+ @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
+ @@handler_options[:allow_override] ||= []
+ # Project needs to be overridable if not specified
+ @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
+ # Status needs to be overridable if not specified
+ @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
+ super email
+ end
# Processes incoming emails
- # Currently, it only supports adding a note to an existing issue
- # by replying to the initial notification message
def receive(email)
- # find related issue by parsing the subject
- m = email.subject.match %r{\[.*#(\d+)\]}
- return unless m
- issue = Issue.find_by_id(m[1])
- return unless issue
-
- # find user
- user = User.find_active(:first, :conditions => {:mail => email.from.first})
- return unless user
+ @email = email
+ @user = User.find_active(:first, :conditions => {:mail => email.from.first})
+ unless @user
+ # Unknown user => the email is ignored
+ # TODO: ability to create the user's account
+ logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
+ return false
+ end
+ User.current = @user
+ dispatch
+ end
+
+ private
+
+ ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
+
+ def dispatch
+ if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
+ receive_issue_update(m[1].to_i)
+ else
+ receive_issue
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ # TODO: send a email to the user
+ logger.error e.message if logger
+ false
+ rescue MissingInformation => e
+ logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
+ false
+ rescue UnauthorizedAction => e
+ logger.error "MailHandler: unauthorized attempt from #{user}" if logger
+ false
+ end
+
+ # Creates a new issue
+ def receive_issue
+ project = target_project
+ tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
+ category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
+ priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
+ status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status))) || IssueStatus.default
+
# check permission
- return unless user.allowed_to?(:add_issue_notes, issue.project)
+ raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
+ issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :status => status)
+ issue.subject = email.subject.chomp
+ issue.description = email.plain_text_body.chomp
+ issue.save!
+ add_attachments(issue)
+ logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
+ Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
+ issue
+ end
+
+ def target_project
+ # TODO: other ways to specify project:
+ # * parse the email To field
+ # * specific project (eg. Setting.mail_handler_target_project)
+ target = Project.find_by_identifier(get_keyword(:project))
+ raise MissingInformation.new('Unable to determine target project') if target.nil?
+ target
+ end
+
+ # Adds a note to an existing issue
+ def receive_issue_update(issue_id)
+ status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
+ issue = Issue.find_by_id(issue_id)
+ return unless issue
+ # check permission
+ raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
+ raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
+
# add the note
- issue.init_journal(user, email.body.chomp)
- issue.save
+ journal = issue.init_journal(user, email.plain_text_body.chomp)
+ add_attachments(issue)
+ issue.status = status unless status.nil?
+ issue.save!
+ logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
+ Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
+ journal
+ end
+
+ def add_attachments(obj)
+ if email.has_attachments?
+ email.attachments.each do |attachment|
+ Attachment.create(:container => obj,
+ :file => attachment,
+ :author => user,
+ :content_type => attachment.content_type)
+ end
+ end
+ end
+
+ def get_keyword(attr)
+ if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
+ $1.strip
+ elsif !@@handler_options[:issue][attr].blank?
+ @@handler_options[:issue][attr]
+ end
end
end
+
+class TMail::Mail
+ # Returns body of the first plain text part found if any
+ def plain_text_body
+ return @plain_text_body unless @plain_text_body.nil?
+ p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
+ plain = p.detect {|c| c.content_type == 'text/plain'}
+ @plain_text_body = plain.nil? ? self.body : plain.body
+ end
+end
+
diff --git a/groups/app/models/mailer.rb b/groups/app/models/mailer.rb
index 6fc879a15..61e5d596c 100644
--- a/groups/app/models/mailer.rb
+++ b/groups/app/models/mailer.rb
@@ -51,6 +51,15 @@ class Mailer < ActionMailer::Base
:issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
end
+ def reminder(user, issues, days)
+ set_language_if_valid user.language
+ recipients user.mail
+ subject l(:mail_subject_reminder, issues.size)
+ body :issues => issues,
+ :days => days,
+ :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc')
+ end
+
def document_added(document)
redmine_headers 'Project' => document.project.identifier
recipients document.project.recipients
@@ -144,6 +153,30 @@ class Mailer < ActionMailer::Base
(bcc.nil? || bcc.empty?)
super
end
+
+ # Sends reminders to issue assignees
+ # Available options:
+ # * :days => how many days in the future to remind about (defaults to 7)
+ # * :tracker => id of tracker for filtering issues (defaults to all trackers)
+ # * :project => id or identifier of project to process (defaults to all projects)
+ def self.reminders(options={})
+ days = options[:days] || 7
+ project = options[:project] ? Project.find(options[:project]) : nil
+ tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
+
+ s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
+ s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
+ s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
+ s << "#{Issue.table_name}.project_id = #{project.id}" if project
+ s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
+
+ issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
+ :conditions => s.conditions
+ ).group_by(&:assigned_to)
+ issues_by_assignee.each do |assignee, issues|
+ deliver_reminder(assignee, issues, days) unless assignee.nil?
+ end
+ end
private
def initialize_defaults(method_name)
diff --git a/groups/app/models/message.rb b/groups/app/models/message.rb
index a18d126c9..80df7a33a 100644
--- a/groups/app/models/message.rb
+++ b/groups/app/models/message.rb
@@ -23,14 +23,17 @@ class Message < ActiveRecord::Base
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_searchable :columns => ['subject', 'content'],
- :include => :board,
+ :include => {:board, :project},
:project_key => 'project_id',
- :date_column => 'created_on'
+ :date_column => "#{table_name}.created_on"
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:description => :content,
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
- :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id, :id => o.id}}
-
+ :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
+ {:id => o.parent_id, :anchor => "message-#{o.id}"})}
+
+ acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]}
+
attr_protected :locked, :sticky
validates_presence_of :subject, :content
validates_length_of :subject, :maximum => 255
diff --git a/groups/app/models/news.rb b/groups/app/models/news.rb
index 3d8c4d661..4c4943b78 100644
--- a/groups/app/models/news.rb
+++ b/groups/app/models/news.rb
@@ -24,9 +24,10 @@ class News < ActiveRecord::Base
validates_length_of :title, :maximum => 60
validates_length_of :summary, :maximum => 255
- acts_as_searchable :columns => ['title', 'description']
+ acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
-
+ acts_as_activity_provider :find_options => {:include => [:project, :author]}
+
# returns latest news for projects visible by user
def self.latest(user=nil, count=5)
find(:all, :limit => count, :conditions => Project.visible_by(user), :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
diff --git a/groups/app/models/project.rb b/groups/app/models/project.rb
index 3deac3231..f7feb7349 100644
--- a/groups/app/models/project.rb
+++ b/groups/app/models/project.rb
@@ -23,7 +23,6 @@ class Project < ActiveRecord::Base
has_many :members, :include => :user, :conditions => "#{Member.table_name}.principal_type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
has_many :memberships, :class_name => 'Member'
has_many :users, :through => :members, :uniq => true
- has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :enabled_modules, :dependent => :delete_all
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
@@ -39,7 +38,7 @@ class Project < ActiveRecord::Base
has_many :changesets, :through => :repository
has_one :wiki, :dependent => :destroy
# Custom field for the project issues
- has_and_belongs_to_many :custom_fields,
+ has_and_belongs_to_many :issue_custom_fields,
:class_name => 'IssueCustomField',
:order => "#{CustomField.table_name}.position",
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
@@ -47,18 +46,19 @@ class Project < ActiveRecord::Base
acts_as_tree :order => "name", :counter_cache => true
- acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
+ acts_as_customizable
+ acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
- :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
+ :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
+ :author => nil
attr_protected :status, :enabled_module_names
validates_presence_of :name, :identifier
validates_uniqueness_of :name, :identifier
- validates_associated :custom_values, :on => :update
validates_associated :repository, :wiki
validates_length_of :name, :maximum => 30
- validates_length_of :homepage, :maximum => 60
+ validates_length_of :homepage, :maximum => 255
validates_length_of :identifier, :in => 3..20
validates_format_of :identifier, :with => /^[a-z0-9\-]*$/
@@ -74,9 +74,9 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false)
conditions = nil
- if include_subprojects && !active_children.empty?
- ids = [id] + active_children.collect {|c| c.id}
- conditions = ["#{Project.table_name}.id IN (#{ids.join(',')})"]
+ if include_subprojects
+ ids = [id] + child_ids
+ conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
end
conditions ||= ["#{Project.table_name}.id = ?", id]
# Quick and dirty fix for Rails 2 compatibility
@@ -94,6 +94,7 @@ class Project < ActiveRecord::Base
end
def self.visible_by(user=nil)
+ user ||= User.current
if user && user.admin?
return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
elsif user && user.memberships.any?
@@ -113,16 +114,18 @@ class Project < ActiveRecord::Base
end
if user.admin?
# no restriction
- elsif user.logged?
- statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
- allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
- statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
- elsif Role.anonymous.allowed_to?(permission)
- # anonymous user allowed on public project
- statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
else
- # anonymous user is not authorized
statements << "1=0"
+ if user.logged?
+ statements << "#{Project.table_name}.is_public = #{connection.quoted_true}" if Role.non_member.allowed_to?(permission)
+ allowed_project_ids = user.memberships.select {|m| m.role.allowed_to?(permission)}.collect {|m| m.project_id}
+ statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
+ elsif Role.anonymous.allowed_to?(permission)
+ # anonymous user allowed on public project
+ statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
+ else
+ # anonymous user is not authorized
+ end
end
statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
end
@@ -144,7 +147,8 @@ class Project < ActiveRecord::Base
end
def to_param
- identifier
+ # id is used for projects with a numeric identifier (compatibility)
+ @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
end
def active?
@@ -194,12 +198,12 @@ class Project < ActiveRecord::Base
# Returns an array of all custom fields enabled for project issues
# (explictly associated custom fields and custom fields enabled for all projects)
- def custom_fields_for_issues(tracker)
- all_custom_fields.select {|c| tracker.custom_fields.include? c }
+ def all_issue_custom_fields
+ @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
end
- def all_custom_fields
- @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
+ def project
+ self
end
def <=>(project)
diff --git a/groups/app/models/query.rb b/groups/app/models/query.rb
index 641c0d17b..0ce9a6a21 100644
--- a/groups/app/models/query.rb
+++ b/groups/app/models/query.rb
@@ -88,7 +88,7 @@ class Query < ActiveRecord::Base
:date_past => [ ">t-", "<t-", "t-", "t", "w" ],
:string => [ "=", "~", "!", "!~" ],
:text => [ "~", "!~" ],
- :integer => [ "=", ">=", "<=" ] }
+ :integer => [ "=", ">=", "<=", "!*", "*" ] }
cattr_reader :operators_by_filter_type
@@ -125,7 +125,7 @@ class Query < ActiveRecord::Base
filters.each_key do |field|
errors.add label_for(field), :activerecord_error_blank unless
# filter requires one or more values
- (values_for(field) and !values_for(field).first.empty?) or
+ (values_for(field) and !values_for(field).first.blank?) or
# filter doesn't require any value
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
end if filters
@@ -152,7 +152,8 @@ class Query < ActiveRecord::Base
"updated_on" => { :type => :date_past, :order => 10 },
"start_date" => { :type => :date, :order => 11 },
"due_date" => { :type => :date, :order => 12 },
- "done_ratio" => { :type => :integer, :order => 13 }}
+ "estimated_hours" => { :type => :integer, :order => 13 },
+ "done_ratio" => { :type => :integer, :order => 14 }}
user_values = []
user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
@@ -166,29 +167,20 @@ class Query < ActiveRecord::Base
@available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
if project
- # project specific filters
- @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
- @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
+ # project specific filters
+ unless @project.issue_categories.empty?
+ @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
+ end
+ unless @project.versions.empty?
+ @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
+ end
unless @project.active_children.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } }
end
- @project.all_custom_fields.select(&:is_filter?).each do |field|
- case field.field_format
- when "text"
- options = { :type => :text, :order => 20 }
- when "list"
- options = { :type => :list_optional, :values => field.possible_values, :order => 20}
- when "date"
- options = { :type => :date, :order => 20 }
- when "bool"
- options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
- else
- options = { :type => :string, :order => 20 }
- end
- @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
- end
- # remove category filter if no category defined
- @available_filters.delete "category_id" if @available_filters["category_id"][:values].empty?
+ add_custom_fields_filters(@project.all_issue_custom_fields)
+ else
+ # global filters for cross project issue list
+ add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
end
@available_filters
end
@@ -227,7 +219,7 @@ class Query < ActiveRecord::Base
end
def label_for(field)
- label = @available_filters[field][:name] if @available_filters.has_key?(field)
+ label = available_filters[field][:name] if available_filters.has_key?(field)
label ||= field.gsub(/\_id$/, "")
end
@@ -235,7 +227,7 @@ class Query < ActiveRecord::Base
return @available_columns if @available_columns
@available_columns = Query.available_columns
@available_columns += (project ?
- project.all_custom_fields :
+ project.all_issue_custom_fields :
IssueCustomField.find(:all, :conditions => {:is_for_all => true})
).collect {|cf| QueryCustomFieldColumn.new(cf) }
end
@@ -265,7 +257,7 @@ class Query < ActiveRecord::Base
def statement
# project/subprojects clause
- clause = ''
+ project_clauses = []
if project && !@project.active_children.empty?
ids = [project.id]
if has_filter?("subproject_id")
@@ -277,17 +269,16 @@ class Query < ActiveRecord::Base
# main project only
else
# all subprojects
- ids += project.active_children.collect{|p| p.id}
+ ids += project.child_ids
end
elsif Setting.display_subprojects_issues?
- ids += project.active_children.collect{|p| p.id}
+ ids += project.child_ids
end
- clause << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
+ project_clauses << "#{Issue.table_name}.project_id IN (%s)" % ids.join(',')
elsif project
- clause << "#{Issue.table_name}.project_id = %d" % project.id
- else
- clause << Project.visible_by(User.current)
+ project_clauses << "#{Issue.table_name}.project_id = %d" % project.id
end
+ project_clauses << Project.visible_by(User.current)
# filters clauses
filters_clauses = []
@@ -296,11 +287,13 @@ class Query < ActiveRecord::Base
v = values_for(field).clone
next unless v and !v.empty?
- sql = ''
+ sql = ''
+ is_custom_filter = false
if field =~ /^cf_(\d+)$/
# custom field
db_table = CustomValue.table_name
db_field = 'value'
+ is_custom_filter = true
sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
else
# regular field
@@ -320,9 +313,11 @@ class Query < ActiveRecord::Base
when "!"
sql = sql + "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + v.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
when "!*"
- sql = sql + "#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} = ''"
+ sql = sql + "#{db_table}.#{db_field} IS NULL"
+ sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
when "*"
- sql = sql + "#{db_table}.#{db_field} IS NOT NULL AND #{db_table}.#{db_field} <> ''"
+ sql = sql + "#{db_table}.#{db_field} IS NOT NULL"
+ sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
when ">="
sql = sql + "#{db_table}.#{db_field} >= #{v.first.to_i}"
when "<="
@@ -361,8 +356,28 @@ class Query < ActiveRecord::Base
filters_clauses << sql
end if filters and valid?
- clause << ' AND ' unless clause.empty?
- clause << filters_clauses.join(' AND ') unless filters_clauses.empty?
- clause
+ (project_clauses + filters_clauses).join(' AND ')
+ end
+
+ private
+
+ def add_custom_fields_filters(custom_fields)
+ @available_filters ||= {}
+
+ custom_fields.select(&:is_filter?).each do |field|
+ case field.field_format
+ when "text"
+ options = { :type => :text, :order => 20 }
+ when "list"
+ options = { :type => :list_optional, :values => field.possible_values, :order => 20}
+ when "date"
+ options = { :type => :date, :order => 20 }
+ when "bool"
+ options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
+ else
+ options = { :type => :string, :order => 20 }
+ end
+ @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
+ end
end
end
diff --git a/groups/app/models/repository.rb b/groups/app/models/repository.rb
index 8b1f8d0af..9768e3e3c 100644
--- a/groups/app/models/repository.rb
+++ b/groups/app/models/repository.rb
@@ -17,9 +17,16 @@
class Repository < ActiveRecord::Base
belongs_to :project
- has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
+ has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
has_many :changes, :through => :changesets
-
+
+ # Raw SQL to delete changesets and changes in the database
+ # has_many :changesets, :dependent => :destroy is too slow for big repositories
+ before_destroy :clear_changesets
+
+ # Checks if the SCM is enabled when creating a repository
+ validate_on_create { |r| r.errors.add(:type, :activerecord_error_invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
+
# Removes leading and trailing whitespace
def url=(arg)
write_attribute(:url, arg ? arg.to_s.strip : nil)
@@ -48,12 +55,24 @@ class Repository < ActiveRecord::Base
scm.supports_annotate?
end
+ def entry(path=nil, identifier=nil)
+ scm.entry(path, identifier)
+ end
+
def entries(path=nil, identifier=nil)
scm.entries(path, identifier)
end
- def diff(path, rev, rev_to, type)
- scm.diff(path, rev, rev_to, type)
+ def properties(path, identifier=nil)
+ scm.properties(path, identifier)
+ end
+
+ def cat(path, identifier=nil)
+ scm.cat(path, identifier)
+ end
+
+ def diff(path, rev, rev_to)
+ scm.diff(path, rev, rev_to)
end
# Default behaviour: we search in cached changesets
@@ -64,6 +83,11 @@ class Repository < ActiveRecord::Base
:order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
end
+ # Returns a path relative to the url of the repository
+ def relative_path(path)
+ path
+ end
+
def latest_changeset
@latest_changeset ||= changesets.find(:first)
end
@@ -107,4 +131,9 @@ class Repository < ActiveRecord::Base
root_url.strip!
true
end
+
+ def clear_changesets
+ connection.delete("DELETE FROM changes WHERE changes.changeset_id IN (SELECT changesets.id FROM changesets WHERE changesets.repository_id = #{id})")
+ connection.delete("DELETE FROM changesets WHERE changesets.repository_id = #{id}")
+ end
end
diff --git a/groups/app/models/repository/bazaar.rb b/groups/app/models/repository/bazaar.rb
index 1b75066c2..ec953bd45 100644
--- a/groups/app/models/repository/bazaar.rb
+++ b/groups/app/models/repository/bazaar.rb
@@ -34,6 +34,11 @@ class Repository::Bazaar < Repository
if entries
entries.each do |e|
next if e.lastrev.revision.blank?
+ # Set the filesize unless browsing a specific revision
+ if identifier.nil? && e.is_file?
+ full_path = File.join(root_url, e.path)
+ e.size = File.stat(full_path).size if File.file?(full_path)
+ end
c = Change.find(:first,
:include => :changeset,
:conditions => ["#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?", e.lastrev.revision, id],
diff --git a/groups/app/models/repository/cvs.rb b/groups/app/models/repository/cvs.rb
index c2d8be977..82082b3d6 100644
--- a/groups/app/models/repository/cvs.rb
+++ b/groups/app/models/repository/cvs.rb
@@ -29,9 +29,9 @@ class Repository::Cvs < Repository
'CVS'
end
- def entry(path, identifier)
- e = entries(path, identifier)
- e ? e.first : nil
+ def entry(path=nil, identifier=nil)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.entry(path, rev.nil? ? nil : rev.committed_on)
end
def entries(path=nil, identifier=nil)
@@ -53,7 +53,12 @@ class Repository::Cvs < Repository
entries
end
- def diff(path, rev, rev_to, type)
+ def cat(path, identifier=nil)
+ rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.cat(path, rev.nil? ? nil : rev.committed_on)
+ end
+
+ def diff(path, rev, rev_to)
#convert rev to revision. CVS can't handle changesets here
diff=[]
changeset_from=changesets.find_by_revision(rev)
@@ -76,7 +81,8 @@ class Repository::Cvs < Repository
unless revision_to
revision_to=scm.get_previous_revision(revision_from)
end
- diff=diff+scm.diff(change_from.path, revision_from, revision_to, type)
+ file_diff = scm.diff(change_from.path, revision_from, revision_to)
+ diff = diff + file_diff unless file_diff.nil?
end
end
return diff
@@ -103,7 +109,7 @@ class Repository::Cvs < Repository
cs = changesets.find(:first, :conditions=>{
:committed_on=>revision.time-time_delta..revision.time+time_delta,
:committer=>revision.author,
- :comments=>revision.message
+ :comments=>Changeset.normalize_comments(revision.message)
})
# create a new changeset....
diff --git a/groups/app/models/repository/darcs.rb b/groups/app/models/repository/darcs.rb
index c7c14a397..855a403fc 100644
--- a/groups/app/models/repository/darcs.rb
+++ b/groups/app/models/repository/darcs.rb
@@ -28,6 +28,11 @@ class Repository::Darcs < Repository
'Darcs'
end
+ def entry(path=nil, identifier=nil)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.entry(path, patch.nil? ? nil : patch.scmid)
+ end
+
def entries(path=nil, identifier=nil)
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
@@ -46,14 +51,19 @@ class Repository::Darcs < Repository
entries
end
- def diff(path, rev, rev_to, type)
+ def cat(path, identifier=nil)
+ patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
+ scm.cat(path, patch.nil? ? nil : patch.scmid)
+ end
+
+ def diff(path, rev, rev_to)
patch_from = changesets.find_by_revision(rev)
return nil if patch_from.nil?
patch_to = changesets.find_by_revision(rev_to) if rev_to
if path.blank?
path = patch_from.changes.collect{|change| change.path}.join(' ')
end
- patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
+ patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
end
def fetch_changesets
diff --git a/groups/app/models/repository/filesystem.rb b/groups/app/models/repository/filesystem.rb
new file mode 100644
index 000000000..da096cc09
--- /dev/null
+++ b/groups/app/models/repository/filesystem.rb
@@ -0,0 +1,43 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# FileSystem adapter
+# File written by Paul Rivier, at Demotera.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/filesystem_adapter'
+
+class Repository::Filesystem < Repository
+ attr_protected :root_url
+ validates_presence_of :url
+
+ def scm_adapter
+ Redmine::Scm::Adapters::FilesystemAdapter
+ end
+
+ def self.scm_name
+ 'Filesystem'
+ end
+
+ def entries(path=nil, identifier=nil)
+ scm.entries(path, identifier)
+ end
+
+ def fetch_changesets
+ nil
+ end
+
+end
diff --git a/groups/app/models/repository/git.rb b/groups/app/models/repository/git.rb
index 7213588ac..2f440fe29 100644
--- a/groups/app/models/repository/git.rb
+++ b/groups/app/models/repository/git.rb
@@ -44,10 +44,8 @@ class Repository::Git < Repository
scm_revision = scm_info.lastrev.scmid
unless changesets.find_by_scmid(scm_revision)
-
- revisions = scm.revisions('', db_revision, nil)
- transaction do
- revisions.reverse_each do |revision|
+ scm.revisions('', db_revision, nil, :reverse => true) do |revision|
+ transaction do
changeset = Changeset.create(:repository => self,
:revision => revision.identifier,
:scmid => revision.scmid,
diff --git a/groups/app/models/repository/subversion.rb b/groups/app/models/repository/subversion.rb
index 0c2239c43..3981d6f4c 100644
--- a/groups/app/models/repository/subversion.rb
+++ b/groups/app/models/repository/subversion.rb
@@ -35,6 +35,11 @@ class Repository::Subversion < Repository
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC") : []
end
+ # Returns a path relative to the url of the repository
+ def relative_path(path)
+ path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
+ end
+
def fetch_changesets
scm_info = scm.info
if scm_info
@@ -71,4 +76,14 @@ class Repository::Subversion < Repository
end
end
end
+
+ private
+
+ # Returns the relative url of the repository
+ # Eg: root_url = file:///var/svn/foo
+ # url = file:///var/svn/foo/bar
+ # => returns /bar
+ def relative_url
+ @relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url)}"), '')
+ end
end
diff --git a/groups/app/models/setting.rb b/groups/app/models/setting.rb
index 185991d9b..072afa0db 100644
--- a/groups/app/models/setting.rb
+++ b/groups/app/models/setting.rb
@@ -33,6 +33,45 @@ class Setting < ActiveRecord::Base
'%H:%M',
'%I:%M %p'
]
+
+ ENCODINGS = %w(US-ASCII
+ windows-1250
+ windows-1251
+ windows-1252
+ windows-1253
+ windows-1254
+ windows-1255
+ windows-1256
+ windows-1257
+ windows-1258
+ windows-31j
+ ISO-2022-JP
+ ISO-2022-KR
+ ISO-8859-1
+ ISO-8859-2
+ ISO-8859-3
+ ISO-8859-4
+ ISO-8859-5
+ ISO-8859-6
+ ISO-8859-7
+ ISO-8859-8
+ ISO-8859-9
+ ISO-8859-13
+ ISO-8859-15
+ KOI8-R
+ UTF-8
+ UTF-16
+ UTF-16BE
+ UTF-16LE
+ EUC-JP
+ Shift_JIS
+ GB18030
+ GBK
+ ISCII91
+ EUC-KR
+ Big5
+ Big5-HKSCS
+ TIS-620)
cattr_accessor :available_settings
@@available_settings = YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml"))
diff --git a/groups/app/models/time_entry.rb b/groups/app/models/time_entry.rb
index ddaff2b60..57a75604d 100644
--- a/groups/app/models/time_entry.rb
+++ b/groups/app/models/time_entry.rb
@@ -24,11 +24,25 @@ class TimeEntry < ActiveRecord::Base
belongs_to :activity, :class_name => 'Enumeration', :foreign_key => :activity_id
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
+
+ acts_as_customizable
+ acts_as_event :title => Proc.new {|o| "#{o.user}: #{lwr(:label_f_hour, o.hours)} (#{(o.issue || o.project).event_title})"},
+ :url => Proc.new {|o| {:controller => 'timelog', :action => 'details', :project_id => o.project}},
+ :author => :user,
+ :description => :comments
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
validates_numericality_of :hours, :allow_nil => true
- validates_length_of :comments, :maximum => 255
+ validates_length_of :comments, :maximum => 255, :allow_nil => true
+ def after_initialize
+ if new_record? && self.activity.nil?
+ if default_activity = Enumeration.default('ACTI')
+ self.activity_id = default_activity.id
+ end
+ end
+ end
+
def before_validation
self.project = issue.project if issue && project.nil?
end
diff --git a/groups/app/models/time_entry_custom_field.rb b/groups/app/models/time_entry_custom_field.rb
new file mode 100644
index 000000000..2ec3d27be
--- /dev/null
+++ b/groups/app/models/time_entry_custom_field.rb
@@ -0,0 +1,23 @@
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+class TimeEntryCustomField < CustomField
+ def type_name
+ :label_spent_time
+ end
+end
+
diff --git a/groups/app/models/user.rb b/groups/app/models/user.rb
index 3743fcb3f..0e02cb78c 100644
--- a/groups/app/models/user.rb
+++ b/groups/app/models/user.rb
@@ -19,8 +19,6 @@ require "digest/sha1"
class User < ActiveRecord::Base
- class OnTheFlyCreationFailure < Exception; end
-
# Account statuses
STATUS_ANONYMOUS = 0
STATUS_ACTIVE = 1
@@ -39,17 +37,17 @@ class User < ActiveRecord::Base
:as => :principal,
:include => [ :project, :role ],
:conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}",
- :order => "#{Project.table_name}.name, inherited_from ASC",
- :dependent => :delete_all
-
+ :order => "#{Project.table_name}.name, inherited_from ASC"
+ has_many :members, :as => :principal, :dependent => :delete_all
has_many :projects, :through => :memberships
- has_many :custom_values, :dependent => :delete_all, :as => :customized
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
has_one :rss_token, :dependent => :destroy, :class_name => 'Token', :conditions => "action='feeds'"
belongs_to :auth_source
belongs_to :group
+ acts_as_customizable
+
attr_accessor :password, :password_confirmation
attr_accessor :last_before_login_on
# Prevents unauthorized assignments
@@ -61,13 +59,12 @@ class User < ActiveRecord::Base
# Login must contain lettres, numbers, underscores only
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
validates_length_of :login, :maximum => 30
- validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-]*$/i
+ validates_format_of :firstname, :lastname, :with => /^[\w\s\'\-\.]*$/i
validates_length_of :firstname, :lastname, :maximum => 30
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_nil => true
validates_length_of :mail, :maximum => 60, :allow_nil => true
validates_length_of :password, :minimum => 4, :allow_nil => true
validates_confirmation_of :password, :allow_nil => true
- validates_associated :custom_values, :on => :update
def before_create
self.mail_notification = false
@@ -96,6 +93,7 @@ class User < ActiveRecord::Base
def group_id=(gid)
@group_changed = true unless gid == group_id
+ group_id_will_change!
write_attribute(:group_id, gid)
end
@@ -130,19 +128,16 @@ class User < ActiveRecord::Base
# user is not yet registered, try to authenticate with available sources
attrs = AuthSource.authenticate(login, password)
if attrs
- onthefly = new(*attrs)
- onthefly.login = login
- onthefly.language = Setting.default_language
- if onthefly.save
- user = find(:first, :conditions => ["login=?", login])
+ user = new(*attrs)
+ user.login = login
+ user.language = Setting.default_language
+ if user.save
+ user.reload
logger.info("User '#{user.login}' created from the LDAP") if logger
- else
- logger.error("User '#{onthefly.login}' found in LDAP but could not be created (#{onthefly.errors.full_messages.join(', ')})") if logger
- raise OnTheFlyCreationFailure.new
end
end
end
- user.update_attribute(:last_login_on, Time.now) if user
+ user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
user
rescue => text
raise text
@@ -228,6 +223,10 @@ class User < ActiveRecord::Base
true
end
+ def anonymous?
+ !logged?
+ end
+
# Return user's role for project
def role_for_project(project)
# No role on archived projects
@@ -285,13 +284,12 @@ class User < ActiveRecord::Base
end
def self.anonymous
- return @anonymous_user if @anonymous_user
anonymous_user = AnonymousUser.find(:first)
if anonymous_user.nil?
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
end
- @anonymous_user = anonymous_user
+ anonymous_user
end
private
@@ -308,6 +306,10 @@ class AnonymousUser < User
errors.add_to_base 'An anonymous user already exists.' if AnonymousUser.find(:first)
end
+ def available_custom_fields
+ []
+ end
+
# Overrides a few properties
def logged?; false end
def admin; false end
diff --git a/groups/app/models/user_preference.rb b/groups/app/models/user_preference.rb
index 73e4a50c6..3daa7a740 100644
--- a/groups/app/models/user_preference.rb
+++ b/groups/app/models/user_preference.rb
@@ -42,8 +42,10 @@ class UserPreference < ActiveRecord::Base
if attribute_present? attr_name
super
else
- self.others ||= {}
- self.others.store attr_name, value
+ h = read_attribute(:others).dup || {}
+ h.update(attr_name => value)
+ write_attribute(:others, h)
+ value
end
end
diff --git a/groups/app/models/watcher.rb b/groups/app/models/watcher.rb
index cb6ff52ea..38110c584 100644
--- a/groups/app/models/watcher.rb
+++ b/groups/app/models/watcher.rb
@@ -19,5 +19,12 @@ class Watcher < ActiveRecord::Base
belongs_to :watchable, :polymorphic => true
belongs_to :user
+ validates_presence_of :user
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
+
+ protected
+
+ def validate
+ errors.add :user_id, :activerecord_error_invalid unless user.nil? || user.active?
+ end
end
diff --git a/groups/app/models/wiki.rb b/groups/app/models/wiki.rb
index b6d6a9b50..3432a2bc7 100644
--- a/groups/app/models/wiki.rb
+++ b/groups/app/models/wiki.rb
@@ -17,7 +17,7 @@
class Wiki < ActiveRecord::Base
belongs_to :project
- has_many :pages, :class_name => 'WikiPage', :dependent => :destroy
+ has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
validates_presence_of :start_page
diff --git a/groups/app/models/wiki_content.rb b/groups/app/models/wiki_content.rb
index 724354ad6..f2ee39c4d 100644
--- a/groups/app/models/wiki_content.rb
+++ b/groups/app/models/wiki_content.rb
@@ -35,6 +35,17 @@ class WikiContent < ActiveRecord::Base
:type => 'wiki-page',
:url => Proc.new {|o| {:controller => 'wiki', :id => o.page.wiki.project_id, :page => o.page.title, :version => o.version}}
+ acts_as_activity_provider :type => 'wiki_pages',
+ :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
+ :permission => :view_wiki_pages,
+ :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
+ "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
+ "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
+ "#{WikiContent.versioned_table_name}.id",
+ :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
+ "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
+ "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
+
def text=(plain)
case Setting.wiki_compression
when 'gzip'
diff --git a/groups/app/models/wiki_page.rb b/groups/app/models/wiki_page.rb
index 8ce71cb80..2416fab74 100644
--- a/groups/app/models/wiki_page.rb
+++ b/groups/app/models/wiki_page.rb
@@ -22,14 +22,15 @@ class WikiPage < ActiveRecord::Base
belongs_to :wiki
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
has_many :attachments, :as => :container, :dependent => :destroy
-
+ acts_as_tree :order => 'title'
+
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
:description => :text,
:datetime => :created_on,
:url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
acts_as_searchable :columns => ['title', 'text'],
- :include => [:wiki, :content],
+ :include => [{:wiki => :project}, :content],
:project_key => "#{Wiki.table_name}.project_id"
attr_accessor :redirect_existing_links
@@ -105,6 +106,29 @@ class WikiPage < ActiveRecord::Base
def text
content.text if content
end
+
+ # Returns true if usr is allowed to edit the page, otherwise false
+ def editable_by?(usr)
+ !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
+ end
+
+ def parent_title
+ @parent_title || (self.parent && self.parent.pretty_title)
+ end
+
+ def parent_title=(t)
+ @parent_title = t
+ parent_page = t.blank? ? nil : self.wiki.find_page(t)
+ self.parent = parent_page
+ end
+
+ protected
+
+ def validate
+ errors.add(:parent_title, :activerecord_error_invalid) if !@parent_title.blank? && parent.nil?
+ errors.add(:parent_title, :activerecord_error_circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
+ errors.add(:parent_title, :activerecord_error_not_same_project) if parent && (parent.wiki_id != wiki_id)
+ end
end
class WikiDiff
diff --git a/groups/app/views/account/login.rhtml b/groups/app/views/account/login.rhtml
index ea1a1cd44..d8c1f313f 100644
--- a/groups/app/views/account/login.rhtml
+++ b/groups/app/views/account/login.rhtml
@@ -1,5 +1,6 @@
<div id="login-form">
<% form_tag({:action=> "login"}) do %>
+<%= back_url_hidden_field_tag %>
<table>
<tr>
<td align="right"><label for="username"><%=l(:field_login)%>:</label></td>
diff --git a/groups/app/views/account/register.rhtml b/groups/app/views/account/register.rhtml
index 7cf4b6da3..755a7ad4b 100644
--- a/groups/app/views/account/register.rhtml
+++ b/groups/app/views/account/register.rhtml
@@ -5,8 +5,9 @@
<div class="box">
<!--[form:user]-->
+<% if @user.auth_source_id.nil? %>
<p><label for="user_login"><%=l(:field_login)%> <span class="required">*</span></label>
-<%= text_field 'user', 'login', :size => 25 %></p>
+<%= text_field 'user', 'login', :size => 25 %></p>
<p><label for="password"><%=l(:field_password)%> <span class="required">*</span></label>
<%= password_field_tag 'password', nil, :size => 25 %><br />
@@ -14,6 +15,7 @@
<p><label for="password_confirmation"><%=l(:field_password_confirmation)%> <span class="required">*</span></label>
<%= password_field_tag 'password_confirmation', nil, :size => 25 %></p>
+<% end %>
<p><label for="user_firstname"><%=l(:field_firstname)%> <span class="required">*</span></label>
<%= text_field 'user', 'firstname' %></p>
@@ -27,8 +29,8 @@
<p><label for="user_language"><%=l(:field_language)%></label>
<%= select("user", "language", lang_options_for_select) %></p>
-<% for @custom_value in @custom_values %>
- <p><%= custom_field_tag_with_label @custom_value %></p>
+<% @user.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :user, value %></p>
<% end %>
<!--[eoform:user]-->
</div>
diff --git a/groups/app/views/account/show.rhtml b/groups/app/views/account/show.rhtml
index 97212b377..1160a5d8c 100644
--- a/groups/app/views/account/show.rhtml
+++ b/groups/app/views/account/show.rhtml
@@ -1,7 +1,11 @@
+<div class="contextual">
+<%= link_to(l(:button_edit), {:controller => 'users', :action => 'edit', :id => @user}, :class => 'icon icon-edit') if User.current.admin? %>
+</div>
+
<h2><%=h @user.name %></h2>
<p>
-<%= mail_to @user.mail unless @user.pref.hide_mail %>
+<%= mail_to(h(@user.mail)) unless @user.pref.hide_mail %>
<ul>
<li><%=l(:label_registered_on)%>: <%= format_date(@user.created_on) %></li>
<% for custom_value in @custom_values %>
@@ -16,8 +20,8 @@
<h3><%=l(:label_project_plural)%></h3>
<ul>
<% for membership in @memberships %>
- <li><%= link_to membership.project.name, :controller => 'projects', :action => 'show', :id => membership.project %>
- (<%= membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
+ <li><%= link_to(h(membership.project.name), :controller => 'projects', :action => 'show', :id => membership.project) %>
+ (<%=h membership.role.name %>, <%= format_date(membership.created_on) %>)</li>
<% end %>
</ul>
<% end %>
diff --git a/groups/app/views/attachments/_links.rhtml b/groups/app/views/attachments/_links.rhtml
index 4d485548b..9aae909fe 100644
--- a/groups/app/views/attachments/_links.rhtml
+++ b/groups/app/views/attachments/_links.rhtml
@@ -1,6 +1,6 @@
<div class="attachments">
<% for attachment in attachments %>
-<p><%= link_to attachment.filename, {:controller => 'attachments', :action => 'download', :id => attachment }, :class => 'icon icon-attachment' -%>
+<p><%= link_to_attachment attachment, :class => 'icon icon-attachment' -%>
<%= h(" - #{attachment.description}") unless attachment.description.blank? %>
<span class="size">(<%= number_to_human_size attachment.filesize %>)</span>
<% if options[:delete_url] %>
diff --git a/groups/app/views/attachments/diff.rhtml b/groups/app/views/attachments/diff.rhtml
new file mode 100644
index 000000000..7b64dca17
--- /dev/null
+++ b/groups/app/views/attachments/diff.rhtml
@@ -0,0 +1,15 @@
+<h2><%=h @attachment.filename %></h2>
+
+<div class="attachments">
+<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
+ <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
+<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
+ <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
+
+</div>
+&nbsp;
+<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
+
+<% content_for :header_tags do -%>
+ <%= stylesheet_link_tag "scm" -%>
+<% end -%>
diff --git a/groups/app/views/attachments/file.rhtml b/groups/app/views/attachments/file.rhtml
new file mode 100644
index 000000000..468c6b666
--- /dev/null
+++ b/groups/app/views/attachments/file.rhtml
@@ -0,0 +1,15 @@
+<h2><%=h @attachment.filename %></h2>
+
+<div class="attachments">
+<p><%= h("#{@attachment.description} - ") unless @attachment.description.blank? %>
+ <span class="author"><%= @attachment.author %>, <%= format_time(@attachment.created_on) %></span></p>
+<p><%= link_to_attachment @attachment, :text => l(:button_download), :download => true -%>
+ <span class="size">(<%= number_to_human_size @attachment.filesize %>)</span></p>
+
+</div>
+&nbsp;
+<%= render :partial => 'common/file', :locals => {:content => @content, :filename => @attachment.filename} %>
+
+<% content_for :header_tags do -%>
+ <%= stylesheet_link_tag "scm" -%>
+<% end -%>
diff --git a/groups/app/views/auth_sources/_form.rhtml b/groups/app/views/auth_sources/_form.rhtml
index 3d148c11f..9ffffafc7 100644
--- a/groups/app/views/auth_sources/_form.rhtml
+++ b/groups/app/views/auth_sources/_form.rhtml
@@ -22,14 +22,12 @@
<p><label for="auth_source_base_dn"><%=l(:field_base_dn)%> <span class="required">*</span></label>
<%= text_field 'auth_source', 'base_dn', :size => 60 %></p>
-</div>
-<div class="box">
<p><label for="auth_source_onthefly_register"><%=l(:field_onthefly)%></label>
<%= check_box 'auth_source', 'onthefly_register' %></p>
+</div>
-<p>
-<fieldset><legend><%=l(:label_attribute_plural)%></legend>
+<fieldset class="box"><legend><%=l(:label_attribute_plural)%></legend>
<p><label for="auth_source_attr_login"><%=l(:field_login)%> <span class="required">*</span></label>
<%= text_field 'auth_source', 'attr_login', :size => 20 %></p>
@@ -42,7 +40,5 @@
<p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label>
<%= text_field 'auth_source', 'attr_mail', :size => 20 %></p>
</fieldset>
-</p>
-</div>
<!--[eoform:auth_source]-->
diff --git a/groups/app/views/boards/index.rhtml b/groups/app/views/boards/index.rhtml
index 8d4560653..655352a96 100644
--- a/groups/app/views/boards/index.rhtml
+++ b/groups/app/views/boards/index.rhtml
@@ -38,3 +38,5 @@
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:controller => 'projects', :action => 'activity', :id => @project, :format => 'atom', :show_messages => 1, :key => User.current.rss_key}) %>
<% end %>
+
+<% html_title l(:label_board_plural) %>
diff --git a/groups/app/views/boards/show.rhtml b/groups/app/views/boards/show.rhtml
index 26d17ae56..96818df34 100644
--- a/groups/app/views/boards/show.rhtml
+++ b/groups/app/views/boards/show.rhtml
@@ -33,7 +33,7 @@
<th><%= l(:field_subject) %></th>
<th><%= l(:field_author) %></th>
<%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
- <th><%= l(:label_reply_plural) %></th>
+ <%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %>
<%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
</tr></thead>
<tbody>
@@ -57,3 +57,5 @@
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
+
+<% html_title h(@board.name) %>
diff --git a/groups/app/views/common/_diff.rhtml b/groups/app/views/common/_diff.rhtml
new file mode 100644
index 000000000..0b28101b7
--- /dev/null
+++ b/groups/app/views/common/_diff.rhtml
@@ -0,0 +1,64 @@
+<% Redmine::UnifiedDiff.new(diff, diff_type).each do |table_file| -%>
+<div class="autoscroll">
+<% if diff_type == 'sbs' -%>
+<table class="filecontent CodeRay">
+<thead>
+<tr><th colspan="4" class="filename"><%= table_file.file_name %></th></tr>
+</thead>
+<tbody>
+<% prev_line_left, prev_line_right = nil, nil -%>
+<% table_file.keys.sort.each do |key| -%>
+<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
+<tr class="spacing">
+<th class="line-num">...</th><td></td><th class="line-num">...</th><td></td>
+<% end -%>
+<tr>
+ <th class="line-num"><%= table_file[key].nb_line_left %></th>
+ <td class="line-code <%= table_file[key].type_diff_left %>">
+ <pre><%=to_utf8 table_file[key].line_left %></pre>
+ </td>
+ <th class="line-num"><%= table_file[key].nb_line_right %></th>
+ <td class="line-code <%= table_file[key].type_diff_right %>">
+ <pre><%=to_utf8 table_file[key].line_right %></pre>
+ </td>
+</tr>
+<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%>
+<% end -%>
+</tbody>
+</table>
+
+<% else -%>
+<table class="filecontent CodeRay">
+<thead>
+<tr><th colspan="3" class="filename"><%= table_file.file_name %></th></tr>
+</thead>
+<tbody>
+<% prev_line_left, prev_line_right = nil, nil -%>
+<% table_file.keys.sort.each do |key, line| %>
+<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%>
+<tr class="spacing">
+<th class="line-num">...</th><th class="line-num">...</th><td></td>
+</tr>
+<% end -%>
+<tr>
+ <th class="line-num"><%= table_file[key].nb_line_left %></th>
+ <th class="line-num"><%= table_file[key].nb_line_right %></th>
+ <% if table_file[key].line_left.empty? -%>
+ <td class="line-code <%= table_file[key].type_diff_right %>">
+ <pre><%=to_utf8 table_file[key].line_right %></pre>
+ </td>
+ <% else -%>
+ <td class="line-code <%= table_file[key].type_diff_left %>">
+ <pre><%=to_utf8 table_file[key].line_left %></pre>
+ </td>
+ <% end -%>
+</tr>
+<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%>
+<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%>
+<% end -%>
+</tbody>
+</table>
+<% end -%>
+
+</div>
+<% end -%>
diff --git a/groups/app/views/common/_file.rhtml b/groups/app/views/common/_file.rhtml
new file mode 100644
index 000000000..43f5c6c4b
--- /dev/null
+++ b/groups/app/views/common/_file.rhtml
@@ -0,0 +1,11 @@
+<div class="autoscroll">
+<table class="filecontent CodeRay">
+<tbody>
+<% line_num = 1 %>
+<% syntax_highlight(filename, to_utf8(content)).each_line do |line| %>
+<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr>
+<% line_num += 1 %>
+<% end %>
+</tbody>
+</table>
+</div>
diff --git a/groups/app/views/common/_preview.rhtml b/groups/app/views/common/_preview.rhtml
index e3bfc3a25..fd95f1188 100644
--- a/groups/app/views/common/_preview.rhtml
+++ b/groups/app/views/common/_preview.rhtml
@@ -1,3 +1,3 @@
<fieldset class="preview"><legend><%= l(:label_preview) %></legend>
-<%= textilizable @text, :attachments => @attachements %>
+<%= textilizable @text, :attachments => @attachements, :object => @previewed %>
</fieldset>
diff --git a/groups/app/views/common/feed.atom.rxml b/groups/app/views/common/feed.atom.rxml
index b5cbeeed9..c1b88a28e 100644
--- a/groups/app/views/common/feed.atom.rxml
+++ b/groups/app/views/common/feed.atom.rxml
@@ -1,6 +1,6 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
- xml.title @title
+ xml.title truncate_single_line(@title, 100)
xml.link "rel" => "self", "href" => url_for(params.merge({:format => nil, :only_path => false}))
xml.link "rel" => "alternate", "href" => url_for(:controller => 'welcome', :only_path => false)
xml.id url_for(:controller => 'welcome', :only_path => false)
@@ -10,11 +10,15 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom" do
@items.each do |item|
xml.entry do
url = url_for(item.event_url(:only_path => false))
- xml.title truncate(item.event_title, 100)
+ if @project
+ xml.title truncate_single_line(item.event_title, 100)
+ else
+ xml.title truncate_single_line("#{item.project} - #{item.event_title}", 100)
+ end
xml.link "rel" => "alternate", "href" => url
xml.id url
xml.updated item.event_datetime.xmlschema
- author = item.event_author if item.respond_to?(:author)
+ author = item.event_author if item.respond_to?(:event_author)
xml.author do
xml.name(author)
xml.email(author.mail) if author.respond_to?(:mail) && !author.mail.blank?
diff --git a/groups/app/views/custom_fields/_form.rhtml b/groups/app/views/custom_fields/_form.rhtml
index b3731fac7..db87c9217 100644
--- a/groups/app/views/custom_fields/_form.rhtml
+++ b/groups/app/views/custom_fields/_form.rhtml
@@ -102,6 +102,9 @@ when "IssueCustomField" %>
<% else %>
<p><%= f.check_box :is_required %></p>
+<% when "TimeEntryCustomField" %>
+ <p><%= f.check_box :is_required %></p>
+
<% end %>
</div>
<%= javascript_tag "toggle_custom_field_format();" %>
diff --git a/groups/app/views/enumerations/destroy.rhtml b/groups/app/views/enumerations/destroy.rhtml
new file mode 100644
index 000000000..657df8322
--- /dev/null
+++ b/groups/app/views/enumerations/destroy.rhtml
@@ -0,0 +1,12 @@
+<h2><%= l(@enumeration.option_name) %>: <%=h @enumeration %></h2>
+
+<% form_tag({}) do %>
+<div class="box">
+<p><strong><%= l(:text_enumeration_destroy_question, @enumeration.objects_count) %></strong></p>
+<p><%= l(:text_enumeration_category_reassign_to) %>
+<%= select_tag 'reassign_to_id', ("<option>--- #{l(:actionview_instancetag_blank_option)} ---</option>" + options_from_collection_for_select(@enumerations, 'id', 'name')) %></p>
+</div>
+
+<%= submit_tag l(:button_apply) %>
+<%= link_to l(:button_cancel), :controller => 'enumerations', :action => 'index' %>
+<% end %>
diff --git a/groups/app/views/enumerations/list.rhtml b/groups/app/views/enumerations/list.rhtml
index 9de9bf37c..7f3886b44 100644
--- a/groups/app/views/enumerations/list.rhtml
+++ b/groups/app/views/enumerations/list.rhtml
@@ -1,14 +1,14 @@
<h2><%=l(:label_enumerations)%></h2>
-<% Enumeration::OPTIONS.each do |option, name| %>
-<h3><%= l(name) %></h3>
+<% Enumeration::OPTIONS.each do |option, params| %>
+<h3><%= l(params[:label]) %></h3>
<% enumerations = Enumeration.get_values(option) %>
<% if enumerations.any? %>
<table class="list">
<% enumerations.each do |enumeration| %>
<tr class="<%= cycle('odd', 'even') %>">
- <td><%= link_to enumeration.name, :action => 'edit', :id => enumeration %></td>
+ <td><%= link_to h(enumeration), :action => 'edit', :id => enumeration %></td>
<td style="width:15%;"><%= image_tag('true.png') if enumeration.is_default? %></td>
<td style="width:15%;">
<%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => enumeration, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
@@ -16,6 +16,9 @@
<%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => enumeration, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
<%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => enumeration, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
</td>
+ <td align="center" style="width:10%;">
+ <%= link_to l(:button_delete), { :action => 'destroy', :id => enumeration }, :method => :post, :confirm => l(:text_are_you_sure), :class => "icon icon-del" %>
+ </td>
</tr>
<% end %>
</table>
diff --git a/groups/app/views/issues/_edit.rhtml b/groups/app/views/issues/_edit.rhtml
index 2e00ab520..2c7a4286e 100644
--- a/groups/app/views/issues/_edit.rhtml
+++ b/groups/app/views/issues/_edit.rhtml
@@ -4,6 +4,7 @@
:class => nil,
:multipart => true} do |f| %>
<%= error_messages_for 'issue' %>
+ <%= error_messages_for 'time_entry' %>
<div class="box">
<% if @edit_allowed || !@allowed_statuses.empty? %>
<fieldset class="tabular"><legend><%= l(:label_change_properties) %>
@@ -21,9 +22,12 @@
<p><%= time_entry.text_field :hours, :size => 6, :label => :label_spent_time %> <%= l(:field_hours) %></p>
</div>
<div class="splitcontentright">
- <p><%= time_entry.text_field :comments, :size => 40 %></p>
- <p><%= time_entry.select :activity_id, (@activities.collect {|p| [p.name, p.id]}) %></p>
+ <p><%= time_entry.select :activity_id, activity_collection_for_select_options %></p>
</div>
+ <p><%= time_entry.text_field :comments, :size => 60 %></p>
+ <% @time_entry.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :time_entry, value %></p>
+ <% end %>
<% end %>
</fieldset>
<% end %>
diff --git a/groups/app/views/issues/_form.rhtml b/groups/app/views/issues/_form.rhtml
index 9bb74fd34..4eca3cb4a 100644
--- a/groups/app/views/issues/_form.rhtml
+++ b/groups/app/views/issues/_form.rhtml
@@ -42,7 +42,7 @@
</div>
<div style="clear:both;"> </div>
-<%= render :partial => 'form_custom_fields', :locals => {:values => @custom_values} %>
+<%= render :partial => 'form_custom_fields' %>
<% if @issue.new_record? %>
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p>
diff --git a/groups/app/views/issues/_form_custom_fields.rhtml b/groups/app/views/issues/_form_custom_fields.rhtml
index 1268bb1f9..752fb4d37 100644
--- a/groups/app/views/issues/_form_custom_fields.rhtml
+++ b/groups/app/views/issues/_form_custom_fields.rhtml
@@ -1,11 +1,12 @@
<div class="splitcontentleft">
-<% i = 1 %>
-<% for @custom_value in values %>
- <p><%= custom_field_tag_with_label @custom_value %></p>
- <% if i == values.size / 2 %>
+<% i = 0 %>
+<% split_on = @issue.custom_field_values.size / 2 %>
+<% @issue.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :issue, value %></p>
+<% if i == split_on -%>
</div><div class="splitcontentright">
- <% end %>
- <% i += 1 %>
-<% end %>
+<% end -%>
+<% i += 1 -%>
+<% end -%>
</div>
<div style="clear:both;"> </div>
diff --git a/groups/app/views/issues/_history.rhtml b/groups/app/views/issues/_history.rhtml
index f29a44daf..b8efdb400 100644
--- a/groups/app/views/issues/_history.rhtml
+++ b/groups/app/views/issues/_history.rhtml
@@ -1,3 +1,4 @@
+<% reply_links = authorize_for('issues', 'edit') -%>
<% for journal in journals %>
<div id="change-<%= journal.id %>" class="journal">
<h4><div style="float:right;"><%= link_to "##{journal.indice}", :anchor => "note-#{journal.indice}" %></div>
@@ -8,6 +9,6 @@
<li><%= show_detail(detail) %></li>
<% end %>
</ul>
- <%= render_notes(journal) unless journal.notes.blank? %>
+ <%= render_notes(journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
<% end %>
diff --git a/groups/app/views/issues/_list.rhtml b/groups/app/views/issues/_list.rhtml
index 000f79853..b42357894 100644
--- a/groups/app/views/issues/_list.rhtml
+++ b/groups/app/views/issues/_list.rhtml
@@ -1,7 +1,7 @@
<% form_tag({}) do -%>
<table class="list issues">
<thead><tr>
- <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(this.up("form")); return false;',
+ <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
</th>
<%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
diff --git a/groups/app/views/issues/_pdf.rfpdf b/groups/app/views/issues/_pdf.rfpdf
deleted file mode 100644
index 6830506f6..000000000
--- a/groups/app/views/issues/_pdf.rfpdf
+++ /dev/null
@@ -1,118 +0,0 @@
-<% pdf.SetFontStyle('B',11)
- pdf.Cell(190,10, "#{issue.project.name} - #{issue.tracker.name} # #{issue.id}: #{issue.subject}")
- pdf.Ln
-
- y0 = pdf.GetY
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_status) + ":","LT")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.status.name,"RT")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_priority) + ":","LT")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.priority.name,"RT")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_author) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, issue.author.name,"R")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_category) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, (issue.category ? issue.category.name : "-"),"R")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_created_on) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.created_on),"R")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, (issue.assigned_to ? issue.assigned_to.name : "-"),"R")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.updated_on),"RB")
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_due_date) + ":","LB")
- pdf.SetFontStyle('',9)
- pdf.Cell(60,5, format_date(issue.due_date),"RB")
- pdf.Ln
-
- for custom_value in issue.custom_values
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
- pdf.SetFontStyle('',9)
- pdf.MultiCell(155,5, (show_value custom_value),"R")
- end
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_subject) + ":","LTB")
- pdf.SetFontStyle('',9)
- pdf.Cell(155,5, issue.subject,"RTB")
- pdf.Ln
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(35,5, l(:field_description) + ":")
- pdf.SetFontStyle('',9)
- pdf.MultiCell(155,5, issue.description,"BR")
-
- pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
- pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
-
- pdf.Ln
-
- if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, issue.project)
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_associated_revisions), "B")
- pdf.Ln
- for changeset in @issue.changesets
- pdf.SetFontStyle('B',8)
- pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer)
- pdf.Ln
- unless changeset.comments.blank?
- pdf.SetFontStyle('',8)
- pdf.MultiCell(190,5, changeset.comments)
- end
- pdf.Ln
- end
- end
-
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_history), "B")
- pdf.Ln
- for journal in issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
- pdf.SetFontStyle('B',8)
- pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
- pdf.Ln
- pdf.SetFontStyle('I',8)
- for detail in journal.details
- pdf.Cell(190,5, "- " + show_detail(detail, true))
- pdf.Ln
- end
- if journal.notes?
- pdf.SetFontStyle('',8)
- pdf.MultiCell(190,5, journal.notes)
- end
- pdf.Ln
- end
-
- if issue.attachments.any?
- pdf.SetFontStyle('B',9)
- pdf.Cell(190,5, l(:label_attachment_plural), "B")
- pdf.Ln
- for attachment in issue.attachments
- pdf.SetFontStyle('',8)
- pdf.Cell(80,5, attachment.filename)
- pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
- pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
- pdf.Cell(65,5, attachment.author.name,0,0,"R")
- pdf.Ln
- end
- end
-%>
diff --git a/groups/app/views/issues/context_menu.rhtml b/groups/app/views/issues/context_menu.rhtml
index f42f254e8..671655db7 100644
--- a/groups/app/views/issues/context_menu.rhtml
+++ b/groups/app/views/issues/context_menu.rhtml
@@ -6,47 +6,83 @@
<a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
<ul>
<% @statuses.each do |s| -%>
- <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}},
+ <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'edit', :id => @issue, :issue => {:status_id => s}, :back_to => @back}, :method => :post,
:selected => (s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
<% end -%>
</ul>
</li>
+<% else %>
+ <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
+ :class => 'icon-edit', :disabled => !@can[:edit] %></li>
+<% end %>
+
<li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a>
<ul>
<% @priorities.each do |p| -%>
- <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[priority_id]' => p, :back_to => @back}, :method => :post,
- :selected => (p == @issue.priority), :disabled => !@can[:edit] %></li>
+ <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'priority_id' => p, :back_to => @back}, :method => :post,
+ :selected => (@issue && p == @issue.priority), :disabled => !@can[:edit] %></li>
+ <% end -%>
+ </ul>
+ </li>
+ <% unless @project.nil? || @project.versions.empty? -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
+ <ul>
+ <% @project.versions.sort.each do |v| -%>
+ <li><%= context_menu_link v.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => v, :back_to => @back}, :method => :post,
+ :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
<% end -%>
+ <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'fixed_version_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
</ul>
</li>
+ <% end %>
+ <% unless @assignables.nil? || @assignables.empty? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
<ul>
<% @assignables.each do |u| -%>
- <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => u, :back_to => @back}, :method => :post,
- :selected => (u == @issue.assigned_to), :disabled => !@can[:update] %></li>
+ <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => u, :back_to => @back}, :method => :post,
+ :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
<% end -%>
- <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[assigned_to_id]' => '', :back_to => @back}, :method => :post,
- :selected => @issue.assigned_to.nil?, :disabled => !@can[:update] %></li>
+ <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'assigned_to_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
</ul>
</li>
+ <% end %>
+ <% unless @project.nil? || @project.issue_categories.empty? -%>
+ <li class="folder">
+ <a href="#" class="submenu"><%= l(:field_category) %></a>
+ <ul>
+ <% @project.issue_categories.each do |u| -%>
+ <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => u, :back_to => @back}, :method => :post,
+ :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
+ <% end -%>
+ <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'category_id' => 'none', :back_to => @back}, :method => :post,
+ :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
+ </ul>
+ </li>
+ <% end -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
<ul>
<% (0..10).map{|x|x*10}.each do |p| -%>
- <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'edit', :id => @issue, 'issue[done_ratio]' => p, :back_to => @back}, :method => :post,
- :selected => (p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
+ <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
+ :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
<% end -%>
</ul>
</li>
+
+<% if !@issue.nil? %>
<li><%= context_menu_link l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
:class => 'icon-copy', :disabled => !@can[:copy] %></li>
-<% else -%>
- <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
- :class => 'icon-edit', :disabled => !@can[:edit] %></li>
-<% end -%>
-
+ <% if @can[:log_time] -%>
+ <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
+ :class => 'icon-time' %></li>
+ <% end %>
+<% end %>
+
<li><%= context_menu_link l(:button_move), {:controller => 'issues', :action => 'move', :ids => @issues.collect(&:id)},
:class => 'icon-move', :disabled => !@can[:move] %></li>
<li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
diff --git a/groups/app/views/issues/index.rhtml b/groups/app/views/issues/index.rhtml
index 027f3f006..973f3eb25 100644
--- a/groups/app/views/issues/index.rhtml
+++ b/groups/app/views/issues/index.rhtml
@@ -45,7 +45,7 @@
<p class="other-formats">
<%= l(:label_export_to) %>
-<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
+<span><%= link_to 'Atom', {:query_id => @query, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
<span><%= link_to 'CSV', {:format => 'csv'}, :class => 'csv' %></span>
<span><%= link_to 'PDF', {:format => 'pdf'}, :class => 'pdf' %></span>
</p>
diff --git a/groups/app/views/issues/show.rfpdf b/groups/app/views/issues/show.rfpdf
index 08f2cb92d..73d9d66b5 100644
--- a/groups/app/views/issues/show.rfpdf
+++ b/groups/app/views/issues/show.rfpdf
@@ -4,7 +4,123 @@
pdf.footer_date = format_date(Date.today)
pdf.AddPage
- render :partial => 'issues/pdf', :locals => { :pdf => pdf, :issue => @issue }
+ pdf.SetFontStyle('B',11)
+ pdf.Cell(190,10, "#{@issue.project} - #{@issue.tracker} # #{@issue.id}: #{@issue.subject}")
+ pdf.Ln
+
+ y0 = pdf.GetY
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_status) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.status.name,"RT")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_priority) + ":","LT")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.priority.name,"RT")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_author) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, @issue.author.name,"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_category) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, (@issue.category ? @issue.category.name : "-"),"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_created_on) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.created_on),"R")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_assigned_to) + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, (@issue.assigned_to ? @issue.assigned_to.name : "-"),"R")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_updated_on) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.updated_on),"RB")
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_due_date) + ":","LB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(60,5, format_date(@issue.due_date),"RB")
+ pdf.Ln
+
+ for custom_value in @issue.custom_values
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, custom_value.custom_field.name + ":","L")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, (show_value custom_value),"R")
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_subject) + ":","LTB")
+ pdf.SetFontStyle('',9)
+ pdf.Cell(155,5, @issue.subject,"RTB")
+ pdf.Ln
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(35,5, l(:field_description) + ":")
+ pdf.SetFontStyle('',9)
+ pdf.MultiCell(155,5, @issue.description,"BR")
+
+ pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
+ pdf.Line(pdf.GetX, pdf.GetY, 170, pdf.GetY)
+
+ pdf.Ln
+
+ if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @issue.project)
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_associated_revisions), "B")
+ pdf.Ln
+ for changeset in @issue.changesets
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(changeset.committed_on) + " - " + changeset.committer)
+ pdf.Ln
+ unless changeset.comments.blank?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, changeset.comments)
+ end
+ pdf.Ln
+ end
+ end
+
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_history), "B")
+ pdf.Ln
+ for journal in @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
+ pdf.SetFontStyle('B',8)
+ pdf.Cell(190,5, format_time(journal.created_on) + " - " + journal.user.name)
+ pdf.Ln
+ pdf.SetFontStyle('I',8)
+ for detail in journal.details
+ pdf.Cell(190,5, "- " + show_detail(detail, true))
+ pdf.Ln
+ end
+ if journal.notes?
+ pdf.SetFontStyle('',8)
+ pdf.MultiCell(190,5, journal.notes)
+ end
+ pdf.Ln
+ end
+
+ if @issue.attachments.any?
+ pdf.SetFontStyle('B',9)
+ pdf.Cell(190,5, l(:label_attachment_plural), "B")
+ pdf.Ln
+ for attachment in @issue.attachments
+ pdf.SetFontStyle('',8)
+ pdf.Cell(80,5, attachment.filename)
+ pdf.Cell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
+ pdf.Cell(25,5, format_date(attachment.created_on),0,0,"R")
+ pdf.Cell(65,5, attachment.author.name,0,0,"R")
+ pdf.Ln
+ end
+ end
%>
<%= pdf.Output %>
diff --git a/groups/app/views/issues/show.rhtml b/groups/app/views/issues/show.rhtml
index f788d0ec8..2dd1bacaa 100644
--- a/groups/app/views/issues/show.rhtml
+++ b/groups/app/views/issues/show.rhtml
@@ -1,5 +1,5 @@
<div class="contextual">
-<%= show_and_goto_link(l(:button_update), 'update', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if authorize_for('issues', 'edit') %>
+<%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
<%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time' %>
<%= watcher_tag(@issue, User.current) %>
<%= link_to_if_authorized l(:button_copy), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue }, :class => 'icon icon-copy' %>
@@ -18,34 +18,34 @@
<table width="100%">
<tr>
- <td style="width:15%"><b><%=l(:field_status)%> :</b></td><td style="width:35%"><%= @issue.status.name %></td>
- <td style="width:15%"><b><%=l(:field_start_date)%> :</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
+ <td style="width:15%"><b><%=l(:field_status)%>:</b></td><td style="width:35%"><%= @issue.status.name %></td>
+ <td style="width:15%"><b><%=l(:field_start_date)%>:</b></td><td style="width:35%"><%= format_date(@issue.start_date) %></td>
</tr>
<tr>
- <td><b><%=l(:field_priority)%> :</b></td><td><%= @issue.priority.name %></td>
- <td><b><%=l(:field_due_date)%> :</b></td><td><%= format_date(@issue.due_date) %></td>
+ <td><b><%=l(:field_priority)%>:</b></td><td><%= @issue.priority.name %></td>
+ <td><b><%=l(:field_due_date)%>:</b></td><td><%= format_date(@issue.due_date) %></td>
</tr>
<tr>
- <td><b><%=l(:field_assigned_to)%> :</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
- <td><b><%=l(:field_done_ratio)%> :</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
+ <td><b><%=l(:field_assigned_to)%>:</b></td><td><%= @issue.assigned_to ? link_to_user(@issue.assigned_to) : "-" %></td>
+ <td><b><%=l(:field_done_ratio)%>:</b></td><td><%= progress_bar @issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%" %></td>
</tr>
<tr>
- <td><b><%=l(:field_category)%> :</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
+ <td><b><%=l(:field_category)%>:</b></td><td><%=h @issue.category ? @issue.category.name : "-" %></td>
<% if User.current.allowed_to?(:view_time_entries, @project) %>
- <td><b><%=l(:label_spent_time)%> :</b></td>
+ <td><b><%=l(:label_spent_time)%>:</b></td>
<td><%= @issue.spent_hours > 0 ? (link_to lwr(:label_f_hour, @issue.spent_hours), {:controller => 'timelog', :action => 'details', :project_id => @project, :issue_id => @issue}, :class => 'icon icon-time') : "-" %></td>
<% end %>
</tr>
<tr>
- <td><b><%=l(:field_fixed_version)%> :</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
+ <td><b><%=l(:field_fixed_version)%>:</b></td><td><%= @issue.fixed_version ? link_to_version(@issue.fixed_version) : "-" %></td>
<% if @issue.estimated_hours %>
- <td><b><%=l(:field_estimated_hours)%> :</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
+ <td><b><%=l(:field_estimated_hours)%>:</b></td><td><%= lwr(:label_f_hour, @issue.estimated_hours) %></td>
<% end %>
</tr>
<tr>
-<% n = 0
-for custom_value in @custom_values %>
- <td valign="top"><b><%= custom_value.custom_field.name %> :</b></td><td valign="top"><%= simple_format(h(show_value(custom_value))) %></td>
+<% n = 0 -%>
+<% @issue.custom_values.each do |value| -%>
+ <td valign="top"><b><%=h value.custom_field.name %>:</b></td><td valign="top"><%= simple_format(h(show_value(value))) %></td>
<% n = n + 1
if (n > 1)
n = 0 %>
@@ -56,6 +56,10 @@ end %>
</table>
<hr />
+<div class="contextual">
+<%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'reply', :id => @issue} }, :class => 'icon icon-comment' %>
+</div>
+
<p><strong><%=l(:field_description)%></strong></p>
<div class="wiki">
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
@@ -72,6 +76,14 @@ end %>
</div>
<% end %>
+<% if User.current.allowed_to?(:add_issue_watchers, @project) ||
+ (@issue.watchers.any? && User.current.allowed_to?(:view_issue_watchers, @project)) %>
+<hr />
+<div id="watchers">
+<%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %>
+</div>
+<% end %>
+
</div>
<% if @issue.changesets.any? && User.current.allowed_to?(:view_changesets, @project) %>
@@ -110,4 +122,5 @@ end %>
<% content_for :header_tags do %>
<%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %>
+ <%= stylesheet_link_tag 'scm' %>
<% end %>
diff --git a/groups/app/views/layouts/base.rhtml b/groups/app/views/layouts/base.rhtml
index 0b9d31512..62d542b7b 100644
--- a/groups/app/views/layouts/base.rhtml
+++ b/groups/app/views/layouts/base.rhtml
@@ -36,7 +36,7 @@
<%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
</div>
- <h1><%= h(@project ? @project.name : Setting.app_title) %></h1>
+ <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
<div id="main-menu">
<%= render_main_menu(@project) %>
diff --git a/groups/app/views/mailer/layout.text.html.rhtml b/groups/app/views/mailer/layout.text.html.rhtml
index b78e92bdd..c95c94501 100644
--- a/groups/app/views/mailer/layout.text.html.rhtml
+++ b/groups/app/views/mailer/layout.text.html.rhtml
@@ -1,12 +1,32 @@
<html>
<head>
<style>
-body { font-family: Verdana, sans-serif; font-size: 0.8em; color:#484848; }
-body h1 { font-family: "Trebuchet MS", Verdana, sans-serif; font-size: 1.2em; margin: 0;}
-a, a:link, a:visited{ color: #2A5685; }
-a:hover, a:active{ color: #c61a1a; }
-hr { width: 100%; height: 1px; background: #ccc; border: 0; }
-.footer { font-size: 0.8em; font-style: italic; }
+body {
+ font-family: Verdana, sans-serif;
+ font-size: 0.8em;
+ color:#484848;
+}
+h1 {
+ font-family: "Trebuchet MS", Verdana, sans-serif;
+ font-size: 1.2em;
+ margin: 0px;
+}
+a, a:link, a:visited {
+ color: #2A5685;
+}
+a:hover, a:active {
+ color: #c61a1a;
+}
+hr {
+ width: 100%;
+ height: 1px;
+ background: #ccc;
+ border: 0;
+}
+.footer {
+ font-size: 0.8em;
+ font-style: italic;
+}
</style>
</head>
<body>
diff --git a/groups/app/views/mailer/lost_password.text.html.rhtml b/groups/app/views/mailer/lost_password.text.html.rhtml
index 26eacfa92..4dd570c94 100644
--- a/groups/app/views/mailer/lost_password.text.html.rhtml
+++ b/groups/app/views/mailer/lost_password.text.html.rhtml
@@ -1,2 +1,4 @@
<p><%= l(:mail_body_lost_password) %><br />
<%= auto_link(@url) %></p>
+
+<p><%= l(:field_login) %>: <b><%= @token.user.login %></b></p>
diff --git a/groups/app/views/mailer/lost_password.text.plain.rhtml b/groups/app/views/mailer/lost_password.text.plain.rhtml
index aec1b5b86..f5000ed7e 100644
--- a/groups/app/views/mailer/lost_password.text.plain.rhtml
+++ b/groups/app/views/mailer/lost_password.text.plain.rhtml
@@ -1,2 +1,4 @@
<%= l(:mail_body_lost_password) %>
<%= @url %>
+
+<%= l(:field_login) %>: <%= @token.user.login %>
diff --git a/groups/app/views/mailer/reminder.text.html.rhtml b/groups/app/views/mailer/reminder.text.html.rhtml
new file mode 100644
index 000000000..1e33fbe43
--- /dev/null
+++ b/groups/app/views/mailer/reminder.text.html.rhtml
@@ -0,0 +1,9 @@
+<p><%= l(:mail_body_reminder, @issues.size, @days) %></p>
+
+<ul>
+<% @issues.each do |issue| -%>
+ <li><%=h "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %></li>
+<% end -%>
+</ul>
+
+<p><%= link_to l(:label_issue_view_all), @issues_url %></p>
diff --git a/groups/app/views/mailer/reminder.text.plain.rhtml b/groups/app/views/mailer/reminder.text.plain.rhtml
new file mode 100644
index 000000000..7e6a2e585
--- /dev/null
+++ b/groups/app/views/mailer/reminder.text.plain.rhtml
@@ -0,0 +1,7 @@
+<%= l(:mail_body_reminder, @issues.size, @days) %>:
+
+<% @issues.each do |issue| -%>
+* <%= "#{issue.project} - #{issue.tracker} ##{issue.id}: #{issue.subject}" %>
+<% end -%>
+
+<%= @issues_url %>
diff --git a/groups/app/views/messages/show.rhtml b/groups/app/views/messages/show.rhtml
index 251b7c7a5..c24be7a21 100644
--- a/groups/app/views/messages/show.rhtml
+++ b/groups/app/views/messages/show.rhtml
@@ -2,6 +2,7 @@
link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
<div class="contextual">
+ <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
<%= link_to_if_authorized l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit' %>
<%= link_to_if_authorized l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del' %>
</div>
@@ -17,10 +18,12 @@
</div>
<br />
+<% unless @replies.empty? %>
<h3 class="icon22 icon22-comment"><%= l(:label_reply_plural) %></h3>
<% @replies.each do |message| %>
<a name="<%= "message-#{message.id}" %>"></a>
<div class="contextual">
+ <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
<%= link_to_if_authorized image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit) %>
<%= link_to_if_authorized image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete) %>
</div>
@@ -30,6 +33,7 @@
<%= link_to_attachments message.attachments, :no_author => true %>
</div>
<% end %>
+<% end %>
<% if !@topic.locked? && authorize_for('messages', 'reply') %>
<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
@@ -48,3 +52,9 @@
<div id="preview" class="wiki"></div>
</div>
<% end %>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
+
+<% html_title h(@topic.subject) %>
diff --git a/groups/app/views/my/blocks/_documents.rhtml b/groups/app/views/my/blocks/_documents.rhtml
index a34be936f..d222e4203 100644
--- a/groups/app/views/my/blocks/_documents.rhtml
+++ b/groups/app/views/my/blocks/_documents.rhtml
@@ -1,8 +1,9 @@
<h3><%=l(:label_document_plural)%></h3>
+<% project_ids = @user.projects.select {|p| @user.allowed_to?(:view_documents, p)}.collect(&:id) %>
<%= render(:partial => 'documents/document',
:collection => Document.find(:all,
:limit => 10,
:order => "#{Document.table_name}.created_on DESC",
- :conditions => "#{Document.table_name}.project_id in (#{@user.projects.collect{|m| m.id}.join(',')})",
- :include => [:project])) unless @user.projects.empty? %> \ No newline at end of file
+ :conditions => "#{Document.table_name}.project_id in (#{project_ids.join(',')})",
+ :include => [:project])) unless project_ids.empty? %> \ No newline at end of file
diff --git a/groups/app/views/news/edit.rhtml b/groups/app/views/news/edit.rhtml
index a7e5e6e36..4be566e0b 100644
--- a/groups/app/views/news/edit.rhtml
+++ b/groups/app/views/news/edit.rhtml
@@ -5,7 +5,7 @@
<%= render :partial => 'form', :locals => { :f => f } %>
<%= submit_tag l(:button_save) %>
<%= link_to_remote l(:label_preview),
- { :url => { :controller => 'news', :action => 'preview' },
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('news-form')"
diff --git a/groups/app/views/news/index.rhtml b/groups/app/views/news/index.rhtml
index 87db8a5f7..9cac39002 100644
--- a/groups/app/views/news/index.rhtml
+++ b/groups/app/views/news/index.rhtml
@@ -12,7 +12,7 @@
<%= render :partial => 'news/form', :locals => { :f => f } %>
<%= submit_tag l(:button_create) %>
<%= link_to_remote l(:label_preview),
- { :url => { :controller => 'news', :action => 'preview' },
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('news-form')"
diff --git a/groups/app/views/news/new.rhtml b/groups/app/views/news/new.rhtml
index 9208d8840..a4d29a0a9 100644
--- a/groups/app/views/news/new.rhtml
+++ b/groups/app/views/news/new.rhtml
@@ -5,7 +5,7 @@
<%= render :partial => 'news/form', :locals => { :f => f } %>
<%= submit_tag l(:button_create) %>
<%= link_to_remote l(:label_preview),
- { :url => { :controller => 'news', :action => 'preview' },
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('news-form')"
diff --git a/groups/app/views/news/show.rhtml b/groups/app/views/news/show.rhtml
index a55b56f0b..78be9c247 100644
--- a/groups/app/views/news/show.rhtml
+++ b/groups/app/views/news/show.rhtml
@@ -15,7 +15,7 @@
<%= render :partial => 'form', :locals => { :f => f } %>
<%= submit_tag l(:button_save) %>
<%= link_to_remote l(:label_preview),
- { :url => { :controller => 'news', :action => 'preview' },
+ { :url => { :controller => 'news', :action => 'preview', :project_id => @project },
:method => 'post',
:update => 'preview',
:with => "Form.serialize('news-form')"
@@ -55,3 +55,7 @@
<% end %>
<% html_title @news.title -%>
+
+<% content_for :header_tags do %>
+ <%= stylesheet_link_tag 'scm' %>
+<% end %>
diff --git a/groups/app/views/projects/_form.rhtml b/groups/app/views/projects/_form.rhtml
index 32e4dcd44..11f7e3933 100644
--- a/groups/app/views/projects/_form.rhtml
+++ b/groups/app/views/projects/_form.rhtml
@@ -13,12 +13,12 @@
<% unless @project.identifier_frozen? %>
<br /><em><%= l(:text_length_between, 3, 20) %> <%= l(:text_project_identifier_info) %></em>
<% end %></p>
-<p><%= f.text_field :homepage, :size => 40 %></p>
+<p><%= f.text_field :homepage, :size => 60 %></p>
<p><%= f.check_box :is_public %></p>
<%= wikitoolbar_for 'project_description' %>
-<% for @custom_value in @custom_values %>
- <p><%= custom_field_tag_with_label @custom_value %></p>
+<% @project.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :project, value %></p>
<% end %>
</div>
@@ -34,15 +34,15 @@
</fieldset>
<% end %>
-<% unless @custom_fields.empty? %>
+<% unless @issue_custom_fields.empty? %>
<fieldset class="box"><legend><%=l(:label_custom_field_plural)%></legend>
-<% for custom_field in @custom_fields %>
+<% @issue_custom_fields.each do |custom_field| %>
<label class="floating">
- <%= check_box_tag 'project[custom_field_ids][]', custom_field.id, ((@project.custom_fields.include? custom_field) or custom_field.is_for_all?), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
+ <%= check_box_tag 'project[issue_custom_field_ids][]', custom_field.id, (@project.all_issue_custom_fields.include? custom_field), (custom_field.is_for_all? ? {:disabled => "disabled"} : {}) %>
<%= custom_field.name %>
</label>
<% end %>
-<%= hidden_field_tag 'project[custom_field_ids][]', '' %>
+<%= hidden_field_tag 'project[issue_custom_field_ids][]', '' %>
</fieldset>
<% end %>
<!--[eoform:project]-->
diff --git a/groups/app/views/projects/activity.rhtml b/groups/app/views/projects/activity.rhtml
index c2f2f9ebd..fa25812ac 100644
--- a/groups/app/views/projects/activity.rhtml
+++ b/groups/app/views/projects/activity.rhtml
@@ -6,11 +6,11 @@
<h3><%= format_activity_day(day) %></h3>
<dl>
<% @events_by_day[day].sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%>
- <dt class="<%= e.event_type %>"><span class="time"><%= format_time(e.event_datetime, false) %></span>
- <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %> <%= link_to h(truncate(e.event_title, 100)), e.event_url %></dt>
- <dd><% unless e.event_description.blank? -%>
- <span class="description"><%= format_activity_description(e.event_description) %></span><br />
- <% end %>
+ <dt class="<%= e.event_type %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
+ <span class="time"><%= format_time(e.event_datetime, false) %></span>
+ <%= content_tag('span', h(e.project), :class => 'project') if @project.nil? || @project != e.project %>
+ <%= link_to format_activity_title(e.event_title), e.event_url %></dt>
+ <dd><span class="description"><%= format_activity_description(e.event_description) %></span>
<span class="author"><%= e.event_author if e.respond_to?(:event_author) %></span></dd>
<% end -%>
</dl>
@@ -44,8 +44,8 @@
<% content_for :sidebar do %>
<% form_tag({}, :method => :get) do %>
<h3><%= l(:label_activity) %></h3>
-<p><% @event_types.each do |t| %>
-<label><%= check_box_tag "show_#{t}", 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br />
+<p><% @activity.event_types.each do |t| %>
+<label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br />
<% end %></p>
<% if @project && @project.active_children.any? %>
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
diff --git a/groups/app/views/projects/calendar.rhtml b/groups/app/views/projects/calendar.rhtml
index 743721cb3..048d8a5df 100644
--- a/groups/app/views/projects/calendar.rhtml
+++ b/groups/app/views/projects/calendar.rhtml
@@ -23,7 +23,7 @@
<% content_for :sidebar do %>
<h3><%= l(:label_calendar) %></h3>
- <% form_tag() do %>
+ <% form_tag({}, :method => :get) do %>
<p><%= select_month(@month, :prefix => "month", :discard_type => true) %>
<%= select_year(@year, :prefix => "year", :discard_type => true) %></p>
diff --git a/groups/app/views/projects/gantt.rfpdf b/groups/app/views/projects/gantt.rfpdf
index a293906ba..e94fc5814 100644
--- a/groups/app/views/projects/gantt.rfpdf
+++ b/groups/app/views/projects/gantt.rfpdf
@@ -124,9 +124,9 @@ pdf.SetFontStyle('B',7)
if i.is_a? Issue
i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
- i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to )
+ i_end_date = (i.due_before <= @date_to ? i.due_before : @date_to )
- i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
diff --git a/groups/app/views/projects/gantt.rhtml b/groups/app/views/projects/gantt.rhtml
index d941d2777..b18bca34c 100644
--- a/groups/app/views/projects/gantt.rhtml
+++ b/groups/app/views/projects/gantt.rhtml
@@ -166,9 +166,9 @@ top = headers_height + 10
@events.each do |i|
if i.is_a? Issue
i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
- i_end_date = (i.due_date <= @date_to ? i.due_date : @date_to )
+ i_end_date = (i.due_before <= @date_to ? i.due_before : @date_to )
- i_done_date = i.start_date + ((i.due_date - i.start_date+1)*i.done_ratio/100).floor
+ i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
@@ -190,7 +190,6 @@ top = headers_height + 10
<%= i.status.name %>
<%= (i.done_ratio).to_i %>%
</div>
- <% # === tooltip === %>
<div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
<span class="tip">
<%= render_issue_tooltip i %>
@@ -235,7 +234,7 @@ if Date.today >= @date_from and Date.today <= @date_to %>
<% content_for :sidebar do %>
<h3><%= l(:label_gantt) %></h3>
- <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil)) do %>
+ <% form_tag(params.merge(:tracker_ids => nil, :with_subprojects => nil), :method => :get) do %>
<% @trackers.each do |tracker| %>
<label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s) %> <%= tracker.name %></label><br />
<% end %>
@@ -243,7 +242,7 @@ if Date.today >= @date_from and Date.today <= @date_to %>
<br /><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label>
<%= hidden_field_tag 'with_subprojects', 0 %>
<% end %>
- <p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
+ <p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
<% end %>
<% end %>
diff --git a/groups/app/views/projects/list.rhtml b/groups/app/views/projects/index.rhtml
index b8bb62ebb..4c68717f5 100644
--- a/groups/app/views/projects/list.rhtml
+++ b/groups/app/views/projects/index.rhtml
@@ -1,4 +1,5 @@
<div class="contextual">
+ <%= link_to(l(:label_project_new), {:controller => 'projects', :action => 'add'}, :class => 'icon icon-add') + ' |' if User.current.admin? %>
<%= link_to l(:label_issue_view_all), { :controller => 'issues' } %> |
<%= link_to l(:label_overall_activity), { :controller => 'projects', :action => 'activity' }%>
</div>
@@ -17,9 +18,14 @@
<% end %>
<% if User.current.logged? %>
-<div class="contextual">
+<p style="text-align:right;">
<span class="icon icon-fav"><%= l(:label_my_projects) %></span>
-</div>
+</p>
<% end %>
+<p class="other-formats">
+<%= l(:label_export_to) %>
+<span><%= link_to 'Atom', {:format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
+</p>
+
<% html_title(l(:label_project_plural)) -%>
diff --git a/groups/app/views/projects/list_files.rhtml b/groups/app/views/projects/list_files.rhtml
index f385229ae..79e41f16d 100644
--- a/groups/app/views/projects/list_files.rhtml
+++ b/groups/app/views/projects/list_files.rhtml
@@ -23,8 +23,7 @@
<% for file in version.attachments %>
<tr class="<%= cycle("odd", "even") %>">
<td></td>
- <td><%= link_to(file.filename, {:controller => 'versions', :action => 'download', :id => version, :attachment_id => file},
- :title => file.description) %></td>
+ <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
<td align="center"><%= format_time(file.created_on) %></td>
<td align="center"><%= number_to_human_size(file.filesize) %></td>
<td align="center"><%= file.downloads %></td>
diff --git a/groups/app/views/projects/roadmap.rhtml b/groups/app/views/projects/roadmap.rhtml
index d9329d109..0778d8138 100644
--- a/groups/app/views/projects/roadmap.rhtml
+++ b/groups/app/views/projects/roadmap.rhtml
@@ -20,7 +20,7 @@
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
<ul>
<%- issues.each do |issue| -%>
- <li class="issue <%= 'closed' if issue.closed? %>"><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
+ <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
<%- end -%>
</ul>
</fieldset>
@@ -30,7 +30,7 @@
<% end %>
<% content_for :sidebar do %>
-<% form_tag do %>
+<% form_tag({}, :method => :get) do %>
<h3><%= l(:label_roadmap) %></h3>
<% @trackers.each do |tracker| %>
<label><%= check_box_tag "tracker_ids[]", tracker.id, (@selected_tracker_ids.include? tracker.id.to_s), :id => nil %>
@@ -38,12 +38,12 @@
<% end %>
<br />
<label for="completed"><%= check_box_tag "completed", 1, params[:completed] %> <%= l(:label_show_completed_versions) %></label>
-<p><%= submit_tag l(:button_apply), :class => 'button-small' %></p>
+<p><%= submit_tag l(:button_apply), :class => 'button-small', :name => nil %></p>
<% end %>
<h3><%= l(:label_version_plural) %></h3>
<% @versions.each do |version| %>
-<%= link_to version.name, :anchor => version.name %><br />
+<%= link_to version.name, "##{version.name}" %><br />
<% end %>
<% end %>
diff --git a/groups/app/views/projects/settings/_repository.rhtml b/groups/app/views/projects/settings/_repository.rhtml
index 95830ab98..dcfabbbf0 100644
--- a/groups/app/views/projects/settings/_repository.rhtml
+++ b/groups/app/views/projects/settings/_repository.rhtml
@@ -17,5 +17,5 @@
:class => 'icon icon-del') if @repository && !@repository.new_record? %>
</div>
-<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save)) %>
+<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save), :disabled => @repository.nil?) %>
<% end %>
diff --git a/groups/app/views/projects/show.rhtml b/groups/app/views/projects/show.rhtml
index 66c4838d6..778c8c220 100644
--- a/groups/app/views/projects/show.rhtml
+++ b/groups/app/views/projects/show.rhtml
@@ -3,14 +3,14 @@
<div class="splitcontentleft">
<%= textilizable @project.description %>
<ul>
- <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link @project.homepage %></li><% end %>
+ <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)) %></li><% end %>
<% if @subprojects.any? %>
<li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
<% end %>
<% if @project.parent %>
<li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
<% end %>
- <% for custom_value in @custom_values %>
+ <% @project.custom_values.each do |custom_value| %>
<% if !custom_value.value.empty? %>
<li><%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %></li>
<% end %>
diff --git a/groups/app/views/queries/_filters.rhtml b/groups/app/views/queries/_filters.rhtml
index ec9d4fef6..c9d612364 100644
--- a/groups/app/views/queries/_filters.rhtml
+++ b/groups/app/views/queries/_filters.rhtml
@@ -78,7 +78,7 @@ function toggle_multi_select(field) {
<select <%= "multiple=true" if query.values_for(field) and query.values_for(field).length > 1 %> name="values[<%= field %>][]" id="values_<%= field %>" class="select-small" style="vertical-align: top;">
<%= options_for_select options[:values], query.values_for(field) %>
</select>
- <%= link_to_function image_tag('expand.png'), "toggle_multi_select('#{field}');" %>
+ <%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select('#{field}');", :style => "vertical-align: bottom;" %>
<% when :date, :date_past %>
<%= text_field_tag "values[#{field}][]", query.values_for(field), :id => "values_#{field}", :size => 3, :class => "select-small" %> <%= l(:label_day_plural) %>
<% when :string, :text %>
diff --git a/groups/app/views/repositories/_dir_list_content.rhtml b/groups/app/views/repositories/_dir_list_content.rhtml
index 3564e52ab..20473a264 100644
--- a/groups/app/views/repositories/_dir_list_content.rhtml
+++ b/groups/app/views/repositories/_dir_list_content.rhtml
@@ -1,32 +1,24 @@
<% @entries.each do |entry| %>
<% tr_id = Digest::MD5.hexdigest(entry.path)
depth = params[:depth].to_i %>
-<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry">
-<td class="filename">
-<%= if entry.is_dir?
- link_to_remote h(entry.name),
- {:url => {:action => 'browse', :id => @project, :path => entry.path, :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
+<tr id="<%= tr_id %>" class="<%= params[:parent_id] %> entry <%= entry.kind %>">
+<td style="padding-left: <%=18 * depth%>px;" class="filename">
+<% if entry.is_dir? %>
+<span class="expander" onclick="<%= remote_function :url => {:action => 'browse', :id => @project, :path => to_path_param(entry.path), :rev => @rev, :depth => (depth + 1), :parent_id => tr_id},
:update => { :success => tr_id },
:position => :after,
:success => "scmEntryLoaded('#{tr_id}')",
- :condition => "scmEntryClick('#{tr_id}')"
- },
- {:href => url_for({:action => 'browse', :id => @project, :path => entry.path, :rev => @rev}),
- :class => ('icon icon-folder'),
- :style => "margin-left: #{18 * depth}px;"
- }
-else
- link_to h(entry.name),
- {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => entry.path, :rev => @rev},
- :class => 'icon icon-file',
- :style => "margin-left: #{18 * depth}px;"
-end %>
+ :condition => "scmEntryClick('#{tr_id}')"%>">&nbsp</span>
+<% end %>
+<%= link_to h(entry.name),
+ {:action => (entry.is_dir? ? 'browse' : 'changes'), :id => @project, :path => to_path_param(entry.path), :rev => @rev},
+ :class => (entry.is_dir? ? 'icon icon-folder' : 'icon icon-file')%>
</td>
<td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
<td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
<td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
<td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
-<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
+<% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %>
<td class="comments"><%=h truncate(changeset.comments, 50) unless changeset.nil? %></td>
</tr>
<% end %>
diff --git a/groups/app/views/repositories/_navigation.rhtml b/groups/app/views/repositories/_navigation.rhtml
index b7ac989bc..25a15f496 100644
--- a/groups/app/views/repositories/_navigation.rhtml
+++ b/groups/app/views/repositories/_navigation.rhtml
@@ -10,10 +10,10 @@ dirs.each do |dir|
link_path << '/' unless link_path.empty?
link_path << "#{dir}"
%>
- / <%= link_to h(dir), :action => 'browse', :id => @project, :path => link_path, :rev => @rev %>
+ / <%= link_to h(dir), :action => 'browse', :id => @project, :path => to_path_param(link_path), :rev => @rev %>
<% end %>
<% if filename %>
- / <%= link_to h(filename), :action => 'changes', :id => @project, :path => "#{link_path}/#{filename}", :rev => @rev %>
+ / <%= link_to h(filename), :action => 'changes', :id => @project, :path => to_path_param("#{link_path}/#{filename}"), :rev => @rev %>
<% end %>
<%= "@ #{revision}" if revision %>
diff --git a/groups/app/views/repositories/_revisions.rhtml b/groups/app/views/repositories/_revisions.rhtml
index 1bcf0208c..a938fecb8 100644
--- a/groups/app/views/repositories/_revisions.rhtml
+++ b/groups/app/views/repositories/_revisions.rhtml
@@ -1,4 +1,4 @@
-<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => path}, :method => :get) do %>
+<% form_tag({:controller => 'repositories', :action => 'diff', :id => @project, :path => to_path_param(path)}, :method => :get) do %>
<table class="list changesets">
<thead><tr>
<th>#</th>
diff --git a/groups/app/views/repositories/browse.rhtml b/groups/app/views/repositories/browse.rhtml
index 868388f11..4029a77d2 100644
--- a/groups/app/views/repositories/browse.rhtml
+++ b/groups/app/views/repositories/browse.rhtml
@@ -7,6 +7,7 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'dir', :revision => @rev } %></h2>
<%= render :partial => 'dir_list' %>
+<%= render_properties(@properties) %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag "scm" %>
diff --git a/groups/app/views/repositories/changes.rhtml b/groups/app/views/repositories/changes.rhtml
index 2d7462b29..ca5c58328 100644
--- a/groups/app/views/repositories/changes.rhtml
+++ b/groups/app/views/repositories/changes.rhtml
@@ -1,18 +1,19 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => (@entry ? @entry.kind : nil), :revision => @rev } %></h2>
-<h3><%=h @entry.name %></h3>
-
<p>
<% if @repository.supports_cat? %>
- <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
+ <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
<% if @repository.supports_annotate? %>
- <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> |
+ <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => to_path_param(@path), :rev => @rev } %> |
<% end %>
-<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
+<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => to_path_param(@path), :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
</p>
-<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%>
+<%= render_properties(@properties) %>
+
+<%= render(:partial => 'revisions',
+ :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }) unless @changesets.empty? %>
<% html_title(l(:label_change_plural)) -%>
diff --git a/groups/app/views/repositories/diff.rhtml b/groups/app/views/repositories/diff.rhtml
index eaef1abf5..52a5d6057 100644
--- a/groups/app/views/repositories/diff.rhtml
+++ b/groups/app/views/repositories/diff.rhtml
@@ -11,82 +11,14 @@
<%= select_tag 'type', options_for_select([[l(:label_diff_inline), "inline"], [l(:label_diff_side_by_side), "sbs"]], @diff_type), :onchange => "if (this.value != '') {this.form.submit()}" %></p>
<% end %>
-<% cache(@cache_key) do %>
-<% @diff.each do |table_file| %>
-<div class="autoscroll">
-<% if @diff_type == 'sbs' %>
- <table class="filecontent CodeRay">
- <thead>
- <tr>
- <th colspan="4" class="filename">
- <%= table_file.file_name %>
- </th>
- </tr>
- <tr>
- <th colspan="2">@<%= format_revision @rev %></th>
- <th colspan="2">@<%= format_revision @rev_to %></th>
- </tr>
- </thead>
- <tbody>
- <% table_file.keys.sort.each do |key| %>
- <tr>
- <th class="line-num">
- <%= table_file[key].nb_line_left %>
- </th>
- <td class="line-code <%= table_file[key].type_diff_left %>">
- <pre><%=to_utf8 table_file[key].line_left %></pre>
- </td>
- <th class="line-num">
- <%= table_file[key].nb_line_right %>
- </th>
- <td class="line-code <%= table_file[key].type_diff_right %>">
- <pre><%=to_utf8 table_file[key].line_right %></pre>
- </td>
- </tr>
- <% end %>
- </tbody>
- </table>
+<% cache(@cache_key) do -%>
+<%= render :partial => 'common/diff', :locals => {:diff => @diff, :diff_type => @diff_type} %>
+<% end -%>
-<% else %>
- <table class="filecontent CodeRay">
- <thead>
- <tr>
- <th colspan="3" class="filename">
- <%= table_file.file_name %>
- </th>
- </tr>
- <tr>
- <th>@<%= format_revision @rev %></th>
- <th>@<%= format_revision @rev_to %></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <% table_file.keys.sort.each do |key, line| %>
- <tr>
- <th class="line-num">
- <%= table_file[key].nb_line_left %>
- </th>
- <th class="line-num">
- <%= table_file[key].nb_line_right %>
- </th>
- <% if table_file[key].line_left.empty? %>
- <td class="line-code <%= table_file[key].type_diff_right %>">
- <pre><%=to_utf8 table_file[key].line_right %></pre>
- </td>
- <% else %>
- <td class="line-code <%= table_file[key].type_diff_left %>">
- <pre><%=to_utf8 table_file[key].line_left %></pre>
- </td>
- <% end %>
- </tr>
- <% end %>
- </tbody>
- </table>
-<% end %>
-</div>
-<% end %>
-<% end %>
+<p class="other-formats">
+<%= l(:label_export_to) %>
+<span><%= link_to 'Unified diff', params.merge(:format => 'diff') %></span>
+</p>
<% html_title(with_leading_slash(@path), 'Diff') -%>
diff --git a/groups/app/views/repositories/entry.rhtml b/groups/app/views/repositories/entry.rhtml
index 309da76fc..8e1e1992c 100644
--- a/groups/app/views/repositories/entry.rhtml
+++ b/groups/app/views/repositories/entry.rhtml
@@ -1,16 +1,6 @@
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
-<div class="autoscroll">
-<table class="filecontent CodeRay">
-<tbody>
-<% line_num = 1 %>
-<% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %>
-<tr><th class="line-num" id="L<%= line_num %>"><%= line_num %></th><td class="line-code"><pre><%= line %></pre></td></tr>
-<% line_num += 1 %>
-<% end %>
-</tbody>
-</table>
-</div>
+<%= render :partial => 'common/file', :locals => {:filename => @path, :content => @content} %>
<% content_for :header_tags do %>
<%= stylesheet_link_tag "scm" %>
diff --git a/groups/app/views/repositories/revision.rhtml b/groups/app/views/repositories/revision.rhtml
index f1e176669..80ac3bd1a 100644
--- a/groups/app/views/repositories/revision.rhtml
+++ b/groups/app/views/repositories/revision.rhtml
@@ -13,9 +13,9 @@
<% end -%>
&#187;&nbsp;
- <% form_tag do %>
+ <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %>
<%= text_field_tag 'rev', @rev, :size => 5 %>
- <%= submit_tag 'OK' %>
+ <%= submit_tag 'OK', :name => nil %>
<% end %>
</div>
@@ -46,10 +46,16 @@
<tbody>
<% @changes.each do |change| %>
<tr class="<%= cycle 'odd', 'even' %>">
-<td><div class="square action_<%= change.action %>"></div> <%= change.path %> <%= "(#{change.revision})" unless change.revision.blank? %></td>
+<td><div class="square action_<%= change.action %>"></div>
+<% if change.action == "D" -%>
+ <%= change.path -%>
+<% else -%>
+ <%= link_to change.path, :action => 'entry', :id => @project, :path => to_path_param(change.relative_path), :rev => @changeset.revision -%>
+<% end -%>
+<%= "(#{change.revision})" unless change.revision.blank? %></td>
<td align="right">
<% if change.action == "M" %>
-<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => without_leading_slash(change.path), :rev => @changeset.revision %>
+<%= link_to l(:label_view_diff), :action => 'diff', :id => @project, :path => to_path_param(change.relative_path), :rev => @changeset.revision %>
<% end %>
</td>
</tr>
diff --git a/groups/app/views/repositories/show.rhtml b/groups/app/views/repositories/show.rhtml
index 469ac063e..9a73183e8 100644
--- a/groups/app/views/repositories/show.rhtml
+++ b/groups/app/views/repositories/show.rhtml
@@ -1,11 +1,16 @@
<div class="contextual">
<%= link_to l(:label_statistics), {:action => 'stats', :id => @project}, :class => 'icon icon-stats' %>
+
+<% if !@entries.nil? && authorize_for('repositories', 'browse') -%>
+<% form_tag(:action => 'browse', :id => @project) do -%>
+| <%= l(:label_revision) %>: <%= text_field_tag 'rev', @rev, :size => 5 %>
+<% end -%>
+<% end -%>
</div>
<h2><%= l(:label_repository) %> (<%= @repository.scm_name %>)</h2>
<% if !@entries.nil? && authorize_for('repositories', 'browse') %>
-<h3><%= l(:label_browse) %></h3>
<%= render :partial => 'dir_list' %>
<% end %>
diff --git a/groups/app/views/repositories/stats.rhtml b/groups/app/views/repositories/stats.rhtml
index 76ce892d5..1e617577e 100644
--- a/groups/app/views/repositories/stats.rhtml
+++ b/groups/app/views/repositories/stats.rhtml
@@ -1,13 +1,12 @@
<h2><%= l(:label_statistics) %></h2>
-<table width="100%">
-<tr><td>
-<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
-</td><td>
-<%= tag("embed", :width => 500, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
-</td></tr>
-</table>
-<br />
+<p>
+<%= tag("embed", :width => 800, :height => 300, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_month")) %>
+</p>
+<p>
+<%= tag("embed", :width => 800, :height => 400, :type => "image/svg+xml", :src => url_for(:controller => 'repositories', :action => 'graph', :id => @project, :graph => "commits_per_author")) %>
+</p>
+
<p><%= link_to l(:button_back), :action => 'show', :id => @project %></p>
<% html_title(l(:label_repository), l(:label_statistics)) -%>
diff --git a/groups/app/views/roles/_form.rhtml b/groups/app/views/roles/_form.rhtml
index 58dc2af41..4aad45471 100644
--- a/groups/app/views/roles/_form.rhtml
+++ b/groups/app/views/roles/_form.rhtml
@@ -12,7 +12,7 @@
<% end %>
<h3><%= l(:label_permissions) %></h3>
-<div class="box">
+<div class="box" id="permissions">
<% perms_by_module = @permissions.group_by {|p| p.project_module.to_s} %>
<% perms_by_module.keys.sort.each do |mod| %>
<fieldset><legend><%= mod.blank? ? l(:label_project) : mod.humanize %></legend>
@@ -24,6 +24,6 @@
<% end %>
</fieldset>
<% end %>
-<br /><%= check_all_links 'role_form' %>
+<br /><%= check_all_links 'permissions' %>
<%= hidden_field_tag 'role[permissions][]', '' %>
</div>
diff --git a/groups/app/views/roles/report.rhtml b/groups/app/views/roles/report.rhtml
index 98c3b651e..8e254379e 100644
--- a/groups/app/views/roles/report.rhtml
+++ b/groups/app/views/roles/report.rhtml
@@ -1,13 +1,17 @@
<h2><%=l(:label_permissions_report)%></h2>
<% form_tag({:action => 'report'}, :id => 'permissions_form') do %>
-<%= hidden_field_tag 'permissions[0]', '' %>
+<%= hidden_field_tag 'permissions[0]', '', :id => nil %>
<table class="list">
<thead>
<tr>
<th><%=l(:label_permissions)%></th>
<% @roles.each do |role| %>
- <th><%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %></th>
+ <th>
+ <%= content_tag(role.builtin? ? 'em' : 'span', h(role.name)) %>
+ <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('input.role-#{role.id}')",
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
+ </th>
<% end %>
</tr>
</thead>
@@ -18,12 +22,16 @@
<tr><%= content_tag('th', mod.humanize, :colspan => (@roles.size + 1), :align => 'left') %></tr>
<% end %>
<% perms_by_module[mod].each do |permission| %>
- <tr class="<%= cycle('odd', 'even') %>">
- <td><%= permission.name.to_s.humanize %></td>
+ <tr class="<%= cycle('odd', 'even') %> permission-<%= permission.name %>">
+ <td>
+ <%= link_to_function(image_tag('toggle_check.png'), "toggleCheckboxesBySelector('.permission-#{permission.name} input')",
+ :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}") %>
+ <%= permission.name.to_s.humanize %>
+ </td>
<% @roles.each do |role| %>
<td align="center">
<% if role.setable_permissions.include? permission %>
- <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name) %>
+ <%= check_box_tag "permissions[#{role.id}][]", permission.name, (role.permissions.include? permission.name), :id => nil, :class => "role-#{role.id}" %>
<% end %>
</td>
<% end %>
diff --git a/groups/app/views/search/index.rhtml b/groups/app/views/search/index.rhtml
index 29c604a21..cb5b70a4c 100644
--- a/groups/app/views/search/index.rhtml
+++ b/groups/app/views/search/index.rhtml
@@ -4,27 +4,33 @@
<% form_tag({}, :method => :get) do %>
<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
<%= javascript_tag "Field.focus('search-input')" %>
-
-<% @object_types.each do |t| %>
-<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
-<% end %>
-<br />
+<%= project_select_tag %>
<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
</p>
-<%= submit_tag l(:button_submit), :name => 'submit' %>
+<p>
+<% @object_types.each do |t| %>
+<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
+<% end %>
+</p>
+
+<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
<% end %>
</div>
<% if @results %>
- <h3><%= l(:label_result_plural) %></h3>
- <ul>
+ <div id="search-results-counts">
+ <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
+ </div>
+
+ <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
+ <dl id="search-results">
<% @results.each do |e| %>
- <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
- <%= highlight_tokens(e.event_description, @tokens) %><br />
- <span class="author"><%= format_time(e.event_datetime) %></span></p></li>
+ <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %></dt>
+ <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
+ <span class="author"><%= format_time(e.event_datetime) %></span></dd>
<% end %>
- </ul>
+ </dl>
<% end %>
<p><center>
diff --git a/groups/app/views/settings/_mail_handler.rhtml b/groups/app/views/settings/_mail_handler.rhtml
new file mode 100644
index 000000000..830b1ba4a
--- /dev/null
+++ b/groups/app/views/settings/_mail_handler.rhtml
@@ -0,0 +1,18 @@
+<% form_tag({:action => 'edit', :tab => 'mail_handler'}) do %>
+
+<div class="box tabular settings">
+<p><label><%= l(:setting_mail_handler_api_enabled) %></label>
+<%= check_box_tag 'settings[mail_handler_api_enabled]', 1, Setting.mail_handler_api_enabled?,
+ :onclick => "if (this.checked) { Form.Element.enable('settings_mail_handler_api_key'); } else { Form.Element.disable('settings_mail_handler_api_key'); }" %>
+<%= hidden_field_tag 'settings[mail_handler_api_enabled]', 0 %></p>
+
+<p><label><%= l(:setting_mail_handler_api_key) %></label>
+<%= text_field_tag 'settings[mail_handler_api_key]', Setting.mail_handler_api_key,
+ :size => 30,
+ :id => 'settings_mail_handler_api_key',
+ :disabled => !Setting.mail_handler_api_enabled? %>
+<%= link_to_function l(:label_generate_key), "if ($('settings_mail_handler_api_key').disabled == false) { $('settings_mail_handler_api_key').value = randomKey(20) }" %></p>
+</div>
+
+<%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/groups/app/views/settings/_notifications.rhtml b/groups/app/views/settings/_notifications.rhtml
index ac3213853..36701463a 100644
--- a/groups/app/views/settings/_notifications.rhtml
+++ b/groups/app/views/settings/_notifications.rhtml
@@ -1,3 +1,4 @@
+<% if @deliveries %>
<% form_tag({:action => 'edit', :tab => 'notifications'}) do %>
<div class="box tabular settings">
@@ -9,13 +10,13 @@
<%= hidden_field_tag 'settings[bcc_recipients]', 0 %></p>
</div>
-<fieldset class="box"><legend><%=l(:text_select_mail_notifications)%></legend>
+<fieldset class="box" id="notified_events"><legend><%=l(:text_select_mail_notifications)%></legend>
<% @notifiables.each do |notifiable| %>
<label><%= check_box_tag 'settings[notified_events][]', notifiable, Setting.notified_events.include?(notifiable) %>
<%= l_or_humanize(notifiable) %></label><br />
<% end %>
<%= hidden_field_tag 'settings[notified_events][]', '' %>
-<p><%= check_all_links('mail-options-form') %></p>
+<p><%= check_all_links('notified_events') %></p>
</fieldset>
<fieldset class="box"><legend><%= l(:setting_emails_footer) %></legend>
@@ -28,3 +29,8 @@
<%= submit_tag l(:button_save) %>
<% end %>
+<% else %>
+<div class="nodata">
+<%= simple_format(l(:text_email_delivery_not_configured)) %>
+</div>
+<% end %>
diff --git a/groups/app/views/settings/_repositories.rhtml b/groups/app/views/settings/_repositories.rhtml
index 59b3b51de..a8c924430 100644
--- a/groups/app/views/settings/_repositories.rhtml
+++ b/groups/app/views/settings/_repositories.rhtml
@@ -7,8 +7,18 @@
<p><label><%= l(:setting_sys_api_enabled) %></label>
<%= check_box_tag 'settings[sys_api_enabled]', 1, Setting.sys_api_enabled? %><%= hidden_field_tag 'settings[sys_api_enabled]', 0 %></p>
+<p><label><%= l(:setting_enabled_scm) %></label>
+<% REDMINE_SUPPORTED_SCM.each do |scm| -%>
+<%= check_box_tag 'settings[enabled_scm][]', scm, Setting.enabled_scm.include?(scm) %> <%= scm %>
+<% end -%>
+<%= hidden_field_tag 'settings[enabled_scm][]', '' %>
+</p>
+
<p><label><%= l(:setting_repositories_encodings) %></label>
<%= text_field_tag 'settings[repositories_encodings]', Setting.repositories_encodings, :size => 60 %><br /><em><%= l(:text_comma_separated) %></em></p>
+
+<p><label><%= l(:setting_commit_logs_encoding) %></label>
+<%= select_tag 'settings[commit_logs_encoding]', options_for_select(Setting::ENCODINGS, Setting.commit_logs_encoding) %></p>
</div>
<fieldset class="box tabular settings"><legend><%= l(:text_issues_ref_in_commit_messages) %></legend>
diff --git a/groups/app/views/timelog/_list.rhtml b/groups/app/views/timelog/_list.rhtml
index 189f4f5e8..8aebd75de 100644
--- a/groups/app/views/timelog/_list.rhtml
+++ b/groups/app/views/timelog/_list.rhtml
@@ -27,9 +27,9 @@
<td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
<td align="center">
<% if entry.editable_by?(User.current) -%>
- <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry},
+ <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
:title => l(:button_edit) %>
- <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry},
+ <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
:confirm => l(:text_are_you_sure),
:method => :post,
:title => l(:button_delete) %>
diff --git a/groups/app/views/timelog/_report_criteria.rhtml b/groups/app/views/timelog/_report_criteria.rhtml
index 94f3d20f9..c9a1cfb45 100644
--- a/groups/app/views/timelog/_report_criteria.rhtml
+++ b/groups/app/views/timelog/_report_criteria.rhtml
@@ -3,7 +3,7 @@
<% next if hours_for_value.empty? -%>
<tr class="<%= cycle('odd', 'even') %> <%= 'last-level' unless criterias.length > level+1 %>">
<%= '<td></td>' * level %>
-<td><%= format_criteria_value(criterias[level], value) %></td>
+<td><%= h(format_criteria_value(criterias[level], value)) %></td>
<%= '<td></td>' * (criterias.length - level - 1) -%>
<% total = 0 -%>
<% @periods.each do |period| -%>
diff --git a/groups/app/views/timelog/details.rhtml b/groups/app/views/timelog/details.rhtml
index f02da9959..f111cbfc0 100644
--- a/groups/app/views/timelog/details.rhtml
+++ b/groups/app/views/timelog/details.rhtml
@@ -24,8 +24,13 @@
<p class="other-formats">
<%= l(:label_export_to) %>
+<span><%= link_to 'Atom', {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :class => 'feed' %></span>
<span><%= link_to 'CSV', params.merge(:format => 'csv'), :class => 'csv' %></span>
</p>
<% end %>
<% html_title l(:label_spent_time), l(:label_details) %>
+
+<% content_for :header_tags do %>
+ <%= auto_discovery_link_tag(:atom, {:issue_id => @issue, :format => 'atom', :key => User.current.rss_key}, :title => l(:label_spent_time)) %>
+<% end %>
diff --git a/groups/app/views/timelog/edit.rhtml b/groups/app/views/timelog/edit.rhtml
index f9dae8a99..0dd3503ec 100644
--- a/groups/app/views/timelog/edit.rhtml
+++ b/groups/app/views/timelog/edit.rhtml
@@ -9,7 +9,10 @@
<p><%= f.text_field :spent_on, :size => 10, :required => true %><%= calendar_for('time_entry_spent_on') %></p>
<p><%= f.text_field :hours, :size => 6, :required => true %></p>
<p><%= f.text_field :comments, :size => 100 %></p>
-<p><%= f.select :activity_id, (@activities.collect {|p| [p.name, p.id]}), :required => true %></p>
+<p><%= f.select :activity_id, activity_collection_for_select_options, :required => true %></p>
+<% @time_entry.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :time_entry, value %></p>
+<% end %>
</div>
<%= submit_tag l(:button_save) %>
diff --git a/groups/app/views/users/_form.rhtml b/groups/app/views/users/_form.rhtml
index f2b330828..e305581fe 100644
--- a/groups/app/views/users/_form.rhtml
+++ b/groups/app/views/users/_form.rhtml
@@ -2,7 +2,6 @@
<!--[form:user]-->
<div class="box">
-<h3><%=l(:label_information_plural)%></h3>
<p><%= f.text_field :login, :required => true, :size => 25 %></p>
<p><%= f.text_field :firstname, :required => true %></p>
<p><%= f.text_field :lastname, :required => true %></p>
@@ -12,11 +11,11 @@
<% end -%>
<p><%= f.select :language, lang_options_for_select %></p>
-<% for @custom_value in @custom_values %>
- <p><%= custom_field_tag_with_label @custom_value %></p>
-<% end if @custom_values%>
+<% @user.custom_field_values.each do |value| %>
+ <p><%= custom_field_tag_with_label :user, value %></p>
+<% end %>
-<p><%= f.check_box :admin %></p>
+<p><%= f.check_box :admin, :disabled => (@user == User.current) %></p>
</div>
<div class="box">
diff --git a/groups/app/views/users/_general.rhtml b/groups/app/views/users/_general.rhtml
new file mode 100644
index 000000000..80615ff6c
--- /dev/null
+++ b/groups/app/views/users/_general.rhtml
@@ -0,0 +1,4 @@
+<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/groups/app/views/users/_memberships.rhtml b/groups/app/views/users/_memberships.rhtml
index 06d3f6029..94b49159e 100644
--- a/groups/app/views/users/_memberships.rhtml
+++ b/groups/app/views/users/_memberships.rhtml
@@ -1,33 +1,40 @@
-<div class="box" style="margin-top: 16px;">
-<h3><%= l(:label_project_plural) %></h3>
-
-<% @user.memberships.select {|m| m.inherited_from.nil? }.each do |membership| %>
-<% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }, :class => "tabular") do %>
-<p style="margin:0;padding-top:0;">
- <label><%= membership.project.name %></label>
- <select name="membership[role_id]">
- <%= options_from_collection_for_select @roles, "id", "name", membership.role_id %>
- </select>
- <%= submit_tag l(:button_change), :class => "button-small" %>
- <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %>
-</p>
-<% end %>
+<% if @memberships.any? %>
+<table class="list memberships">
+ <thead>
+ <th><%= l(:label_project) %></th>
+ <th><%= l(:label_role) %></th>
+ <th style="width:15%"></th>
+ </thead>
+ <tbody>
+ <% @memberships.each do |membership| %>
+ <% next if membership.new_record? %>
+ <tr class="<%= cycle 'odd', 'even' %>">
+ <td><%=h membership.project %></td>
+ <td align="center">
+ <% form_tag({ :action => 'edit_membership', :id => @user, :membership_id => membership }) do %>
+ <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name", membership.role_id) %>
+ <%= submit_tag l(:button_change), :class => "small" %>
+ <% end %>
+ </td>
+ <td align="center">
+ <%= link_to l(:button_delete), {:action => 'destroy_membership', :id => @user, :membership_id => membership }, :method => :post, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ </tbody>
+<% end; reset_cycle %>
+</table>
+<% else %>
+<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
-<% unless @projects.empty? || @roles.empty? %>
-<hr />
+<% if @projects.any? %>
<p>
<label><%=l(:label_project_new)%></label><br/>
<% form_tag({ :action => 'edit_membership', :id => @user }) do %>
-<select name="membership[project_id]">
-<%= options_from_collection_for_select @projects, "id", "name", @membership.project_id %>
-</select>
+<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %>
<%= l(:label_role) %>:
-<select name="membership[role_id]">
-<%= options_from_collection_for_select @roles, "id", "name", @membership.role_id %>
-</select>
+<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
<%= submit_tag l(:button_add) %>
<% end %>
</p>
<% end %>
-</div> \ No newline at end of file
diff --git a/groups/app/views/users/edit.rhtml b/groups/app/views/users/edit.rhtml
index 0da99d0d2..4714bcecb 100644
--- a/groups/app/views/users/edit.rhtml
+++ b/groups/app/views/users/edit.rhtml
@@ -1,8 +1,27 @@
-<h2><%=l(:label_user)%></h2>
+<div class="contextual">
+<%= change_status_link(@user) %>
+</div>
-<% labelled_tabular_form_for :user, @user, :url => { :action => "edit" } do |f| %>
-<%= render :partial => 'form', :locals => { :f => f } %>
-<%= submit_tag l(:button_save) %>
-<% end %>
+<h2><%=l(:label_user)%>: <%=h @user.login %></h2>
-<%= render :partial => 'memberships' %> \ No newline at end of file
+<% selected_tab = params[:tab] ? params[:tab].to_s : user_settings_tabs.first[:name] %>
+
+<div class="tabs">
+<ul>
+<% user_settings_tabs.each do |tab| -%>
+ <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
+ :id => "tab-#{tab[:name]}",
+ :class => (tab[:name] != selected_tab ? nil : 'selected'),
+ :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
+<% end -%>
+</ul>
+</div>
+
+<% user_settings_tabs.each do |tab| -%>
+<%= content_tag('div', render(:partial => tab[:partial]),
+ :id => "tab-content-#{tab[:name]}",
+ :style => (tab[:name] != selected_tab ? 'display:none' : nil),
+ :class => 'tab-content') %>
+<% end -%>
+
+<% html_title(l(:label_user), @user.login, l(:label_administration)) -%>
diff --git a/groups/app/views/users/list.rhtml b/groups/app/views/users/list.rhtml
index 47a629469..6e6861ea9 100644
--- a/groups/app/views/users/list.rhtml
+++ b/groups/app/views/users/list.rhtml
@@ -7,7 +7,7 @@
<% form_tag({}, :method => :get) do %>
<fieldset><legend><%= l(:label_filter_plural) %></legend>
<label><%= l(:field_status) %> :</label>
-<%= select_tag 'status', status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
+<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %>
</fieldset>
<% end %>
&nbsp;
@@ -27,10 +27,10 @@
<tbody>
<% for user in @users -%>
<tr class="user <%= cycle("odd", "even") %> <%= %w(anon active registered locked)[user.status] %>">
- <td class="username"><%= link_to user.login, :action => 'edit', :id => user %></td>
- <td class="firstname"><%=h user.firstname %></td>
- <td class="lastname"><%=h user.lastname %></td>
- <td class="email"><%=h user.mail %></td>
+ <td class="username"><%= link_to h(user.login), :action => 'edit', :id => user %></td>
+ <td class="firstname"><%= h(user.firstname) %></td>
+ <td class="lastname"><%= h(user.lastname) %></td>
+ <td class="email"><%= mail_to(h(user.mail)) %></td>
<td class="group"><%=h user.group %></td>
<td align="center"><%= image_tag('true.png') if user.admin? %></td>
<td class="created_on" align="center"><%= format_time(user.created_on) %></td>
diff --git a/groups/app/views/versions/show.rhtml b/groups/app/views/versions/show.rhtml
index 7f81cf503..7f9518af8 100644
--- a/groups/app/views/versions/show.rhtml
+++ b/groups/app/views/versions/show.rhtml
@@ -38,7 +38,7 @@
<fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
<ul>
<% issues.each do |issue| -%>
- <li class="issue <%= 'closed' if issue.closed? %>"><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
+ <li><%= link_to_issue(issue) %>: <%=h issue.subject %></li>
<% end -%>
</ul>
</fieldset>
diff --git a/groups/app/views/watchers/_watchers.rhtml b/groups/app/views/watchers/_watchers.rhtml
new file mode 100644
index 000000000..14bb5fc6b
--- /dev/null
+++ b/groups/app/views/watchers/_watchers.rhtml
@@ -0,0 +1,25 @@
+<div class="contextual">
+<%= link_to_remote l(:button_add),
+ :url => {:controller => 'watchers',
+ :action => 'new',
+ :object_type => watched.class.name.underscore,
+ :object_id => watched} if User.current.allowed_to?(:add_issue_watchers, @project) %>
+</div>
+
+<p><strong><%= l(:label_issue_watchers) %></strong></p>
+<%= watchers_list(watched) %>
+
+<% unless @watcher.nil? %>
+<% remote_form_for(:watcher, @watcher,
+ :url => {:controller => 'watchers',
+ :action => 'new',
+ :object_type => watched.class.name.underscore,
+ :object_id => watched},
+ :method => :post,
+ :html => {:id => 'new-watcher-form'}) do |f| %>
+<p><%= f.select :user_id, (watched.addable_watcher_users.collect {|m| [m.name, m.id]}), :prompt => true %>
+
+<%= submit_tag l(:button_add) %>
+<%= toggle_link l(:button_cancel), 'new-watcher-form'%></p>
+<% end %>
+<% end %>
diff --git a/groups/app/views/welcome/index.rhtml b/groups/app/views/welcome/index.rhtml
index 5da5a1ed3..855248c5e 100644
--- a/groups/app/views/welcome/index.rhtml
+++ b/groups/app/views/welcome/index.rhtml
@@ -12,17 +12,19 @@
</div>
<div class="splitcontentright">
+ <% if @projects.any? %>
<div class="box">
<h3 class="icon22 icon22-projects"><%=l(:label_project_latest)%></h3>
<ul>
<% for project in @projects %>
<li>
- <%= link_to project.name, :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>)
+ <%= link_to h(project.name), :controller => 'projects', :action => 'show', :id => project %> (<%= format_time(project.created_on) %>)
<%= textilizable project.short_description, :project => project %>
</li>
<% end %>
</ul>
- </div>
+ </div>
+ <% end %>
</div>
<% content_for :header_tags do %>
diff --git a/groups/app/views/wiki/export.rhtml b/groups/app/views/wiki/export.rhtml
index 1ab5c13e4..94b4e6f0d 100644
--- a/groups/app/views/wiki/export.rhtml
+++ b/groups/app/views/wiki/export.rhtml
@@ -6,6 +6,10 @@
<style>
body { font:80% Verdana,Tahoma,Arial,sans-serif; }
h1, h2, h3, h4 { font-family: Trebuchet MS,Georgia,"Times New Roman",serif; }
+ul.toc { padding: 4px; margin-left: 0; }
+ul.toc li { list-style-type:none; }
+ul.toc li.heading2 { margin-left: 1em; }
+ul.toc li.heading3 { margin-left: 2em; }
</style>
</head>
<body>
diff --git a/groups/app/views/wiki/history.rhtml b/groups/app/views/wiki/history.rhtml
index 6462e9fdd..7ce78a0f2 100644
--- a/groups/app/views/wiki/history.rhtml
+++ b/groups/app/views/wiki/history.rhtml
@@ -18,7 +18,7 @@
<% line_num = 1 %>
<% @versions.each do |ver| %>
<tr class="<%= cycle("odd", "even") %>">
- <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></th>
+ <td class="id"><%= link_to ver.version, :action => 'index', :page => @page.title, :version => ver.version %></td>
<td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < @versions.size) %></td>
<td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true || $('version_from').value > #{ver.version}) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
<td align="center"><%= format_time(ver.updated_on) %></td>
@@ -30,6 +30,6 @@
<% end %>
</tbody>
</table>
-<%= submit_tag l(:label_view_diff), :class => 'small' %>
+<%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
<span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
<% end %>
diff --git a/groups/app/views/wiki/rename.rhtml b/groups/app/views/wiki/rename.rhtml
index 0c069f43d..260f9af8b 100644
--- a/groups/app/views/wiki/rename.rhtml
+++ b/groups/app/views/wiki/rename.rhtml
@@ -4,8 +4,9 @@
<% labelled_tabular_form_for :wiki_page, @page, :url => { :action => 'rename' } do |f| %>
<div class="box">
-<p><%= f.text_field :title, :required => true, :size => 255 %></p>
+<p><%= f.text_field :title, :required => true, :size => 100 %></p>
<p><%= f.check_box :redirect_existing_links %></p>
+<p><%= f.text_field :parent_title, :size => 100 %></p>
</div>
<%= submit_tag l(:button_rename) %>
<% end %>
diff --git a/groups/app/views/wiki/show.rhtml b/groups/app/views/wiki/show.rhtml
index e4413d090..255b904f5 100644
--- a/groups/app/views/wiki/show.rhtml
+++ b/groups/app/views/wiki/show.rhtml
@@ -1,11 +1,17 @@
<div class="contextual">
+<% if @editable %>
<%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
+<%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
+<%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
<%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
<%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
<%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
+<% end %>
<%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
</div>
+<%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
+
<% if @content.version != @page.content.version %>
<p>
<%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
@@ -22,9 +28,9 @@
<%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
-<%= link_to_attachments @page.attachments, :delete_url => (authorize_for('wiki', 'destroy_attachment') ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %>
+<%= link_to_attachments @page.attachments, :delete_url => ((@editable && authorize_for('wiki', 'destroy_attachment')) ? {:controller => 'wiki', :action => 'destroy_attachment', :page => @page.title} : nil) %>
-<% if authorize_for('wiki', 'add_attachment') %>
+<% if @editable && authorize_for('wiki', 'add_attachment') %>
<p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
:id => 'attach_files_link' %></p>
<% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
diff --git a/groups/app/views/wiki/special_page_index.rhtml b/groups/app/views/wiki/special_page_index.rhtml
index f21cc3423..72b395ef7 100644
--- a/groups/app/views/wiki/special_page_index.rhtml
+++ b/groups/app/views/wiki/special_page_index.rhtml
@@ -4,11 +4,7 @@
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>
-<ul><% @pages.each do |page| %>
- <li><%= link_to page.pretty_title, {:action => 'index', :page => page.title},
- :title => l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) %>
- </li>
-<% end %></ul>
+<%= render_page_hierarchy(@pages_by_parent_id) %>
<% content_for :sidebar do %>
<%= render :partial => 'sidebar' %>
diff --git a/groups/config/boot.rb b/groups/config/boot.rb
index 9fcd50fe3..cd21fb9ea 100644
--- a/groups/config/boot.rb
+++ b/groups/config/boot.rb
@@ -1,19 +1,109 @@
-# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+# Don't change this file!
+# Configure your app in config/environment.rb and config/environments/*.rb
-unless defined?(RAILS_ROOT)
- root_path = File.join(File.dirname(__FILE__), '..')
- unless RUBY_PLATFORM =~ /mswin32/
- require 'pathname'
- root_path = Pathname.new(root_path).cleanpath(true).to_s
+RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
+
+module Rails
+ class << self
+ def boot!
+ unless booted?
+ preinitialize
+ pick_boot.run
+ end
+ end
+
+ def booted?
+ defined? Rails::Initializer
+ end
+
+ def pick_boot
+ (vendor_rails? ? VendorBoot : GemBoot).new
+ end
+
+ def vendor_rails?
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
+ end
+
+ def preinitialize
+ load(preinitializer_path) if File.exist?(preinitializer_path)
+ end
+
+ def preinitializer_path
+ "#{RAILS_ROOT}/config/preinitializer.rb"
+ end
end
- RAILS_ROOT = root_path
-end
-if File.directory?("#{RAILS_ROOT}/vendor/rails")
- require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
-else
- require 'rubygems'
- require 'initializer'
+ class Boot
+ def run
+ load_initializer
+ Rails::Initializer.run(:set_load_path)
+ end
+ end
+
+ class VendorBoot < Boot
+ def load_initializer
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
+ Rails::Initializer.run(:install_gem_spec_stubs)
+ end
+ end
+
+ class GemBoot < Boot
+ def load_initializer
+ self.class.load_rubygems
+ load_rails_gem
+ require 'initializer'
+ end
+
+ def load_rails_gem
+ if version = self.class.gem_version
+ gem 'rails', version
+ else
+ gem 'rails'
+ end
+ rescue Gem::LoadError => load_error
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
+ exit 1
+ end
+
+ class << self
+ def rubygems_version
+ Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion
+ end
+
+ def gem_version
+ if defined? RAILS_GEM_VERSION
+ RAILS_GEM_VERSION
+ elsif ENV.include?('RAILS_GEM_VERSION')
+ ENV['RAILS_GEM_VERSION']
+ else
+ parse_gem_version(read_environment_rb)
+ end
+ end
+
+ def load_rubygems
+ require 'rubygems'
+
+ unless rubygems_version >= '0.9.4'
+ $stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.)
+ exit 1
+ end
+
+ rescue LoadError
+ $stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org)
+ exit 1
+ end
+
+ def parse_gem_version(text)
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
+ end
+
+ private
+ def read_environment_rb
+ File.read("#{RAILS_ROOT}/config/environment.rb")
+ end
+ end
+ end
end
-Rails::Initializer.run(:set_load_path)
+# All that for this:
+Rails.boot!
diff --git a/groups/config/database.yml.example b/groups/config/database.yml.example
index f72844a07..1dc678131 100644
--- a/groups/config/database.yml.example
+++ b/groups/config/database.yml.example
@@ -12,6 +12,7 @@ production:
host: localhost
username: root
password:
+ encoding: utf8
development:
adapter: mysql
@@ -19,6 +20,7 @@ development:
host: localhost
username: root
password:
+ encoding: utf8
test:
adapter: mysql
@@ -26,6 +28,7 @@ test:
host: localhost
username: root
password:
+ encoding: utf8
test_pgsql:
adapter: postgresql
diff --git a/groups/config/email.yml.example b/groups/config/email.yml.example
new file mode 100644
index 000000000..685096da4
--- /dev/null
+++ b/groups/config/email.yml.example
@@ -0,0 +1,21 @@
+# Outgoing email settings
+
+production:
+ delivery_method: :smtp
+ smtp_settings:
+ address: smtp.example.net
+ port: 25
+ domain: example.net
+ authentication: :login
+ user_name: redmine@example.net
+ password: redmine
+
+development:
+ delivery_method: :smtp
+ smtp_settings:
+ address: 127.0.0.1
+ port: 25
+ domain: example.net
+ authentication: :login
+ user_name: redmine@example.net
+ password: redmine
diff --git a/groups/config/environment.rb b/groups/config/environment.rb
index 7878eca47..9a3bf4b1d 100644
--- a/groups/config/environment.rb
+++ b/groups/config/environment.rb
@@ -5,7 +5,7 @@
# ENV['RAILS_ENV'] ||= 'production'
# Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION
+RAILS_GEM_VERSION = '2.1.0' unless defined? RAILS_GEM_VERSION
# Bootstrap the Rails environment, frameworks, and default configuration
require File.join(File.dirname(__FILE__), 'boot')
@@ -31,7 +31,7 @@ Rails::Initializer.run do |config|
# config.log_level = :debug
# Use the database for sessions instead of the file system
- # (create the session table with 'rake create_sessions_table')
+ # (create the session table with 'rake db:sessions:create')
# config.action_controller.session_store = :active_record_store
config.action_controller.session_store = :PStore
@@ -49,54 +49,9 @@ Rails::Initializer.run do |config|
# Use Active Record's schema dumper instead of SQL when creating the test database
# (enables use of different database adapters for development and test environments)
# config.active_record.schema_format = :ruby
-
- # See Rails::Configuration for more options
- # SMTP server configuration
- config.action_mailer.smtp_settings = {
- :address => "127.0.0.1",
- :port => 25,
- :domain => "somenet.foo",
- :authentication => :login,
- :user_name => "redmine@somenet.foo",
- :password => "redmine",
- }
-
- config.action_mailer.perform_deliveries = true
-
- # 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
- config.action_mailer.delivery_method = :smtp
-
+ # Deliveries are disabled by default. Do NOT modify this section.
+ # Define your email configuration in email.yml instead.
+ # It will automatically turn deliveries on
+ config.action_mailer.perform_deliveries = false
end
-
-ActiveRecord::Errors.default_error_messages = {
- :inclusion => "activerecord_error_inclusion",
- :exclusion => "activerecord_error_exclusion",
- :invalid => "activerecord_error_invalid",
- :confirmation => "activerecord_error_confirmation",
- :accepted => "activerecord_error_accepted",
- :empty => "activerecord_error_empty",
- :blank => "activerecord_error_blank",
- :too_long => "activerecord_error_too_long",
- :too_short => "activerecord_error_too_short",
- :wrong_length => "activerecord_error_wrong_length",
- :taken => "activerecord_error_taken",
- :not_a_number => "activerecord_error_not_a_number"
-}
-
-ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
-
-Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
-Mime::Type.register 'application/pdf', :pdf
-
-GLoc.set_config :default_language => :en
-GLoc.clear_strings
-GLoc.set_kcode
-GLoc.load_localized_strings
-GLoc.set_config(:raise_string_not_found_errors => false)
-
-require 'redmine'
-
diff --git a/groups/config/environments/test.rb b/groups/config/environments/test.rb
index 9ba9ae0f8..7c821da07 100644
--- a/groups/config/environments/test.rb
+++ b/groups/config/environments/test.rb
@@ -13,4 +13,5 @@ config.whiny_nils = true
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
+config.action_mailer.perform_deliveries = true
config.action_mailer.delivery_method = :test
diff --git a/groups/config/environments/test_pgsql.rb b/groups/config/environments/test_pgsql.rb
index 35bb19bee..7c821da07 100644
--- a/groups/config/environments/test_pgsql.rb
+++ b/groups/config/environments/test_pgsql.rb
@@ -13,4 +13,5 @@ config.whiny_nils = true
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
-config.action_mailer.delivery_method = :test \ No newline at end of file
+config.action_mailer.perform_deliveries = true
+config.action_mailer.delivery_method = :test
diff --git a/groups/config/environments/test_sqlite3.rb b/groups/config/environments/test_sqlite3.rb
index 35bb19bee..7c821da07 100644
--- a/groups/config/environments/test_sqlite3.rb
+++ b/groups/config/environments/test_sqlite3.rb
@@ -13,4 +13,5 @@ config.whiny_nils = true
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
-config.action_mailer.delivery_method = :test \ No newline at end of file
+config.action_mailer.perform_deliveries = true
+config.action_mailer.delivery_method = :test
diff --git a/groups/config/initializers/10-patches.rb b/groups/config/initializers/10-patches.rb
new file mode 100644
index 000000000..fcc091997
--- /dev/null
+++ b/groups/config/initializers/10-patches.rb
@@ -0,0 +1,17 @@
+
+ActiveRecord::Errors.default_error_messages = {
+ :inclusion => "activerecord_error_inclusion",
+ :exclusion => "activerecord_error_exclusion",
+ :invalid => "activerecord_error_invalid",
+ :confirmation => "activerecord_error_confirmation",
+ :accepted => "activerecord_error_accepted",
+ :empty => "activerecord_error_empty",
+ :blank => "activerecord_error_blank",
+ :too_long => "activerecord_error_too_long",
+ :too_short => "activerecord_error_too_short",
+ :wrong_length => "activerecord_error_wrong_length",
+ :taken => "activerecord_error_taken",
+ :not_a_number => "activerecord_error_not_a_number"
+}
+
+ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| "#{html_tag}" }
diff --git a/groups/config/initializers/20-mime_types.rb b/groups/config/initializers/20-mime_types.rb
new file mode 100644
index 000000000..269742b16
--- /dev/null
+++ b/groups/config/initializers/20-mime_types.rb
@@ -0,0 +1,4 @@
+# Add new mime types for use in respond_to blocks:
+
+Mime::SET << Mime::CSV unless Mime::SET.include?(Mime::CSV)
+Mime::Type.register 'application/pdf', :pdf
diff --git a/groups/config/initializers/30-redmine.rb b/groups/config/initializers/30-redmine.rb
new file mode 100644
index 000000000..f2a9f6a30
--- /dev/null
+++ b/groups/config/initializers/30-redmine.rb
@@ -0,0 +1,7 @@
+GLoc.set_config :default_language => :en
+GLoc.clear_strings
+GLoc.set_kcode
+GLoc.load_localized_strings
+GLoc.set_config(:raise_string_not_found_errors => false)
+
+require 'redmine'
diff --git a/groups/config/initializers/40-email.rb b/groups/config/initializers/40-email.rb
new file mode 100644
index 000000000..5b388ec59
--- /dev/null
+++ b/groups/config/initializers/40-email.rb
@@ -0,0 +1,17 @@
+# Loads action_mailer settings from email.yml
+# and turns deliveries on if configuration file is found
+
+filename = File.join(File.dirname(__FILE__), '..', 'email.yml')
+if File.file?(filename)
+ mailconfig = YAML::load_file(filename)
+
+ if mailconfig.is_a?(Hash) && mailconfig.has_key?(Rails.env)
+ # Enable deliveries
+ ActionMailer::Base.perform_deliveries = true
+
+ mailconfig[Rails.env].each do |k, v|
+ v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
+ ActionMailer::Base.send("#{k}=", v)
+ end
+ end
+end
diff --git a/groups/config/routes.rb b/groups/config/routes.rb
index bc4247837..4213df915 100644
--- a/groups/config/routes.rb
+++ b/groups/config/routes.rb
@@ -31,8 +31,13 @@ ActionController::Routing::Routes.draw do |map|
omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
+ omap.repositories_revision 'repositories/revision/:id/:rev', :action => 'revision'
end
+ map.connect 'attachments/:id', :controller => 'attachments', :action => 'show', :id => /\d+/
+ map.connect 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/
+ map.connect 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/
+
# Allow downloading Web Service WSDL as a file with an extension
# instead of a file named 'wsdl'
map.connect ':controller/service.wsdl', :action => 'wsdl'
diff --git a/groups/config/settings.yml b/groups/config/settings.yml
index bb501823e..ac79edb8d 100644
--- a/groups/config/settings.yml
+++ b/groups/config/settings.yml
@@ -43,7 +43,7 @@ activity_days_default:
per_page_options:
default: '25,50,100'
mail_from:
- default: redmine@somenet.foo
+ default: redmine@example.net
bcc_recipients:
default: 1
text_formatting:
@@ -59,6 +59,15 @@ protocol:
feeds_limit:
format: int
default: 15
+enabled_scm:
+ serialized: true
+ default:
+ - Subversion
+ - Darcs
+ - Mercurial
+ - Cvs
+ - Bazaar
+ - Git
autofetch_changesets:
default: 1
sys_api_enabled:
@@ -92,6 +101,10 @@ notified_events:
default:
- issue_added
- issue_updated
+mail_handler_api_enabled:
+ default: 0
+mail_handler_api_key:
+ default:
issue_list_default_columns:
serialized: true
default:
@@ -109,6 +122,9 @@ default_projects_public:
# multiple values accepted, comma separated
repositories_encodings:
default: ''
+# encoding used to convert commit logs to UTF-8
+commit_logs_encoding:
+ default: 'UTF-8'
ui_theme:
default: ''
emails_footer:
diff --git a/groups/db/migrate/001_setup.rb b/groups/db/migrate/001_setup.rb
index 1160dd5ef..d49e0e444 100644
--- a/groups/db/migrate/001_setup.rb
+++ b/groups/db/migrate/001_setup.rb
@@ -16,7 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Setup < ActiveRecord::Migration
-
+
+ class User < ActiveRecord::Base; end
# model removed
class Permission < ActiveRecord::Base; end
@@ -284,13 +285,15 @@ class Setup < ActiveRecord::Migration
Permission.create :controller => "versions", :action => "destroy_file", :description => "button_delete", :sort => 1322
# create default administrator account
- user = User.create :firstname => "Redmine", :lastname => "Admin", :mail => "admin@somenet.foo", :mail_notification => true, :language => "en"
- user.login = "admin"
- user.password = "admin"
- user.admin = true
- user.save
-
-
+ user = User.create :login => "admin",
+ :hashed_password => "d033e22ae348aeb5660fc2140aec35850c4da997",
+ :admin => true,
+ :firstname => "Redmine",
+ :lastname => "Admin",
+ :mail => "admin@example.net",
+ :mail_notification => true,
+ :language => "en",
+ :status => 1
end
def self.down
diff --git a/groups/db/migrate/072_add_enumerations_position.rb b/groups/db/migrate/072_add_enumerations_position.rb
index e0beaf395..22558a6e9 100644
--- a/groups/db/migrate/072_add_enumerations_position.rb
+++ b/groups/db/migrate/072_add_enumerations_position.rb
@@ -1,7 +1,7 @@
class AddEnumerationsPosition < ActiveRecord::Migration
def self.up
add_column(:enumerations, :position, :integer, :default => 1) unless Enumeration.column_names.include?('position')
- Enumeration.find(:all).group_by(&:opt).each_value do |enums|
+ Enumeration.find(:all).group_by(&:opt).each do |opt, enums|
enums.each_with_index do |enum, i|
# do not call model callbacks
Enumeration.update_all "position = #{i+1}", {:id => enum.id}
diff --git a/groups/db/migrate/078_add_custom_fields_position.rb b/groups/db/migrate/078_add_custom_fields_position.rb
index 7ee8abb58..1c42ae732 100644
--- a/groups/db/migrate/078_add_custom_fields_position.rb
+++ b/groups/db/migrate/078_add_custom_fields_position.rb
@@ -1,7 +1,7 @@
class AddCustomFieldsPosition < ActiveRecord::Migration
def self.up
add_column(:custom_fields, :position, :integer, :default => 1)
- CustomField.find(:all).group_by(&:type).each_value do |fields|
+ CustomField.find(:all).group_by(&:type).each do |t, fields|
fields.each_with_index do |field, i|
# do not call model callbacks
CustomField.update_all "position = #{i+1}", {:id => field.id}
diff --git a/groups/db/migrate/096_add_wiki_pages_protected.rb b/groups/db/migrate/096_add_wiki_pages_protected.rb
new file mode 100644
index 000000000..49720fbb7
--- /dev/null
+++ b/groups/db/migrate/096_add_wiki_pages_protected.rb
@@ -0,0 +1,9 @@
+class AddWikiPagesProtected < ActiveRecord::Migration
+ def self.up
+ add_column :wiki_pages, :protected, :boolean, :default => false, :null => false
+ end
+
+ def self.down
+ remove_column :wiki_pages, :protected
+ end
+end
diff --git a/groups/db/migrate/097_change_projects_homepage_limit.rb b/groups/db/migrate/097_change_projects_homepage_limit.rb
new file mode 100644
index 000000000..98374aa4e
--- /dev/null
+++ b/groups/db/migrate/097_change_projects_homepage_limit.rb
@@ -0,0 +1,9 @@
+class ChangeProjectsHomepageLimit < ActiveRecord::Migration
+ def self.up
+ change_column :projects, :homepage, :string, :limit => nil, :default => ''
+ end
+
+ def self.down
+ change_column :projects, :homepage, :string, :limit => 60, :default => ''
+ end
+end
diff --git a/groups/db/migrate/098_add_wiki_pages_parent_id.rb b/groups/db/migrate/098_add_wiki_pages_parent_id.rb
new file mode 100644
index 000000000..36b922ec1
--- /dev/null
+++ b/groups/db/migrate/098_add_wiki_pages_parent_id.rb
@@ -0,0 +1,9 @@
+class AddWikiPagesParentId < ActiveRecord::Migration
+ def self.up
+ add_column :wiki_pages, :parent_id, :integer, :default => nil
+ end
+
+ def self.down
+ remove_column :wiki_pages, :parent_id
+ end
+end
diff --git a/groups/doc/CHANGELOG b/groups/doc/CHANGELOG
index b39185151..ac8cb6673 100644
--- a/groups/doc/CHANGELOG
+++ b/groups/doc/CHANGELOG
@@ -5,6 +5,86 @@ Copyright (C) 2006-2008 Jean-Philippe Lang
http://www.redmine.org/
+== 2008-07-06 v0.7.3
+
+* Allow dot in firstnames and lastnames
+* Add project name to cross-project Atom feeds
+* Encoding set to utf8 in example database.yml
+* HTML titles on forums related views
+* Fixed: various XSS vulnerabilities
+* Fixed: Entourage (and some old client) fails to correctly render notification styles
+* Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
+* Fixed: wrong relative paths to images in wiki_syntax.html
+
+
+== 2008-06-15 v0.7.2
+
+* "New Project" link on Projects page
+* Links to repository directories on the repo browser
+* Move status to front in Activity View
+* Remove edit step from Status context menu
+* Fixed: No way to do textile horizontal rule
+* Fixed: Repository: View differences doesn't work
+* Fixed: attachement's name maybe invalid.
+* Fixed: Error when creating a new issue
+* Fixed: NoMethodError on @available_filters.has_key?
+* Fixed: Check All / Uncheck All in Email Settings
+* Fixed: "View differences" of one file at /repositories/revision/ fails
+* Fixed: Column width in "my page"
+* Fixed: private subprojects are listed on Issues view
+* Fixed: Textile: bold, italics, underline, etc... not working after parentheses
+* Fixed: Update issue form: comment field from log time end out of screen
+* Fixed: Editing role: "issue can be assigned to this role" out of box
+* Fixed: Unable use angular braces after include word
+* Fixed: Using '*' as keyword for repository referencing keywords doesn't work
+* Fixed: Subversion repository "View differences" on each file rise ERROR
+* Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
+* Fixed: It is possible to lock out the last admin account
+* Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
+* Fixed: Issue number display clipped on 'my issues'
+* Fixed: Roadmap version list links not carrying state
+* Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
+* Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
+* Fixed: browser's language subcodes ignored
+* Fixed: Error on project selection with numeric (only) identifier.
+* Fixed: Link to PDF doesn't work after creating new issue
+* Fixed: "Replies" should not be shown on forum threads that are locked
+* Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
+* Fixed: http links containing hashes don't display correct
+* Fixed: Allow ampersands in Enumeration names
+* Fixed: Atom link on saved query does not include query_id
+* Fixed: Logtime info lost when there's an error updating an issue
+* Fixed: TOC does not parse colorization markups
+* Fixed: CVS: add support for modules names with spaces
+* Fixed: Bad rendering on projects/add
+* Fixed: exception when viewing differences on cvs
+* Fixed: export issue to pdf will messup when use Chinese language
+* Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
+* Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
+* Fixed: Importing from trac : some wiki links are messed
+* Fixed: Incorrect weekend definition in Hebrew calendar locale
+* Fixed: Atom feeds don't provide author section for repository revisions
+* Fixed: In Activity views, changesets titles can be multiline while they should not
+* Fixed: Ignore unreadable subversion directories (read disabled using authz)
+* Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
+* Fixed: Close statement handler in Redmine.pm
+
+
+== 2008-05-04 v0.7.1
+
+* Thai translation added (Gampol Thitinilnithi)
+* Translations updates
+* Escape HTML comment tags
+* Prevent "can't convert nil into String" error when :sort_order param is not present
+* Fixed: Updating tickets add a time log with zero hours
+* Fixed: private subprojects names are revealed on the project overview
+* Fixed: Search for target version of "none" fails with postgres 8.3
+* Fixed: Home, Logout, Login links shouldn't be absolute links
+* Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
+* Fixed: error when using upcase language name in coderay
+* Fixed: error on Trac import when :due attribute is nil
+
+
== 2008-04-28 v0.7.0
* Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
diff --git a/groups/doc/INSTALL b/groups/doc/INSTALL
index 7a00b9367..13502f8d0 100644
--- a/groups/doc/INSTALL
+++ b/groups/doc/INSTALL
@@ -1,25 +1,22 @@
== Redmine installation
Redmine - project management software
-Copyright (C) 2006-2007 Jean-Philippe Lang
+Copyright (C) 2006-2008 Jean-Philippe Lang
http://www.redmine.org/
== Requirements
-* Ruby on Rails 2.0.2
-* A database (see compatibility below)
+* Ruby on Rails 2.1
+* A database:
+ * MySQL (tested with MySQL 5)
+ * PostgreSQL (tested with PostgreSQL 8.1)
+ * SQLite (tested with SQLite 3)
Optional:
* SVN binaries >= 1.3 (needed for repository browsing, must be available in PATH)
* RMagick (gantt export to png)
-Supported databases:
-* MySQL (tested with MySQL 5)
-* PostgreSQL (tested with PostgreSQL 8.1)
-* SQLite (tested with SQLite 3)
-
-
== Installation
1. Uncompress the program archive
@@ -33,24 +30,33 @@ Supported databases:
rake db:migrate RAILS_ENV="production"
It will create tables and an administrator account.
-5. Test the installation by running WEBrick web server:
+5. Setting up permissions
+ The user who runs Redmine must have write permission on the following
+ subdirectories: files, log, tmp (create the last one if not present).
+
+ Assuming you run Redmine with a user named redmine:
+ mkdir tmp
+ sudo chown -R redmine:redmine files log tmp
+ sudo chmod -R 755 files log tmp
+
+6. Test the installation by running WEBrick web server:
ruby script/server -e production
Once WEBrick has started, point your browser to http://localhost:3000/
You should now see the application welcome page
-6. Use default administrator account to log in:
+7. Use default administrator account to log in:
login: admin
password: admin
-7. Go to "Administration" to load the default configuration data (roles,
+ Go to "Administration" to load the default configuration data (roles,
trackers, statuses, workflow) and adjust application settings
-== SMTP server Configuration
-
-In config/environment.rb, you can set parameters for your SMTP server:
-config.action_mailer.smtp_settings: SMTP server configuration
-config.action_mailer.perform_deliveries: set to false to disable mail delivering
+== Email delivery Configuration
+Copy config/email.yml.example to config/email.yml and edit this file
+to adjust your SMTP settings.
Don't forget to restart the application after any change to this file.
+
+Please do not enter your SMTP settings in environment.rb.
diff --git a/groups/doc/README_FOR_APP b/groups/doc/README_FOR_APP
new file mode 100644
index 000000000..fb70acaac
--- /dev/null
+++ b/groups/doc/README_FOR_APP
@@ -0,0 +1,5 @@
+= Redmine
+
+Redmine is a flexible project management web application written using Ruby on Rails framework.
+
+More details can be found at http://www.redmine.org
diff --git a/groups/doc/RUNNING_TESTS b/groups/doc/RUNNING_TESTS
index 7a5e2b992..6ee977811 100644
--- a/groups/doc/RUNNING_TESTS
+++ b/groups/doc/RUNNING_TESTS
@@ -24,6 +24,14 @@ Git
---
gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test
+Darcs (2.0+ required)
+---------------------
+gunzip < test/fixtures/repositories/darcs_repository.tar.gz | tar -xv -C tmp/test
+
+FileSystem
+----------
+gunzip < test/fixtures/repositories/filesystem_repository.tar.gz | tar -xv -C tmp/test
+
Running Tests
=============
diff --git a/groups/doc/UPGRADING b/groups/doc/UPGRADING
index 2edb2952a..1dd901171 100644
--- a/groups/doc/UPGRADING
+++ b/groups/doc/UPGRADING
@@ -10,15 +10,13 @@ http://www.redmine.org/
1. Uncompress the program archive in a new directory
3. Copy your database settings (RAILS_ROOT/config/database.yml)
+ and SMTP settings (RAILS_ROOT/config/email.yml)
into the new config directory
-4. Enter your SMTP settings in config/environment.rb
- Do not replace this file with the old one
-
-5. Migrate your database (please make a backup before doing this):
+4. Migrate your database (please make a backup before doing this):
rake db:migrate RAILS_ENV="production"
-6. Copy the RAILS_ROOT/files directory content into your new installation
+5. Copy the RAILS_ROOT/files directory content into your new installation
This directory contains all the attached files
diff --git a/groups/extra/mail_handler/rdm-mailhandler.rb b/groups/extra/mail_handler/rdm-mailhandler.rb
new file mode 100644
index 000000000..96e975187
--- /dev/null
+++ b/groups/extra/mail_handler/rdm-mailhandler.rb
@@ -0,0 +1,125 @@
+#!/usr/bin/ruby
+
+# rdm-mailhandler
+# Reads an email from standard input and forward it to a Redmine server
+# Can be used from a remote mail server
+
+require 'net/http'
+require 'net/https'
+require 'uri'
+require 'getoptlong'
+
+module Net
+ class HTTPS < HTTP
+ def self.post_form(url, params)
+ request = Post.new(url.path)
+ request.form_data = params
+ request.basic_auth url.user, url.password if url.user
+ http = new(url.host, url.port)
+ http.use_ssl = (url.scheme == 'https')
+ http.start {|h| h.request(request) }
+ end
+ end
+end
+
+class RedmineMailHandler
+ VERSION = '0.1'
+
+ attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key
+
+ def initialize
+ self.issue_attributes = {}
+
+ opts = GetoptLong.new(
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
+ [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
+ [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--category', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--priority', GetoptLong::REQUIRED_ARGUMENT],
+ [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT]
+ )
+
+ opts.each do |opt, arg|
+ case opt
+ when '--url'
+ self.url = arg.dup
+ when '--key'
+ self.key = arg.dup
+ when '--help'
+ usage
+ when '--verbose'
+ self.verbose = true
+ when '--version'
+ puts VERSION; exit
+ when '--project', '--tracker', '--category', '--priority'
+ self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
+ when '--allow-override'
+ self.allow_override = arg.dup
+ end
+ end
+
+ usage if url.nil?
+ end
+
+ def submit(email)
+ uri = url.gsub(%r{/*$}, '') + '/mail_handler'
+
+ data = { 'key' => key, 'email' => email, 'allow_override' => allow_override }
+ issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
+
+ debug "Posting to #{uri}..."
+ response = Net::HTTPS.post_form(URI.parse(uri), data)
+ debug "Response received: #{response.code}"
+ response.code == 201 ? 0 : 1
+ end
+
+ private
+
+ def usage
+ puts <<-USAGE
+Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
+Reads an email from standard input and forward it to a Redmine server
+
+Required:
+ -u, --url URL of the Redmine server
+ -k, --key Redmine API key
+
+General options:
+ -h, --help show this help
+ -v, --verbose show extra information
+ -V, --version show version information and exit
+
+Issue attributes control options:
+ -p, --project=PROJECT identifier of the target project
+ -t, --tracker=TRACKER name of the target tracker
+ --category=CATEGORY name of the target category
+ --priority=PRIORITY name of the target priority
+ -o, --allow-override=ATTRS allow email content to override attributes
+ specified by previous options
+ ATTRS is a comma separated list of attributes
+
+Examples:
+ # No project specified. Emails MUST contain the 'Project' keyword:
+ rdm-mailhandler --url http://redmine.domain.foo --key secret
+
+ # Fixed project and default tracker specified, but emails can override
+ # both tracker and priority attributes:
+ rdm-mailhandler --url https://domain.foo/redmine --key secret \\
+ --project foo \\
+ --tracker bug \\
+ --allow-override tracker,priority
+USAGE
+ exit
+ end
+
+ def debug(msg)
+ puts msg if verbose
+ end
+end
+
+handler = RedmineMailHandler.new
+handler.submit(STDIN.read)
diff --git a/groups/extra/sample_plugin/app/models/meeting.rb b/groups/extra/sample_plugin/app/models/meeting.rb
new file mode 100644
index 000000000..c1bb64a93
--- /dev/null
+++ b/groups/extra/sample_plugin/app/models/meeting.rb
@@ -0,0 +1,11 @@
+class Meeting < ActiveRecord::Base
+ belongs_to :project
+
+ acts_as_event :title => Proc.new {|o| "#{o.scheduled_on} Meeting"},
+ :datetime => :scheduled_on,
+ :author => nil,
+ :url => Proc.new {|o| {:controller => 'meetings', :action => 'show', :id => o.id}}
+
+ acts_as_activity_provider :timestamp => 'scheduled_on',
+ :find_options => { :include => :project }
+end
diff --git a/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb b/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb
new file mode 100644
index 000000000..fec9c8bd1
--- /dev/null
+++ b/groups/extra/sample_plugin/db/migrate/001_create_meetings.rb
@@ -0,0 +1,15 @@
+# Sample plugin migration
+# Use rake db:migrate_plugins to migrate installed plugins
+class CreateMeetings < ActiveRecord::Migration
+ def self.up
+ create_table :meetings do |t|
+ t.column :project_id, :integer, :null => false
+ t.column :description, :string
+ t.column :scheduled_on, :datetime
+ end
+ end
+
+ def self.down
+ drop_table :meetings
+ end
+end
diff --git a/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb b/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb
deleted file mode 100644
index 39d58a649..000000000
--- a/groups/extra/sample_plugin/db/migrate/001_create_some_models.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# Sample plugin migration
-# Use rake db:migrate_plugins to migrate installed plugins
-class CreateSomeModels < ActiveRecord::Migration
- def self.up
- create_table :example_plugin_model, :force => true do |t|
- t.column "example_attribute", :integer
- end
- end
-
- def self.down
- drop_table :example_plugin_model
- end
-end
diff --git a/groups/extra/sample_plugin/init.rb b/groups/extra/sample_plugin/init.rb
index 7389aaa6f..5c543338c 100644
--- a/groups/extra/sample_plugin/init.rb
+++ b/groups/extra/sample_plugin/init.rb
@@ -18,8 +18,13 @@ Redmine::Plugin.register :sample_plugin do
# This permission has to be explicitly given
# It will be listed on the permissions screen
permission :example_say_goodbye, {:example => [:say_goodbye]}
+ # This permission can be given to project members only
+ permission :view_meetings, {:meetings => [:index, :show]}, :require => :member
end
# A new item is added to the project menu
menu :project_menu, :sample_plugin, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
+
+ # Meetings are added to the activity view
+ activity_provider :meetings
end
diff --git a/groups/extra/sample_plugin/lang/en.yml b/groups/extra/sample_plugin/lang/en.yml
index bf62bc344..c4005a764 100644
--- a/groups/extra/sample_plugin/lang/en.yml
+++ b/groups/extra/sample_plugin/lang/en.yml
@@ -1,4 +1,5 @@
# Sample plugin
label_plugin_example: Sample Plugin
+label_meeting_plural: Meetings
text_say_hello: Plugin say 'Hello'
text_say_goodbye: Plugin say 'Good bye'
diff --git a/groups/extra/sample_plugin/lang/fr.yml b/groups/extra/sample_plugin/lang/fr.yml
index 2c0829c32..135050a5a 100644
--- a/groups/extra/sample_plugin/lang/fr.yml
+++ b/groups/extra/sample_plugin/lang/fr.yml
@@ -1,4 +1,5 @@
# Sample plugin
label_plugin_example: Plugin exemple
+label_meeting_plural: Meetings
text_say_hello: Plugin dit 'Bonjour'
text_say_goodbye: Plugin dit 'Au revoir'
diff --git a/groups/extra/svn/Redmine.pm b/groups/extra/svn/Redmine.pm
index 6f3ba4385..09a85fb09 100644
--- a/groups/extra/svn/Redmine.pm
+++ b/groups/extra/svn/Redmine.pm
@@ -36,10 +36,9 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
=head1 CONFIGURATION
- ## if the module isn't in your perl path
- PerlRequire /usr/local/apache/Redmine.pm
- ## else
- # PerlModule Apache::Authn::Redmine
+ ## This module has to be in your perl path
+ ## eg: /usr/lib/perl5/Apache/Authn/Redmine.pm
+ PerlLoadModule Apache::Authn::Redmine
<Location /svn>
DAV svn
SVNParentPath "/var/svn"
@@ -52,12 +51,17 @@ Authen::Simple::LDAP (and IO::Socket::SSL if LDAPS is used):
PerlAuthenHandler Apache::Authn::Redmine::authen_handler
## for mysql
- PerlSetVar dsn DBI:mysql:database=databasename;host=my.db.server
+ RedmineDSN "DBI:mysql:database=databasename;host=my.db.server"
## for postgres
- # PerlSetVar dsn DBI:Pg:dbname=databasename;host=my.db.server
-
- PerlSetVar db_user redmine
- PerlSetVar db_pass password
+ # RedmineDSN "DBI:Pg:dbname=databasename;host=my.db.server"
+
+ RedmineDbUser "redmine"
+ RedmineDbPass "password"
+ ## Optional where clause (fulltext search would be slow and
+ ## database dependant).
+ # RedmineDbWhereClause "and members.role_id IN (1,2)"
+ ## Optional credentials cache size
+ # RedmineCacheCredsMax 50
</Location>
To be able to browse repository inside redmine, you must add something
@@ -92,6 +96,7 @@ And you need to upgrade at least reposman.rb (after r860).
=cut
use strict;
+use warnings FATAL => 'all', NONFATAL => 'redefine';
use DBI;
use Digest::SHA1;
@@ -103,9 +108,87 @@ use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
-use Apache2::Const qw(:common);
+use Apache2::Const qw(:common :override :cmd_how);
+use APR::Pool ();
+use APR::Table ();
+
# use Apache2::Directive qw();
+my @directives = (
+ {
+ name => 'RedmineDSN',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"',
+ },
+ {
+ name => 'RedmineDbUser',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineDbPass',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineDbWhereClause',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ },
+ {
+ name => 'RedmineCacheCredsMax',
+ req_override => OR_AUTHCFG,
+ args_how => TAKE1,
+ errmsg => 'RedmineCacheCredsMax must be decimal number',
+ },
+);
+
+sub RedmineDSN {
+ my ($self, $parms, $arg) = @_;
+ $self->{RedmineDSN} = $arg;
+ my $query = "SELECT
+ hashed_password, auth_source_id
+ FROM members, projects, users
+ WHERE
+ projects.id=members.project_id
+ AND users.id=members.user_id
+ AND users.status=1
+ AND login=?
+ AND identifier=? ";
+ $self->{RedmineQuery} = trim($query);
+}
+sub RedmineDbUser { set_val('RedmineDbUser', @_); }
+sub RedmineDbPass { set_val('RedmineDbPass', @_); }
+sub RedmineDbWhereClause {
+ my ($self, $parms, $arg) = @_;
+ $self->{RedmineQuery} = trim($self->{RedmineQuery}.($arg ? $arg : "")." ");
+}
+
+sub RedmineCacheCredsMax {
+ my ($self, $parms, $arg) = @_;
+ if ($arg) {
+ $self->{RedmineCachePool} = APR::Pool->new;
+ $self->{RedmineCacheCreds} = APR::Table::make($self->{RedmineCachePool}, $arg);
+ $self->{RedmineCacheCredsCount} = 0;
+ $self->{RedmineCacheCredsMax} = $arg;
+ }
+}
+
+sub trim {
+ my $string = shift;
+ $string =~ s/\s{2,}/ /g;
+ return $string;
+}
+
+sub set_val {
+ my ($key, $self, $parms, $arg) = @_;
+ $self->{$key} = $arg;
+}
+
+Apache2::Module::add(__PACKAGE__, \@directives);
+
+
my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
sub access_handler {
@@ -117,7 +200,7 @@ sub access_handler {
}
my $method = $r->method;
- return OK unless 1 == $read_only_methods{$method};
+ return OK if defined $read_only_methods{$method};
my $project_id = get_project_identifier($r);
@@ -152,6 +235,7 @@ sub is_public_project {
$sth->execute($project_id);
my $ret = $sth->fetchrow_array ? 1 : 0;
+ $sth->finish();
$dbh->disconnect();
$ret;
@@ -182,9 +266,14 @@ sub is_member {
my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);
- my $sth = $dbh->prepare(
- "SELECT hashed_password, auth_source_id FROM members, projects, users WHERE projects.id=members.project_id AND users.id=members.user_id AND users.status=1 AND login=? AND identifier=?;"
- );
+ my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
+ my $usrprojpass;
+ if ($cfg->{RedmineCacheCredsMax}) {
+ $usrprojpass = $cfg->{RedmineCacheCreds}->get($redmine_user.":".$project_id);
+ return 1 if (defined $usrprojpass and ($usrprojpass eq $pass_digest));
+ }
+ my $query = $cfg->{RedmineQuery};
+ my $sth = $dbh->prepare($query);
$sth->execute($redmine_user, $project_id);
my $ret;
@@ -216,6 +305,20 @@ sub is_member {
$sth->finish();
$dbh->disconnect();
+ if ($cfg->{RedmineCacheCredsMax} and $ret) {
+ if (defined $usrprojpass) {
+ $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
+ } else {
+ if ($cfg->{RedmineCacheCredsCount} < $cfg->{RedmineCacheCredsMax}) {
+ $cfg->{RedmineCacheCreds}->set($redmine_user.":".$project_id, $pass_digest);
+ $cfg->{RedmineCacheCredsCount}++;
+ } else {
+ $cfg->{RedmineCacheCreds}->clear();
+ $cfg->{RedmineCacheCredsCount} = 0;
+ }
+ }
+ }
+
$ret;
}
@@ -229,9 +332,9 @@ sub get_project_identifier {
sub connect_database {
my $r = shift;
-
- my ($dsn, $db_user, $db_pass) = map { $r->dir_config($_) } qw/dsn db_user db_pass/;
- return DBI->connect($dsn, $db_user, $db_pass);
+
+ my $cfg = Apache2::Module::get_config(__PACKAGE__, $r->server, $r->per_dir_config);
+ return DBI->connect($cfg->{RedmineDSN}, $cfg->{RedmineDbUser}, $cfg->{RedmineDbPass});
}
1;
diff --git a/groups/lang/bg.yml b/groups/lang/bg.yml
index b341d989f..1f174e29f 100644
--- a/groups/lang/bg.yml
+++ b/groups/lang/bg.yml
@@ -48,6 +48,7 @@ general_text_no: 'не'
general_text_yes: 'да'
general_lang_name: 'Bulgarian'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: UTF-8
general_pdf_encoding: UTF-8
general_day_names: Понеделник,Вторник,СрÑда,Четвъртък,Петък,Събота,ÐеделÑ
@@ -618,3 +619,20 @@ setting_default_projects_public: Ðовите проекти Ñа публичн
error_scm_annotate: "Обектът не ÑъщеÑтвува или не може да бъде анотиран."
label_planning: Планиране
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/cs.yml b/groups/lang/cs.yml
index 250c602c2..609e95478 100644
--- a/groups/lang/cs.yml
+++ b/groups/lang/cs.yml
@@ -51,6 +51,7 @@ general_text_no: 'ne'
general_text_yes: 'ano'
general_lang_name: 'Čeština'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: UTF-8
general_pdf_encoding: UTF-8
general_day_names: Pondělí,Úterý,Středa,Čtvrtek,Pátek,Sobota,Neděle
@@ -623,3 +624,20 @@ enumeration_activities: Aktivity (sledování Äasu)
error_scm_annotate: "Položka neexistuje nebo nemůže být komentována."
label_planning: Plánování
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/da.yml b/groups/lang/da.yml
index ff2ed982d..a76e7ea5c 100644
--- a/groups/lang/da.yml
+++ b/groups/lang/da.yml
@@ -48,6 +48,7 @@ general_text_no: 'nej'
general_text_yes: 'ja'
general_lang_name: 'Danish (Dansk)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag
@@ -620,3 +621,20 @@ setting_default_projects_public: Nye projekter er offentlige som default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planlægning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/de.yml b/groups/lang/de.yml
index 77184cf88..e309dfb57 100644
--- a/groups/lang/de.yml
+++ b/groups/lang/de.yml
@@ -48,6 +48,7 @@ general_text_no: 'nein'
general_text_yes: 'ja'
general_lang_name: 'Deutsch'
general_csv_separator: ';'
+general_csv_decimal_separator: ','
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Montag,Dienstag,Mittwoch,Donnerstag,Freitag,Samstag,Sonntag
@@ -619,3 +620,20 @@ enumeration_issue_priorities: Ticket-Prioritäten
enumeration_doc_categories: Dokumentenkategorien
enumeration_activities: Aktivitäten (Zeiterfassung)
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/en.yml b/groups/lang/en.yml
index 320501c2b..7763b44b5 100644
--- a/groups/lang/en.yml
+++ b/groups/lang/en.yml
@@ -48,6 +48,7 @@ general_text_no: 'no'
general_text_yes: 'yes'
general_lang_name: 'English'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
@@ -91,6 +92,8 @@ mail_body_account_information_external: You can use your "%s" account to log in.
mail_body_account_information: Your account information
mail_subject_account_activation_request: %s account activation request
mail_body_account_activation_request: 'A new user (%s) has registered. His account is pending your approval:'
+mail_subject_reminder: "%d issue(s) due in the next days"
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
gui_validation_error: 1 error
gui_validation_error_plural: %d errors
@@ -180,6 +183,7 @@ field_searchable: Searchable
field_default_value: Default value
field_comments_sorting: Display comments
field_group: Group
+field_parent_title: Parent page
setting_app_title: Application title
setting_app_subtitle: Application subtitle
@@ -206,12 +210,16 @@ setting_time_format: Time format
setting_cross_project_issue_relations: Allow cross-project issue relations
setting_issue_list_default_columns: Default columns displayed on the issue list
setting_repositories_encodings: Repositories encodings
+setting_commit_logs_encoding: Commit messages encoding
setting_emails_footer: Emails footer
setting_protocol: Protocol
setting_per_page_options: Objects per page options
setting_user_format: Users display format
setting_activity_days_default: Days displayed on project activity
setting_display_subprojects_issues: Display subprojects issues on main projects by default
+setting_enabled_scm: Enabled SCM
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
project_module_issue_tracking: Issue tracking
project_module_time_tracking: Time tracking
@@ -292,6 +300,7 @@ label_auth_source: Authentication mode
label_auth_source_new: New authentication mode
label_auth_source_plural: Authentication modes
label_subproject_plural: Subprojects
+label_and_its_subprojects: %s and its subprojects
label_min_max_length: Min - Max length
label_list: List
label_date: Date
@@ -446,6 +455,7 @@ label_relation_new: New relation
label_relation_delete: Delete relation
label_relates_to: related to
label_duplicates: duplicates
+label_duplicated_by: duplicated by
label_blocks: blocks
label_blocked_by: blocked by
label_precedes: precedes
@@ -514,6 +524,9 @@ label_planning: Planning
label_group: Group
label_group_plural: Groups
label_group_new: New group
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+label_issue_watchers: Watchers
button_login: Login
button_submit: Submit
@@ -552,6 +565,7 @@ button_copy: Copy
button_annotate: Annotate
button_update: Update
button_configure: Configure
+button_quote: Quote
status_active: active
status_registered: registered
@@ -570,7 +584,7 @@ text_journal_deleted: deleted
text_tip_task_begin_day: task beginning this day
text_tip_task_end_day: task ending this day
text_tip_task_begin_end_day: task beginning and ending this day
-text_project_identifier_info: 'Lower case letters (a-z), numbers and dashes allowed.<br />Once saved, the identifier can not be changed.'
+text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier can not be changed.'
text_caracters_maximum: %d characters maximum.
text_caracters_minimum: Must be at least %d characters long.
text_length_between: Length between %d and %d characters.
@@ -597,6 +611,10 @@ text_destroy_time_entries_question: %.02f hours were reported on the issues you
text_destroy_time_entries: Delete reported hours
text_assign_time_entries_to_project: Assign reported hours to the project
text_reassign_time_entries: 'Reassign reported hours to this issue:'
+text_user_wrote: '%s wrote:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
default_role_manager: Manager
default_role_developper: Developer
diff --git a/groups/lang/es.yml b/groups/lang/es.yml
index c6eef021a..fc9540a02 100644
--- a/groups/lang/es.yml
+++ b/groups/lang/es.yml
@@ -48,6 +48,7 @@ general_text_no: 'no'
general_text_yes: 'sí'
general_lang_name: 'Español'
general_csv_separator: ';'
+general_csv_decimal_separator: ','
general_csv_encoding: ISO-8859-15
general_pdf_encoding: ISO-8859-15
general_day_names: Lunes,Martes,Miércoles,Jueves,Viernes,Sábado,Domingo
@@ -621,3 +622,20 @@ setting_default_projects_public: Los proyectos nuevos son públicos por defecto
error_scm_annotate: "No existe la entrada o no ha podido ser anotada"
label_planning: Planificación
text_subprojects_destroy_warning: 'Sus subprojectos: %s también se eliminarán'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/fi.yml b/groups/lang/fi.yml
index 68b6c20d7..6eb16bfac 100644
--- a/groups/lang/fi.yml
+++ b/groups/lang/fi.yml
@@ -16,7 +16,7 @@ actionview_datehelper_time_in_words_minute_less_than: vähemmän kuin minuuttia
actionview_datehelper_time_in_words_minute_plural: %d minuuttia
actionview_datehelper_time_in_words_minute_single: 1 minuutti
actionview_datehelper_time_in_words_second_less_than: vähemmän kuin sekuntin
-actionview_datehelper_time_in_words_second_less_than_plural: vähemmän kuin %d sekunttia
+actionview_datehelper_time_in_words_second_less_than_plural: vähemmän kuin %d sekuntia
actionview_instancetag_blank_option: Valitse, ole hyvä
activerecord_error_inclusion: ei ole listalla
@@ -34,7 +34,7 @@ activerecord_error_not_a_number: ei ole numero
activerecord_error_not_a_date: ei ole oikea päivä
activerecord_error_greater_than_start_date: tulee olla aloituspäivän jälkeinen
activerecord_error_not_same_project: ei kuulu samaan projektiin
-activerecord_error_circular_dependency: Tämä suhde loisi kiertävän suhteen.
+activerecord_error_circular_dependency: Tämä suhde loisi kehän.
general_fmt_age: %d v.
general_fmt_age_plural: %d vuotta
@@ -48,19 +48,20 @@ general_text_no: 'ei'
general_text_yes: 'kyllä'
general_lang_name: 'Finnish (Suomi)'
general_csv_separator: ','
-general_csv_encoding: ISO-8859-1
-general_pdf_encoding: ISO-8859-1
+general_csv_decimal_separator: '.'
+general_csv_encoding: ISO-8859-15
+general_pdf_encoding: ISO-8859-15
general_day_names: Maanantai,Tiistai,Keskiviikko,Torstai,Perjantai,Lauantai,Sunnuntai
general_first_day_of_week: '1'
notice_account_updated: Tilin päivitys onnistui.
-notice_account_invalid_creditentials: Väärä käyttäjä tai salasana
+notice_account_invalid_creditentials: Virheellinen käyttäjätunnus tai salasana
notice_account_password_updated: Salasanan päivitys onnistui.
notice_account_wrong_password: Väärä salasana
notice_account_register_done: Tilin luonti onnistui. Aktivoidaksesi tilin seuraa linkkiä joka välitettiin sähköpostiisi.
notice_account_unknown_email: Tuntematon käyttäjä.
-notice_can_t_change_password: Tämä tili käyttää ulkoista autentikointi järjestelmää. Mahdotonta muuttaa salasanaa.
-notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje miten vaihdat salasanasi.
+notice_can_t_change_password: Tämä tili käyttää ulkoista tunnistautumisjärjestelmää. Salasanaa ei voi muuttaa.
+notice_account_lost_email_sent: Sinulle on lähetetty sähköposti jossa on ohje kuinka vaihdat salasanasi.
notice_account_activated: Tilisi on nyt aktivoitu, voit kirjautua sisälle.
notice_successful_create: Luonti onnistui.
notice_successful_update: Päivitys onnistui.
@@ -71,20 +72,20 @@ notice_locking_conflict: Toinen käyttäjä on päivittänyt tiedot.
notice_not_authorized: Sinulla ei ole oikeutta näyttää tätä sivua.
notice_email_sent: Sähköposti on lähetty osoitteeseen %s
notice_email_error: Sähköpostilähetyksessä tapahtui virhe (%s)
-notice_feeds_access_key_reseted: RSS pääsy avaimesi on nollaantunut.
+notice_feeds_access_key_reseted: RSS salasana on nollaantunut.
notice_failed_to_save_issues: "%d Tapahtum(an/ien) tallennus epäonnistui %d valitut: %s."
notice_no_issue_selected: "Tapahtumia ei ole valittu! Valitse tapahtumat joita haluat muokata."
notice_account_pending: "Tilisi on luotu ja odottaa ylläpitäjän hyväksyntää."
-notice_default_data_loaded: Vakio asetusten palautus onnistui.
+notice_default_data_loaded: Vakioasetusten palautus onnistui.
-error_can_t_load_default_data: "Vakio asetuksia ei voitu ladata: %s"
-error_scm_not_found: "Syötettä ja/tai versiota ei löydy säiliöstä."
-error_scm_command_failed: "Säiliöön pääsyssä tapahtui virhe: %s"
+error_can_t_load_default_data: "Vakioasetuksia ei voitu ladata: %s"
+error_scm_not_found: "Syötettä ja/tai versiota ei löydy tietovarastosta."
+error_scm_command_failed: "Tietovarastoon pääsyssä tapahtui virhe: %s"
mail_subject_lost_password: Sinun %s salasanasi
-mail_body_lost_password: 'Vaihtaaksesi salasanasi, paina seuraavaa linkkiä:'
+mail_body_lost_password: 'Vaihtaaksesi salasanasi, napsauta seuraavaa linkkiä:'
mail_subject_register: %s tilin aktivointi
-mail_body_register: 'Aktivoidaksesi tilisi, paina seuraavaa linkkiä:'
+mail_body_register: 'Aktivoidaksesi tilisi, napsauta seuraavaa linkkiä:'
mail_body_account_information_external: Voit nyt käyttää "%s" tiliäsi kirjautuaksesi järjestelmään.
mail_body_account_information: Sinun tilin tiedot
mail_subject_account_activation_request: %s tilin aktivointi pyyntö
@@ -97,8 +98,8 @@ field_name: Nimi
field_description: Kuvaus
field_summary: Yhteenveto
field_is_required: Vaaditaan
-field_firstname: Etu nimi
-field_lastname: Suku nimi
+field_firstname: Etunimi
+field_lastname: Sukunimi
field_mail: Sähköposti
field_filename: Tiedosto
field_filesize: Koko
@@ -109,9 +110,9 @@ field_updated_on: Päivitetty
field_field_format: Muoto
field_is_for_all: Kaikille projekteille
field_possible_values: Mahdolliset arvot
-field_regexp: Säännönmukainen ilmentymä (reg exp)
-field_min_length: Minimi pituus
-field_max_length: Maksimi pituus
+field_regexp: Säännöllinen lauseke (reg exp)
+field_min_length: Minimipituus
+field_max_length: Maksimipituus
field_value: Arvo
field_category: Luokka
field_title: Otsikko
@@ -120,18 +121,18 @@ field_issue: Tapahtuma
field_status: Tila
field_notes: Muistiinpanot
field_is_closed: Tapahtuma suljettu
-field_is_default: Vakio arvo
+field_is_default: Vakioarvo
field_tracker: Tapahtuma
field_subject: Aihe
field_due_date: Määräaika
field_assigned_to: Nimetty
field_priority: Prioriteetti
-field_fixed_version: Kohde versio
+field_fixed_version: Kohdeversio
field_user: Käyttäjä
field_role: Rooli
field_homepage: Kotisivu
field_is_public: Julkinen
-field_parent: Alaprojekti
+field_parent: Aliprojekti
field_is_in_chlog: Tapahtumat näytetään muutoslokissa
field_is_in_roadmap: Tapahtumat näytetään roadmap näkymässä
field_login: Kirjautuminen
@@ -145,23 +146,23 @@ field_new_password: Uusi salasana
field_password_confirmation: Vahvistus
field_version: Versio
field_type: Tyyppi
-field_host: Isäntä
+field_host: Verkko-osoite
field_port: Portti
field_account: Tili
field_base_dn: Base DN
-field_attr_login: Kirjautumis määre
-field_attr_firstname: Etuminen määre
-field_attr_lastname: Sukunimen määre
-field_attr_mail: Sähköpostin määre
+field_attr_login: Kirjautumismääre
+field_attr_firstname: Etuminenmääre
+field_attr_lastname: Sukunimenmääre
+field_attr_mail: Sähköpostinmääre
field_onthefly: Automaattinen käyttäjien luonti
field_start_date: Alku
field_done_ratio: %% Tehty
-field_auth_source: Autentikointi muoto
+field_auth_source: Varmennusmuoto
field_hide_mail: Piiloita sähköpostiosoitteeni
field_comments: Kommentti
field_url: URL
-field_start_page: Aloitus sivu
-field_subproject: Alaprojekti
+field_start_page: Aloitussivu
+field_subproject: Aliprojekti
field_hours: Tuntia
field_activity: Historia
field_spent_on: Päivä
@@ -175,32 +176,32 @@ field_estimated_hours: Arvioitu aika
field_column_names: Saraketta
field_time_zone: Aikavyöhyke
field_searchable: Haettava
-field_default_value: Vakio arvo
+field_default_value: Vakioarvo
setting_app_title: Ohjelman otsikko
setting_app_subtitle: Ohjelman alaotsikko
-setting_welcome_text: Tervetulo teksti
-setting_default_language: Vakio kieli
-setting_login_required: Pakollinen autentikointi
-setting_self_registration: Tee-Se-Itse rekisteröinti
-setting_attachment_max_size: Liitteen maksimi koko
-setting_issues_export_limit: Tapahtumien vienti rajoite
+setting_welcome_text: Tervehdysteksti
+setting_default_language: Vakiokieli
+setting_login_required: Pakollinen kirjautuminen
+setting_self_registration: Itserekisteröinti
+setting_attachment_max_size: Liitteen maksimikoko
+setting_issues_export_limit: Tapahtumien vientirajoite
setting_mail_from: Lähettäjän sähköpostiosoite
-setting_bcc_recipients: Blind carbon copy vastaanottajat (bcc)
-setting_host_name: Isännän nimi
+setting_bcc_recipients: Vastaanottajat piilokopiona (bcc)
+setting_host_name: Verkko-osoite
setting_text_formatting: Tekstin muotoilu
setting_wiki_compression: Wiki historian pakkaus
setting_feeds_limit: Syötteen sisällön raja
-setting_autofetch_changesets: Automaatisen haun souritukset
-setting_sys_api_enabled: Salli WS säiliön hallintaan
+setting_autofetch_changesets: Automaattisten muutosjoukkojen haku
+setting_sys_api_enabled: Salli WS tietovaraston hallintaan
setting_commit_ref_keywords: Viittaavat hakusanat
setting_commit_fix_keywords: Korjaavat hakusanat
setting_autologin: Automaatinen kirjautuminen
setting_date_format: Päivän muoto
setting_time_format: Ajan muoto
setting_cross_project_issue_relations: Salli projektien väliset tapahtuminen suhteet
-setting_issue_list_default_columns: Vakio sarakkeiden näyttö tapahtuma listauksessa
-setting_repositories_encodings: Säiliön koodaus
+setting_issue_list_default_columns: Vakiosarakkeiden näyttö tapahtumalistauksessa
+setting_repositories_encodings: Tietovaraston koodaus
setting_emails_footer: Sähköpostin alatunniste
setting_protocol: Protokolla
setting_per_page_options: Sivun objektien määrän asetukset
@@ -235,8 +236,8 @@ label_workflow: Työnkulku
label_issue_status: Tapahtuman tila
label_issue_status_plural: Tapahtumien tilat
label_issue_status_new: Uusi tila
-label_issue_category: Tapahtuma luokka
-label_issue_category_plural: Tapahtuma luokat
+label_issue_category: Tapahtumaluokka
+label_issue_category_plural: Tapahtumaluokat
label_issue_category_new: Uusi luokka
label_custom_field: Räätälöity kenttä
label_custom_field_plural: Räätälöidyt kentät
@@ -249,9 +250,9 @@ label_please_login: Kirjaudu ole hyvä
label_register: Rekisteröidy
label_password_lost: Hukattu salasana
label_home: Koti
-label_my_page: Minun sivu
-label_my_account: Minun tili
-label_my_projects: Minun projektit
+label_my_page: Omasivu
+label_my_account: Oma tili
+label_my_projects: Omat projektit
label_administration: Ylläpito
label_login: Kirjaudu sisään
label_logout: Kirjaudu ulos
@@ -266,11 +267,11 @@ label_activity: Historia
label_new: Uusi
label_logged_as: Kirjauduttu nimellä
label_environment: Ympäristö
-label_authentication: Autentikointi
-label_auth_source: Autentikointi tapa
-label_auth_source_new: Uusi autentikointi tapa
-label_auth_source_plural: Autentikointi tavat
-label_subproject_plural: Alaprojektit
+label_authentication: Varmennus
+label_auth_source: Varmennustapa
+label_auth_source_new: Uusi varmennustapa
+label_auth_source_plural: Varmennustavat
+label_subproject_plural: Aliprojektit
label_min_max_length: Min - Max pituudet
label_list: Lista
label_date: Päivä
@@ -307,8 +308,8 @@ label_confirmation: Vahvistus
label_export_to: Vie
label_read: Lukee...
label_public_projects: Julkiset projektit
-label_open_issues: avoin
-label_open_issues_plural: avointa
+label_open_issues: avoin, yhteensä
+label_open_issues_plural: avointa, yhteensä
label_closed_issues: suljettu
label_closed_issues_plural: suljettua
label_total: Yhteensä
@@ -341,8 +342,8 @@ label_query_plural: Räätälöidyt haut
label_query_new: Uusi haku
label_filter_add: Lisää suodatin
label_filter_plural: Suodattimet
-label_equals: yhtä kuin
-label_not_equals: epäsuuri kuin
+label_equals: sama kuin
+label_not_equals: eri kuin
label_in_less_than: pienempi kuin
label_in_more_than: suurempi kuin
label_today: tänään
@@ -353,8 +354,8 @@ label_ago: päiviä sitten
label_contains: sisältää
label_not_contains: ei sisällä
label_day_plural: päivää
-label_repository: Säiliö
-label_repository_plural: Säiliöt
+label_repository: Tietovarasto
+label_repository_plural: Tietovarastot
label_browse: Selaus
label_modification: %d muutos
label_modification_plural: %d muutettu
@@ -366,7 +367,7 @@ label_deleted: poistettu
label_latest_revision: Viimeisin versio
label_latest_revision_plural: Viimeisimmät versiot
label_view_revisions: Näytä versiot
-label_max_size: Maksimi koko
+label_max_size: Suurin koko
label_sort_highest: Siirrä ylimmäiseksi
label_sort_higher: Siirrä ylös
label_sort_lower: Siirrä alas
@@ -411,15 +412,15 @@ label_loading: Lataa...
label_relation_new: Uusi suhde
label_relation_delete: Poista suhde
label_relates_to: liittyy
-label_duplicates: kaksoiskappale
+label_duplicates: kopio
label_blocks: estää
label_blocked_by: estetty
label_precedes: edeltää
label_follows: seuraa
-label_end_to_start: loppu alkuun
-label_end_to_end: loppu loppuun
-label_start_to_start: alku alkuun
-label_start_to_end: alku loppuun
+label_end_to_start: lopusta alkuun
+label_end_to_end: lopusta loppuun
+label_start_to_start: alusta alkuun
+label_start_to_end: alusta loppuun
label_stay_logged_in: Pysy kirjautuneena
label_disabled: poistettu käytöstä
label_show_completed_versions: Näytä valmiit versiot
@@ -439,14 +440,14 @@ label_week: Viikko
label_language_based: Pohjautuen käyttäjän kieleen
label_sort_by: Lajittele %s
label_send_test_email: Lähetä testi sähköposti
-label_feeds_access_key_created_on: RSS pääsy avain luotiin %s sitten
+label_feeds_access_key_created_on: RSS salasana luotiin %s sitten
label_module_plural: Moduulit
label_added_time_by: Lisännyt %s %s sitten
label_updated_time: Päivitetty %s sitten
label_jump_to_a_project: Siirry projektiin...
label_file_plural: Tiedostot
label_changeset_plural: Muutosryhmät
-label_default_columns: Vakio sarakkeet
+label_default_columns: Vakiosarakkeet
label_no_change_option: (Ei muutosta)
label_bulk_edit_selected_issues: Perusmuotoile valitut tapahtumat
label_theme: Teema
@@ -457,8 +458,8 @@ label_user_mail_option_selected: "Kaikista tapahtumista vain valitsemistani proj
label_user_mail_option_none: "Vain tapahtumista joita valvon tai olen mukana"
label_user_mail_no_self_notified: "En halua muistutusta muutoksista joita itse teen"
label_registration_activation_by_email: tilin aktivointi sähköpostitse
-label_registration_manual_activation: manuaalinen tilin aktivointi
-label_registration_automatic_activation: automaattinen tilin aktivointi
+label_registration_manual_activation: tilin aktivointi käsin
+label_registration_automatic_activation: tilin aktivointi automaattisesti
label_display_per_page: 'Per sivu: %s'
label_age: Ikä
label_change_properties: Vaihda asetuksia
@@ -507,7 +508,7 @@ status_locked: lukittu
text_select_mail_notifications: Valitse tapahtumat joista tulisi lähettää sähköpostimuistutus.
text_regexp_info: esim. ^[A-Z0-9]+$
-text_min_max_length_info: 0 tarkoitta, ei rajoitusta
+text_min_max_length_info: 0 tarkoittaa, ei rajoitusta
text_project_destroy_confirmation: Oletko varma että haluat poistaa tämän projektin ja kaikki siihen kuuluvat tiedot?
text_workflow_edit: Valitse rooli ja tapahtuma muokataksesi työnkulkua
text_are_you_sure: Oletko varma?
@@ -521,7 +522,7 @@ text_project_identifier_info: 'Pienet kirjaimet (a-z), numerot ja viivat ovat sa
text_caracters_maximum: %d merkkiä enintään.
text_caracters_minimum: Täytyy olla vähintään %d merkkiä pitkä.
text_length_between: Pituus välillä %d ja %d merkkiä.
-text_tracker_no_workflow: Ei työnkulkua määritelty tälle tapahtumalle
+text_tracker_no_workflow: Työnkulkua ei määritelty tälle tapahtumalle
text_unallowed_characters: Kiellettyjä merkkejä
text_comma_separated: Useat arvot sallittu (pilkku eroteltuna).
text_issues_ref_in_commit_messages: Liitän ja korjaan ongelmia syötetyssä viestissä
@@ -531,8 +532,8 @@ text_wiki_destroy_confirmation: Oletko varma että haluat poistaa tämän wiki:n
text_issue_category_destroy_question: Jotkut tapahtumat (%d) ovat nimetty tälle luokalle. Mitä haluat tehdä?
text_issue_category_destroy_assignments: Poista luokan tehtävät
text_issue_category_reassign_to: Vaihda tapahtuma tähän luokkaan
-text_user_mail_option: "Valitesemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)."
-text_no_configuration_data: "Rooleja, tikettejä, tapahtumien tiloja ja työnkulkua ei vielä olla määritelty.\nOn erittäin suotavaa ladata vakioasetukset. Voit muuttaa sitä latauksen jälkeen."
+text_user_mail_option: "Valitsemattomille projekteille, saat vain muistutuksen asioista joita seuraat tai olet mukana (esim. tapahtumat joissa olet tekijä tai nimettynä)."
+text_no_configuration_data: "Rooleja, tapahtumien tiloja ja työnkulkua ei vielä olla määritelty.\nOn erittäin suotavaa ladata vakioasetukset. Voit muuttaa sitä latauksen jälkeen."
text_load_default_configuration: Lataa vakioasetukset
default_role_manager: Päälikkö
@@ -557,7 +558,7 @@ default_priority_immediate: Valitön
default_activity_design: Suunnittelu
default_activity_development: Kehitys
-enumeration_issue_priorities: Tapahtuman prioriteetit
+enumeration_issue_priorities: Tapahtuman tärkeysjärjestys
enumeration_doc_categories: Dokumentin luokat
enumeration_activities: Historia (ajan seuranta)
label_associated_revisions: Liittyvät versiot
@@ -578,15 +579,15 @@ project_module_issue_tracking: Tapahtuman seuranta
project_module_wiki: Wiki
project_module_files: Tiedostot
project_module_documents: Dokumentit
-project_module_repository: Säiliö
+project_module_repository: Tietovarasto
project_module_news: Uutiset
project_module_time_tracking: Ajan seuranta
-text_file_repository_writable: Kirjoitettava tiedosto säiliö
+text_file_repository_writable: Kirjoitettava tiedostovarasto
text_default_administrator_account_changed: Vakio hallinoijan tunnus muutettu
text_rmagick_available: RMagick saatavilla (valinnainen)
button_configure: Asetukset
label_plugins: Lisäosat
-label_ldap_authentication: LDAP autentikointi
+label_ldap_authentication: LDAP tunnistautuminen
label_downloads_abbr: D/L
label_add_another_file: Lisää uusi tiedosto
label_this_month: tässä kuussa
@@ -609,7 +610,7 @@ label_date_to: ''
setting_activity_days_default: Päivien esittäminen projektien historiassa
label_date_from: ''
label_in: ''
-setting_display_subprojects_issues: Näytä alaprojektien tapahtumat pääprojektissa oletusarvoisesti
+setting_display_subprojects_issues: Näytä aliprojektien tapahtumat pääprojektissa oletusarvoisesti
field_comments_sorting: Näytä kommentit
label_reverse_chronological_order: Käänteisessä aikajärjestyksessä
label_preferences: Asetukset
@@ -617,4 +618,21 @@ setting_default_projects_public: Uudet projektit ovat oletuksena julkisia
label_overall_activity: Kokonaishistoria
error_scm_annotate: "Merkintää ei ole tai siihen ei voi lisätä selityksiä."
label_planning: Suunnittelu
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+text_subprojects_destroy_warning: 'Tämän aliprojekti(t): %s tullaan myös poistamaan.'
+label_and_its_subprojects: %s ja aliprojektit
+mail_body_reminder: "%d sinulle nimettyä tapahtuma(a) erääntyy %d päivä sisään:"
+mail_subject_reminder: "%d tapahtuma(a) erääntyy lähipäivinä"
+text_user_wrote: '%s kirjoitti:'
+label_duplicated_by: kopioinut
+setting_enabled_scm: Versionhallinta käytettävissä
+text_enumeration_category_reassign_to: 'Siirrä täksi arvoksi:'
+text_enumeration_destroy_question: '%d kohdetta on sijoitettu tälle arvolle.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/fr.yml b/groups/lang/fr.yml
index cbdda4f3d..81e44949f 100644
--- a/groups/lang/fr.yml
+++ b/groups/lang/fr.yml
@@ -48,6 +48,7 @@ general_text_no: 'non'
general_text_yes: 'oui'
general_lang_name: 'Français'
general_csv_separator: ';'
+general_csv_decimal_separator: ','
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Lundi,Mardi,Mercredi,Jeudi,Vendredi,Samedi,Dimanche
@@ -91,6 +92,8 @@ mail_body_account_information_external: Vous pouvez utiliser votre compte "%s" p
mail_body_account_information: Paramètres de connexion de votre compte
mail_subject_account_activation_request: "Demande d'activation d'un compte %s"
mail_body_account_activation_request: "Un nouvel utilisateur (%s) s'est inscrit. Son compte nécessite votre approbation:"
+mail_subject_reminder: "%d demande(s) arrivent à échéance"
+mail_body_reminder: "%d demande(s) qui vous sont assignées arrivent à échéance dans les %d prochains jours:"
gui_validation_error: 1 erreur
gui_validation_error_plural: %d erreurs
@@ -180,6 +183,7 @@ field_time_zone: Fuseau horaire
field_searchable: Utilisé pour les recherches
field_default_value: Valeur par défaut
field_comments_sorting: Afficher les commentaires
+field_parent_title: Page parent
setting_app_title: Titre de l'application
setting_app_subtitle: Sous-titre de l'application
@@ -206,12 +210,16 @@ setting_time_format: Format d'heure
setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
setting_repositories_encodings: Encodages des dépôts
+setting_commit_logs_encoding: Encodage des messages de commit
setting_emails_footer: Pied-de-page des emails
setting_protocol: Protocole
setting_per_page_options: Options d'objets affichés par page
setting_user_format: Format d'affichage des utilisateurs
setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
+setting_enabled_scm: SCM activés
+setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
+setting_mail_handler_api_key: Clé de protection de l'API
project_module_issue_tracking: Suivi des demandes
project_module_time_tracking: Suivi du temps passé
@@ -291,6 +299,7 @@ label_auth_source: Mode d'authentification
label_auth_source_new: Nouveau mode d'authentification
label_auth_source_plural: Modes d'authentification
label_subproject_plural: Sous-projets
+label_and_its_subprojects: %s et ses sous-projets
label_min_max_length: Longueurs mini - maxi
label_list: Liste
label_date: Date
@@ -444,7 +453,8 @@ label_loading: Chargement...
label_relation_new: Nouvelle relation
label_relation_delete: Supprimer la relation
label_relates_to: lié à
-label_duplicates: doublon de
+label_duplicates: duplique
+label_duplicated_by: dupliqué par
label_blocks: bloque
label_blocked_by: bloqué par
label_precedes: précède
@@ -510,6 +520,9 @@ label_preferences: Préférences
label_chronological_order: Dans l'ordre chronologique
label_reverse_chronological_order: Dans l'ordre chronologique inverse
label_planning: Planning
+label_incoming_emails: Emails entrants
+label_generate_key: Générer une clé
+label_issue_watchers: Utilisateurs surveillant cette demande
button_login: Connexion
button_submit: Soumettre
@@ -548,6 +561,7 @@ button_copy: Copier
button_annotate: Annoter
button_update: Mettre à jour
button_configure: Configurer
+button_quote: Citer
status_active: actif
status_registered: enregistré
@@ -566,7 +580,7 @@ text_journal_deleted: supprimé
text_tip_task_begin_day: tâche commençant ce jour
text_tip_task_end_day: tâche finissant ce jour
text_tip_task_begin_end_day: tâche commençant et finissant ce jour
-text_project_identifier_info: 'Lettres minuscules (a-z), chiffres et tirets autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
+text_project_identifier_info: 'Seuls les lettres minuscules (a-z), chiffres et tirets sont autorisés.<br />Un fois sauvegardé, l''identifiant ne pourra plus être modifié.'
text_caracters_maximum: %d caractères maximum.
text_caracters_minimum: %d caractères minimum.
text_length_between: Longueur comprise entre %d et %d caractères.
@@ -593,6 +607,10 @@ text_destroy_time_entries_question: %.02f heures ont été enregistrées sur les
text_destroy_time_entries: Supprimer les heures
text_assign_time_entries_to_project: Reporter les heures sur le projet
text_reassign_time_entries: 'Reporter les heures sur cette demande:'
+text_user_wrote: '%s a écrit:'
+text_enumeration_destroy_question: 'Cette valeur est affectée à %d objets.'
+text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
+text_email_delivery_not_configured: "L'envoi de mail n'est pas configuré, les notifications sont désactivées.\nConfigurez votre serveur SMTP dans config/email.yml et redémarrez l'application pour les activer."
default_role_manager: Manager
default_role_developper: Développeur
diff --git a/groups/lang/he.yml b/groups/lang/he.yml
index a611c8c39..77fe32e53 100644
--- a/groups/lang/he.yml
+++ b/groups/lang/he.yml
@@ -48,6 +48,7 @@ general_text_no: 'ל×'
general_text_yes: 'כן'
general_lang_name: 'Hebrew (עברית)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-8-I
general_pdf_encoding: ISO-8859-8-I
general_day_names: שני,שלישי,רביעי,חמישי,שישי,שבת,ר×שון
@@ -618,3 +619,20 @@ setting_default_projects_public: ×¤×¨×•×™×§×˜×™× ×—×“×©×™× ×”×™× × ×¤×•×ž×‘×™
error_scm_annotate: "הכניסה ×œ× ×§×™×™×ž×ª ×ו ×©×œ× × ×™×ª×Ÿ לת×ר ×ותה."
label_planning: תכנון
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/hu.yml b/groups/lang/hu.yml
new file mode 100644
index 000000000..208b6fe1e
--- /dev/null
+++ b/groups/lang/hu.yml
@@ -0,0 +1,639 @@
+_gloc_rule_default: '|n| n==1 ? "" : "_plural" '
+
+actionview_datehelper_select_day_prefix:
+actionview_datehelper_select_month_names: Január,Február,Március,Ãprilis,Május,Június,Július,Augusztus,Szeptember,Október,November,December
+actionview_datehelper_select_month_names_abbr: Jan,Feb,Már,Ãpr,Máj,Jún,Júl,Aug,Szept,Okt,Nov,Dec
+actionview_datehelper_select_month_prefix:
+actionview_datehelper_select_year_prefix:
+actionview_datehelper_time_in_words_day: 1 nap
+actionview_datehelper_time_in_words_day_plural: %d nap
+actionview_datehelper_time_in_words_hour_about: kb. 1 óra
+actionview_datehelper_time_in_words_hour_about_plural: kb. %d óra
+actionview_datehelper_time_in_words_hour_about_single: kb. 1 óra
+actionview_datehelper_time_in_words_minute: 1 perc
+actionview_datehelper_time_in_words_minute_half: fél perc
+actionview_datehelper_time_in_words_minute_less_than: kevesebb, mint 1 perc
+actionview_datehelper_time_in_words_minute_plural: %d perc
+actionview_datehelper_time_in_words_minute_single: 1 perc
+actionview_datehelper_time_in_words_second_less_than: kevesebb, mint 1 másodperc
+actionview_datehelper_time_in_words_second_less_than_plural: kevesebb, mint %d másodperc
+actionview_instancetag_blank_option: Kérem válasszon
+
+activerecord_error_inclusion: nem található a listában
+activerecord_error_exclusion: foglalt
+activerecord_error_invalid: érvénytelen
+activerecord_error_confirmation: jóváhagyás szükséges
+activerecord_error_accepted: ell kell fogadni
+activerecord_error_empty: nem lehet üres
+activerecord_error_blank: nem lehet üres
+activerecord_error_too_long: túl hosszú
+activerecord_error_too_short: túl rövid
+activerecord_error_wrong_length: hibás a hossza
+activerecord_error_taken: már foglalt
+activerecord_error_not_a_number: nem egy szám
+activerecord_error_not_a_date: nem érvényes dátum
+activerecord_error_greater_than_start_date: nagyobbnak kell lennie, mint az indítás dátuma
+activerecord_error_not_same_project: nem azonos projekthez tartozik
+activerecord_error_circular_dependency: Ez a kapcsolat egy körkörös függőséget eredményez
+
+general_fmt_age: %d év
+general_fmt_age_plural: %d év
+general_fmt_date: %%Y.%%m.%%d
+general_fmt_datetime: %%Y.%%m.%%d %%H:%%M:%%S
+general_fmt_datetime_short: %%b %%d, %%H:%%M:%%S
+general_fmt_time: %%H:%%M:%%S
+general_text_No: 'Nem'
+general_text_Yes: 'Igen'
+general_text_no: 'nem'
+general_text_yes: 'igen'
+general_lang_name: 'Magyar'
+general_csv_separator: ','
+general_csv_decimal_separator: '.'
+general_csv_encoding: ISO-8859-2
+general_pdf_encoding: ISO-8859-2
+general_day_names: Hétfő,Kedd,Szerda,Csütörtök,Péntek,Szombat,Vasárnap
+general_first_day_of_week: '1'
+
+notice_account_updated: A fiók adatai sikeresen frissítve.
+notice_account_invalid_creditentials: Hibás felhasználói név, vagy jelszó
+notice_account_password_updated: A jelszó módosítása megtörtént.
+notice_account_wrong_password: Hibás jelszó
+notice_account_register_done: A fiók sikeresen létrehozva. Aktiválásához kattints az e-mailben kapott linkre
+notice_account_unknown_email: Ismeretlen felhasználó.
+notice_can_t_change_password: A fiók külső azonosítási forrást használ. A jelszó megváltoztatása nem lehetséges.
+notice_account_lost_email_sent: Egy e-mail üzenetben postáztunk Önnek egy leírást az új jelszó beállításáról.
+notice_account_activated: Fiókját aktiváltuk. Most már be tud jelentkezni a rendszerbe.
+notice_successful_create: Sikeres létrehozás.
+notice_successful_update: Sikeres módosítás.
+notice_successful_delete: Sikeres törlés.
+notice_successful_connection: Sikeres bejelentkezés.
+notice_file_not_found: Az oldal, amit meg szeretne nézni nem található, vagy átkerült egy másik helyre.
+notice_locking_conflict: Az adatot egy másik felhasználó idő közben módosította.
+notice_not_authorized: Nincs hozzáférési engedélye ehhez az oldalhoz.
+notice_email_sent: Egy e-mail üzenetet küldtünk a következő címre %s
+notice_email_error: Hiba történt a levél küldése közben (%s)
+notice_feeds_access_key_reseted: Az RSS hozzáférési kulcsát újra generáltuk.
+notice_failed_to_save_issues: "Nem sikerült a %d feladat(ok) mentése a %d -ban kiválasztva: %s."
+notice_no_issue_selected: "Nincs feladat kiválasztva! Kérem jelölje meg melyik feladatot szeretné szerkeszteni!"
+notice_account_pending: "A fiókja létrejött, és adminisztrátori jóváhagyásra vár."
+notice_default_data_loaded: Az alapértelmezett konfiguráció betöltése sikeresen megtörtént.
+
+error_can_t_load_default_data: "Az alapértelmezett konfiguráció betöltése nem lehetséges: %s"
+error_scm_not_found: "A bejegyzés, vagy revízió nem található a tárolóban."
+error_scm_command_failed: "A tároló elérése közben hiba lépett fel: %s"
+error_scm_annotate: "A bejegyzés nem létezik, vagy nics jegyzetekkel ellátva."
+error_issue_not_found_in_project: 'A feladat nem található, vagy nem ehhez a projekthez tartozik'
+
+mail_subject_lost_password: Az Ön Redmine jelszava
+mail_body_lost_password: 'A Redmine jelszó megváltoztatásához, kattintson a következő linkre:'
+mail_subject_register: Redmine azonosító aktiválása
+mail_body_register: 'A Redmine azonosítója aktiválásához, kattintson a következő linkre:'
+mail_body_account_information_external: A "%s" azonosító használatával bejelentkezhet a Redmineba.
+mail_body_account_information: Az Ön Redmine azonosítójának információi
+mail_subject_account_activation_request: Redmine azonosító aktiválási kérelem
+mail_body_account_activation_request: 'Egy új felhasználó (%s) regisztrált, azonosítója jóváhasgyásra várakozik:'
+
+gui_validation_error: 1 hiba
+gui_validation_error_plural: %d hiba
+
+field_name: Név
+field_description: Leírás
+field_summary: Összegzés
+field_is_required: Kötelező
+field_firstname: Keresztnév
+field_lastname: Vezetéknév
+field_mail: E-mail
+field_filename: Fájl
+field_filesize: Méret
+field_downloads: Letöltések
+field_author: Szerző
+field_created_on: Létrehozva
+field_updated_on: Módosítva
+field_field_format: Formátum
+field_is_for_all: Minden projekthez
+field_possible_values: Lehetséges értékek
+field_regexp: Reguláris kifejezés
+field_min_length: Minimum hossz
+field_max_length: Maximum hossz
+field_value: Érték
+field_category: Kategória
+field_title: Cím
+field_project: Projekt
+field_issue: Feladat
+field_status: Státusz
+field_notes: Feljegyzések
+field_is_closed: Feladat lezárva
+field_is_default: Alapértelmezett érték
+field_tracker: Típus
+field_subject: Tárgy
+field_due_date: Befejezés dátuma
+field_assigned_to: Felelős
+field_priority: Prioritás
+field_fixed_version: Cél verzió
+field_user: Felhasználó
+field_role: Szerepkör
+field_homepage: Weboldal
+field_is_public: Nyilvános
+field_parent: Szülő projekt
+field_is_in_chlog: Feladatok látszanak a változás naplóban
+field_is_in_roadmap: Feladatok látszanak az életútban
+field_login: Azonosító
+field_mail_notification: E-mail értesítések
+field_admin: Adminisztrátor
+field_last_login_on: Utolsó bejelentkezés
+field_language: Nyelv
+field_effective_date: Dátum
+field_password: Jelszó
+field_new_password: Új jelszó
+field_password_confirmation: Megerősítés
+field_version: Verzió
+field_type: Típus
+field_host: Kiszolgáló
+field_port: Port
+field_account: Felhasználói fiók
+field_base_dn: Base DN
+field_attr_login: Bejelentkezési tulajdonság
+field_attr_firstname: Családnév
+field_attr_lastname: Utónév
+field_attr_mail: E-mail
+field_onthefly: On-the-fly felhasználó létrehozás
+field_start_date: Kezdés dátuma
+field_done_ratio: Elkészült (%%)
+field_auth_source: Azonosítási mód
+field_hide_mail: Rejtse el az e-mail címem
+field_comments: Megjegyzés
+field_url: URL
+field_start_page: Kezdőlap
+field_subproject: Alprojekt
+field_hours: Óra
+field_activity: Aktivitás
+field_spent_on: Dátum
+field_identifier: Azonosító
+field_is_filter: Szűrőként használható
+field_issue_to_id: Kapcsolódó feladat
+field_delay: Késés
+field_assignable: Feladat rendelhető ehhez a szerepkörhöz
+field_redirect_existing_links: Létező linkek átirányítása
+field_estimated_hours: Becsült idő
+field_column_names: Oszlopok
+field_time_zone: Időzóna
+field_searchable: Kereshető
+field_default_value: Alapértelmezett érték
+field_comments_sorting: Feljegyzések megjelenítése
+
+setting_app_title: Alkalmazás címe
+setting_app_subtitle: Alkalmazás alcíme
+setting_welcome_text: Üdvözlő üzenet
+setting_default_language: Alapértelmezett nyelv
+setting_login_required: Azonosítás szükséges
+setting_self_registration: Regisztráció
+setting_attachment_max_size: Melléklet max. mérete
+setting_issues_export_limit: Feladatok exportálásának korlátja
+setting_mail_from: Kibocsátó e-mail címe
+setting_bcc_recipients: Titkos másolat címzet (bcc)
+setting_host_name: Kiszolgáló neve
+setting_text_formatting: Szöveg formázás
+setting_wiki_compression: Wiki történet tömörítés
+setting_feeds_limit: RSS tartalom korlát
+setting_default_projects_public: Az új projektek alapértelmezés szerint nyilvánosak
+setting_autofetch_changesets: Commitok automatikus lehúzása
+setting_sys_api_enabled: WS engedélyezése a tárolók kezeléséhez
+setting_commit_ref_keywords: Hivatkozó kulcsszavak
+setting_commit_fix_keywords: Javítások kulcsszavai
+setting_autologin: Automatikus bejelentkezés
+setting_date_format: Dátum formátum
+setting_time_format: Idő formátum
+setting_cross_project_issue_relations: Kereszt-projekt feladat hivatkozások engedélyezése
+setting_issue_list_default_columns: Az alapértelmezésként megjelenített oszlopok a feladat listában
+setting_repositories_encodings: Tárolók kódolása
+setting_emails_footer: E-mail lábléc
+setting_protocol: Protokol
+setting_per_page_options: Objektum / oldal opciók
+setting_user_format: Felhasználók megjelenítésének formája
+setting_activity_days_default: Napok megjelenítése a project aktivitásnál
+setting_display_subprojects_issues: Alapértelmezettként mutassa az alprojektek feladatait is a projekteken
+
+project_module_issue_tracking: Feladat követés
+project_module_time_tracking: Idő rögzítés
+project_module_news: Hírek
+project_module_documents: Dokumentumok
+project_module_files: Fájlok
+project_module_wiki: Wiki
+project_module_repository: Tároló
+project_module_boards: Fórumok
+
+label_user: Felhasználó
+label_user_plural: Felhasználók
+label_user_new: Új felhasználó
+label_project: Projekt
+label_project_new: Új projekt
+label_project_plural: Projektek
+label_project_all: Az összes projekt
+label_project_latest: Legutóbbi projektek
+label_issue: Feladat
+label_issue_new: Új feladat
+label_issue_plural: Feladatok
+label_issue_view_all: Minden feladat megtekintése
+label_issues_by: %s feladatai
+label_issue_added: Feladat hozzáadva
+label_issue_updated: Feladat frissítve
+label_document: Dokumentum
+label_document_new: Új dokumentum
+label_document_plural: Dokumentumok
+label_document_added: Dokumentum hozzáadva
+label_role: Szerepkör
+label_role_plural: Szerepkörök
+label_role_new: Új szerepkör
+label_role_and_permissions: Szerepkörök, és jogosultságok
+label_member: Résztvevő
+label_member_new: Új résztvevő
+label_member_plural: Résztvevők
+label_tracker: Feladat típus
+label_tracker_plural: Feladat típusok
+label_tracker_new: Új feladat típus
+label_workflow: Workflow
+label_issue_status: Feladat státusz
+label_issue_status_plural: Feladat státuszok
+label_issue_status_new: Új státusz
+label_issue_category: Feladat kategória
+label_issue_category_plural: Feladat kategóriák
+label_issue_category_new: Új kategória
+label_custom_field: Egyéni mező
+label_custom_field_plural: Egyéni mezők
+label_custom_field_new: Új egyéni mező
+label_enumerations: Felsorolások
+label_enumeration_new: Új érték
+label_information: Információ
+label_information_plural: Információk
+label_please_login: Jelentkezzen be
+label_register: Regisztráljon
+label_password_lost: Elfelejtett jelszó
+label_home: Kezdőlap
+label_my_page: Saját kezdőlapom
+label_my_account: Fiókom adatai
+label_my_projects: Saját projektem
+label_administration: Adminisztráció
+label_login: Bejelentkezés
+label_logout: Kijelentkezés
+label_help: Súgó
+label_reported_issues: Bejelentett feladatok
+label_assigned_to_me_issues: A nekem kiosztott feladatok
+label_last_login: Utolsó bejelentkezés
+label_last_updates: Utoljára frissítve
+label_last_updates_plural: Utoljára módosítva %d
+label_registered_on: Regisztrált
+label_activity: Tevékenységek
+label_overall_activity: Teljes aktivitás
+label_new: Új
+label_logged_as: Bejelentkezve, mint
+label_environment: Környezet
+label_authentication: Azonosítás
+label_auth_source: Azonosítás módja
+label_auth_source_new: Új azonosítási mód
+label_auth_source_plural: Azonosítási módok
+label_subproject_plural: Alprojektek
+label_and_its_subprojects: %s és alprojektjei
+label_min_max_length: Min - Max hossz
+label_list: Lista
+label_date: Dátum
+label_integer: Egész
+label_float: Lebegőpontos
+label_boolean: Logikai
+label_string: Szöveg
+label_text: Hosszú szöveg
+label_attribute: Tulajdonság
+label_attribute_plural: Tulajdonságok
+label_download: %d Letöltés
+label_download_plural: %d Letöltések
+label_no_data: Nincs megjeleníthető adat
+label_change_status: Státusz módosítása
+label_history: Történet
+label_attachment: Fájl
+label_attachment_new: Új fájl
+label_attachment_delete: Fájl törlése
+label_attachment_plural: Fájlok
+label_file_added: Fájl hozzáadva
+label_report: Jelentés
+label_report_plural: Jelentések
+label_news: Hírek
+label_news_new: Hír hozzáadása
+label_news_plural: Hírek
+label_news_latest: Legutóbbi hírek
+label_news_view_all: Minden hír megtekintése
+label_news_added: Hír hozzáadva
+label_change_log: Változás napló
+label_settings: Beállítások
+label_overview: Ãttekintés
+label_version: Verzió
+label_version_new: Új verzió
+label_version_plural: Verziók
+label_confirmation: Jóváhagyás
+label_export_to: Exportálás
+label_read: Olvas...
+label_public_projects: Nyilvános projektek
+label_open_issues: nyitott
+label_open_issues_plural: nyitott
+label_closed_issues: lezárt
+label_closed_issues_plural: lezárt
+label_total: Összesen
+label_permissions: Jogosultságok
+label_current_status: Jelenlegi státusz
+label_new_statuses_allowed: Státusz változtatások engedélyei
+label_all: mind
+label_none: nincs
+label_nobody: senki
+label_next: Következő
+label_previous: Előző
+label_used_by: Használja
+label_details: Részletek
+label_add_note: Jegyzet hozzáadása
+label_per_page: Oldalanként
+label_calendar: Naptár
+label_months_from: hónap, kezdve
+label_gantt: Gantt
+label_internal: Belső
+label_last_changes: utolsó %d változás
+label_change_view_all: Minden változás megtekintése
+label_personalize_page: Az oldal testreszabása
+label_comment: Megjegyzés
+label_comment_plural: Megjegyzés
+label_comment_add: Megjegyzés hozzáadása
+label_comment_added: Megjegyzés hozzáadva
+label_comment_delete: Megjegyzések törlése
+label_query: Egyéni lekérdezés
+label_query_plural: Egyéni lekérdezések
+label_query_new: Új lekérdezés
+label_filter_add: Szűrő hozzáadása
+label_filter_plural: Szűrők
+label_equals: egyenlő
+label_not_equals: nem egyenlő
+label_in_less_than: kevesebb, mint
+label_in_more_than: több, mint
+label_in: in
+label_today: ma
+label_all_time: mindenkor
+label_yesterday: tegnap
+label_this_week: aktuális hét
+label_last_week: múlt hét
+label_last_n_days: az elmúlt %d nap
+label_this_month: aktuális hónap
+label_last_month: múlt hónap
+label_this_year: aktuális év
+label_date_range: Dátum intervallum
+label_less_than_ago: kevesebb, mint nappal ezelőtt
+label_more_than_ago: több, mint nappal ezelőtt
+label_ago: nappal ezelőtt
+label_contains: tartalmazza
+label_not_contains: nem tartalmazza
+label_day_plural: nap
+label_repository: Tároló
+label_repository_plural: Tárolók
+label_browse: Tallóz
+label_modification: %d változás
+label_modification_plural: %d változások
+label_revision: Revízió
+label_revision_plural: Revíziók
+label_associated_revisions: Kapcsolt revíziók
+label_added: hozzáadva
+label_modified: módosítva
+label_deleted: törölve
+label_latest_revision: Legutolsó revízió
+label_latest_revision_plural: Legutolsó revíziók
+label_view_revisions: Revíziók megtekintése
+label_max_size: Maximális méret
+label_on: 'összesen'
+label_sort_highest: Az elejére
+label_sort_higher: Eggyel feljebb
+label_sort_lower: Eggyel lejjebb
+label_sort_lowest: Az aljára
+label_roadmap: Életút
+label_roadmap_due_in: Elkészültéig várhatóan még
+label_roadmap_overdue: %s késésben
+label_roadmap_no_issues: Nincsenek feladatok ehhez a verzióhoz
+label_search: Keresés
+label_result_plural: Találatok
+label_all_words: Minden szó
+label_wiki: Wiki
+label_wiki_edit: Wiki szerkesztés
+label_wiki_edit_plural: Wiki szerkesztések
+label_wiki_page: Wiki oldal
+label_wiki_page_plural: Wiki oldalak
+label_index_by_title: Cím szerint indexelve
+label_index_by_date: Dátum szerint indexelve
+label_current_version: Jelenlegi verzió
+label_preview: Előnézet
+label_feed_plural: Visszajelzések
+label_changes_details: Változások részletei
+label_issue_tracking: Feladat követés
+label_spent_time: Ráfordított idő
+label_f_hour: %.2f óra
+label_f_hour_plural: %.2f óra
+label_time_tracking: Idő követés
+label_change_plural: Változások
+label_statistics: Statisztikák
+label_commits_per_month: Commits havonta
+label_commits_per_author: Commits szerzőnként
+label_view_diff: Különbségek megtekintése
+label_diff_inline: inline
+label_diff_side_by_side: side by side
+label_options: Opciók
+label_copy_workflow_from: Workflow másolása innen
+label_permissions_report: Jogosultsági riport
+label_watched_issues: Megfigyelt feladatok
+label_related_issues: Kapcsolódó feladatok
+label_applied_status: Alkalmazandó státusz
+label_loading: Betöltés...
+label_relation_new: Új kapcsolat
+label_relation_delete: Kapcsolat törlése
+label_relates_to: kapcsolódik
+label_duplicates: duplikálja
+label_blocks: zárolja
+label_blocked_by: zárolta
+label_precedes: megelőzi
+label_follows: követi
+label_end_to_start: végétől indulásig
+label_end_to_end: végétől végéig
+label_start_to_start: indulástól indulásig
+label_start_to_end: indulástól végéig
+label_stay_logged_in: Emlékezzen rám
+label_disabled: kikapcsolva
+label_show_completed_versions: A kész verziók mutatása
+label_me: én
+label_board: Fórum
+label_board_new: Új fórum
+label_board_plural: Fórumok
+label_topic_plural: Témák
+label_message_plural: Üzenetek
+label_message_last: Utolsó üzenet
+label_message_new: Új üzenet
+label_message_posted: Üzenet hozzáadva
+label_reply_plural: Válaszok
+label_send_information: Fiók infomációk küldése a felhasználónak
+label_year: Év
+label_month: Hónap
+label_week: Hét
+label_date_from: 'Kezdet:'
+label_date_to: 'Vége:'
+label_language_based: A felhasználó nyelve alapján
+label_sort_by: %s szerint rendezve
+label_send_test_email: Teszt e-mail küldése
+label_feeds_access_key_created_on: 'RSS hozzáférési kulcs létrehozva ennyivel ezelőtt: %s'
+label_module_plural: Modulok
+label_added_time_by: '%s adta hozzá ennyivel ezelőtt: %s'
+label_updated_time: 'Utolsó módosítás ennyivel ezelőtt: %s'
+label_jump_to_a_project: Ugrás projekthez...
+label_file_plural: Fájlok
+label_changeset_plural: Changesets
+label_default_columns: Alapértelmezett oszlopok
+label_no_change_option: (Nincs változás)
+label_bulk_edit_selected_issues: A kiválasztott feladatok kötegelt szerkesztése
+label_theme: Téma
+label_default: Alapértelmezett
+label_search_titles_only: Keresés csak a címekben
+label_user_mail_option_all: "Minden eseményről minden saját projektemben"
+label_user_mail_option_selected: "Minden eseményről a kiválasztott projektekben..."
+label_user_mail_option_none: "Csak a megfigyelt dolgokról, vagy, amiben részt veszek"
+label_user_mail_no_self_notified: "Nem kérek értesítést az általam végzett módosításokról"
+label_registration_activation_by_email: Fiók aktiválása e-mailben
+label_registration_manual_activation: Manuális fiók aktiválás
+label_registration_automatic_activation: Automatikus fiók aktiválás
+label_display_per_page: 'Oldalanként: %s'
+label_age: Kor
+label_change_properties: Tulajdonságok változtatása
+label_general: Ãltalános
+label_more: továbbiak
+label_scm: SCM
+label_plugins: Pluginek
+label_ldap_authentication: LDAP azonosítás
+label_downloads_abbr: D/L
+label_optional_description: Opcionális leírás
+label_add_another_file: Újabb fájl hozzáadása
+label_preferences: Tulajdonságok
+label_chronological_order: Időrendben
+label_reverse_chronological_order: Fordított időrendben
+label_planning: Tervezés
+
+button_login: Bejelentkezés
+button_submit: Elfogad
+button_save: Mentés
+button_check_all: Mindent kijelöl
+button_uncheck_all: Kijelölés törlése
+button_delete: Töröl
+button_create: Létrehoz
+button_test: Teszt
+button_edit: Szerkeszt
+button_add: Hozzáad
+button_change: Változtat
+button_apply: Alkalmaz
+button_clear: Töröl
+button_lock: Zárol
+button_unlock: Felold
+button_download: Letöltés
+button_list: Lista
+button_view: Megnéz
+button_move: Mozgat
+button_back: Vissza
+button_cancel: Mégse
+button_activate: Aktivál
+button_sort: Rendezés
+button_log_time: Idő rögzítés
+button_rollback: Visszaáll erre a verzióra
+button_watch: Megfigyel
+button_unwatch: Megfigyelés törlése
+button_reply: Válasz
+button_archive: Archivál
+button_unarchive: Dearchivál
+button_reset: Reset
+button_rename: Ãtnevez
+button_change_password: Jelszó megváltoztatása
+button_copy: Másol
+button_annotate: Jegyzetel
+button_update: Módosít
+button_configure: Konfigurál
+
+status_active: aktív
+status_registered: regisztrált
+status_locked: zárolt
+
+text_select_mail_notifications: Válasszon eseményeket, amelyekről e-mail értesítést kell küldeni.
+text_regexp_info: eg. ^[A-Z0-9]+$
+text_min_max_length_info: 0 = nincs korlátozás
+text_project_destroy_confirmation: Biztosan törölni szeretné a projektet és vele együtt minden kapcsolódó adatot ?
+text_subprojects_destroy_warning: 'Az alprojekt(ek): %s szintén törlésre kerülnek.'
+text_workflow_edit: Válasszon egy szerepkört, és egy trackert a workflow szerkesztéséhez
+text_are_you_sure: Biztos benne ?
+text_journal_changed: "változás: %s volt, %s lett"
+text_journal_set_to: "beállítva: %s"
+text_journal_deleted: törölve
+text_tip_task_begin_day: a feladat ezen a napon kezdődik
+text_tip_task_end_day: a feladat ezen a napon ér véget
+text_tip_task_begin_end_day: a feladat ezen a napon kezdődik és ér véget
+text_project_identifier_info: 'Kis betűk (a-z), számok és kötőjel megengedett.<br />Mentés után az azonosítót megváltoztatni nem lehet.'
+text_caracters_maximum: maximum %d karakter.
+text_caracters_minimum: Legkevesebb %d karakter hosszúnek kell lennie.
+text_length_between: Legalább %d és legfeljebb %d hosszú karakter.
+text_tracker_no_workflow: Nincs workflow definiálva ehhez a tracker-hez
+text_unallowed_characters: Tiltott karakterek
+text_comma_separated: Több érték megengedett (vesszővel elválasztva)
+text_issues_ref_in_commit_messages: Hivatkozás feladatokra, feladatok javítása a commit üzenetekben
+text_issue_added: %s feladat bejelentve.
+text_issue_updated: %s feladat frissítve.
+text_wiki_destroy_confirmation: Biztosan törölni szeretné ezt a wiki-t minden tartalmával együtt ?
+text_issue_category_destroy_question: Néhány feladat (%d) hozzá van rendelve ehhez a kategóriához. Mit szeretne tenni ?
+text_issue_category_destroy_assignments: Kategória hozzárendelés megszűntetése
+text_issue_category_reassign_to: Feladatok újra hozzárendelése a kategóriához
+text_user_mail_option: "A nem kiválasztott projektekről csak akkor kap értesítést, ha figyelést kér rá, vagy részt vesz benne (pl. Ön a létrehozó, vagy a hozzárendelő)"
+text_no_configuration_data: "Szerepkörök, trackerek, feladat státuszok, és workflow adatok még nincsenek konfigurálva.\nErősen ajánlott, az alapértelmezett konfiguráció betöltése, és utána módosíthatja azt."
+text_load_default_configuration: Alapértelmezett konfiguráció betöltése
+text_status_changed_by_changeset: Applied in changeset %s.
+text_issues_destroy_confirmation: 'Biztos benne, hogy törölni szeretné a kijelölt feladato(ka)t ?'
+text_select_project_modules: 'Válassza ki az engedélyezett modulokat ehhez a projekthez:'
+text_default_administrator_account_changed: Alapértelmezett adminisztrátor fiók megváltoztatva
+text_file_repository_writable: Fájl tároló írható
+text_rmagick_available: RMagick elérhető (opcionális)
+text_destroy_time_entries_question: %.02f órányi munka van rögzítve a feladatokon, amiket törölni szeretne. Mit szeretne tenni ?
+text_destroy_time_entries: A rögzített órák törlése
+text_assign_time_entries_to_project: A rögzített órák hozzárendelése a projekthez
+text_reassign_time_entries: 'A rögzített órák újra hozzárendelése ehhez a feladathoz:'
+
+default_role_manager: Vezető
+default_role_developper: Fejlesztő
+default_role_reporter: Bejelentő
+default_tracker_bug: Hiba
+default_tracker_feature: Fejlesztés
+default_tracker_support: Support
+default_issue_status_new: Új
+default_issue_status_assigned: Kiosztva
+default_issue_status_resolved: Megoldva
+default_issue_status_feedback: Visszajelzés
+default_issue_status_closed: Lezárt
+default_issue_status_rejected: Elutasított
+default_doc_category_user: Felhasználói dokumentáció
+default_doc_category_tech: Technikai dokumentáció
+default_priority_low: Alacsony
+default_priority_normal: Normál
+default_priority_high: Magas
+default_priority_urgent: Sürgős
+default_priority_immediate: Azonnal
+default_activity_design: Tervezés
+default_activity_development: Fejlesztés
+
+enumeration_issue_priorities: Feladat prioritások
+enumeration_doc_categories: Dokumentum kategóriák
+enumeration_activities: Tevékenységek (idő rögzítés)
+mail_body_reminder: "%d neked kiosztott feladat határidős az elkövetkező %d napban:"
+mail_subject_reminder: "%d feladat határidős az elkövetkező napokban"
+text_user_wrote: '%s írta:'
+label_duplicated_by: duplikálta
+setting_enabled_scm: Forráskódkezelő (SCM) engedélyezése
+text_enumeration_category_reassign_to: 'Újra hozzárendelés ehhez:'
+text_enumeration_destroy_question: '%d objektum van hozzárendelve ehhez az értékhez.'
+label_incoming_emails: Beérkezett levelek
+label_generate_key: Kulcs generálása
+setting_mail_handler_api_enabled: Web Service engedélyezése a beérkezett levelekhez
+setting_mail_handler_api_key: API kulcs
+text_email_delivery_not_configured: "Az E-mail küldés nincs konfigurálva, és az értesítések ki vannak kapcsolva.\nÃllítsd be az SMTP szervert a config/email.yml fájlban és indítsd újra az alkalmazást, hogy érvénybe lépjen."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/it.yml b/groups/lang/it.yml
index 3d1dea09e..d123e913a 100644
--- a/groups/lang/it.yml
+++ b/groups/lang/it.yml
@@ -20,24 +20,24 @@ actionview_datehelper_time_in_words_second_less_than_plural: meno di %d secondi
actionview_instancetag_blank_option: Scegli
activerecord_error_inclusion: non è incluso nella lista
-activerecord_error_exclusion: e' riservato
-activerecord_error_invalid: non e' valido
+activerecord_error_exclusion: è riservato
+activerecord_error_invalid: non è valido
activerecord_error_confirmation: non coincide con la conferma
activerecord_error_accepted: deve essere accettato
activerecord_error_empty: non puo' essere vuoto
activerecord_error_blank: non puo' essere blank
-activerecord_error_too_long: e' troppo lungo/a
-activerecord_error_too_short: e' troppo corto/a
-activerecord_error_wrong_length: e' della lunghezza sbagliata
-activerecord_error_taken: e' gia' stato/a preso/a
-activerecord_error_not_a_number: non e' un numero
-activerecord_error_not_a_date: non e' una data valida
+activerecord_error_too_long: è troppo lungo/a
+activerecord_error_too_short: è troppo corto/a
+activerecord_error_wrong_length: è della lunghezza sbagliata
+activerecord_error_taken: è già stato/a preso/a
+activerecord_error_not_a_number: non è un numero
+activerecord_error_not_a_date: non è una data valida
activerecord_error_greater_than_start_date: deve essere maggiore della data di partenza
-activerecord_error_not_same_project: doesn't belong to the same project
-activerecord_error_circular_dependency: This relation would create a circular dependency
+activerecord_error_not_same_project: non appartiene allo stesso progetto
+activerecord_error_circular_dependency: Questa relazione creerebbe una dipendenza circolare
-general_fmt_age: %d yr
-general_fmt_age_plural: %d yrs
+general_fmt_age: %d anno
+general_fmt_age_plural: %d anni
general_fmt_date: %%d/%%m/%%Y
general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p
general_fmt_datetime_short: %%b %%d, %%I:%%M %%p
@@ -48,6 +48,7 @@ general_text_no: 'no'
general_text_yes: 'si'
general_lang_name: 'Italiano'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Lunedì,Martedì,Mercoledì,Giovedì,Venerdì,Sabato,Domenica
@@ -68,13 +69,13 @@ notice_successful_delete: Eliminazione effettuata.
notice_successful_connection: Connessione effettuata.
notice_file_not_found: La pagina desiderata non esiste o è stata rimossa.
notice_locking_conflict: Le informazioni sono state modificate da un altro utente.
-notice_not_authorized: You are not authorized to access this page.
-notice_email_sent: An email was sent to %s
-notice_email_error: An error occurred while sending mail (%s)
-notice_feeds_access_key_reseted: Your RSS access key was reseted.
+notice_not_authorized: Non sei autorizzato ad accedere a questa pagina.
+notice_email_sent: Una e-mail è stata spedita a %s
+notice_email_error: Si è verificato un errore durante l'invio di una e-mail (%s)
+notice_feeds_access_key_reseted: La tua chiave di accesso RSS è stata reimpostata.
error_scm_not_found: "La risorsa e/o la versione non esistono nel repository."
-error_scm_command_failed: "An error occurred when trying to access the repository: %s"
+error_scm_command_failed: "Si è verificato un errore durante l'accesso al repository: %s"
mail_subject_lost_password: Password %s
mail_body_lost_password: 'Per cambiare la password, usate il seguente collegamento:'
@@ -110,21 +111,21 @@ field_project: Progetto
field_issue: Issue
field_status: Stato
field_notes: Note
-field_is_closed: Chiude il contesto
+field_is_closed: Chiude la segnalazione
field_is_default: Stato predefinito
field_tracker: Tracker
field_subject: Oggetto
field_due_date: Data ultima
field_assigned_to: Assegnato a
field_priority: Priorita'
-field_fixed_version: Target version
+field_fixed_version: Versione prevista
field_user: Utente
field_role: Ruolo
field_homepage: Homepage
field_is_public: Pubblico
field_parent: Sottoprogetto di
-field_is_in_chlog: Contesti mostrati nel changelog
-field_is_in_roadmap: Contesti mostrati nel roadmap
+field_is_in_chlog: Segnalazioni mostrate nel changelog
+field_is_in_roadmap: Segnalazioni mostrate nel roadmap
field_login: Login
field_mail_notification: Notifiche via e-mail
field_admin: Amministratore
@@ -153,16 +154,16 @@ field_comments: Commento
field_url: URL
field_start_page: Pagina principale
field_subproject: Sottoprogetto
-field_hours: Hours
-field_activity: Activity
+field_hours: Ore
+field_activity: Attività
field_spent_on: Data
-field_identifier: Identifier
-field_is_filter: Used as a filter
-field_issue_to_id: Related issue
-field_delay: Delay
-field_assignable: Issues can be assigned to this role
-field_redirect_existing_links: Redirect existing links
-field_estimated_hours: Estimated time
+field_identifier: Identificativo
+field_is_filter: Usato come filtro
+field_issue_to_id: Segnalazioni correlate
+field_delay: Ritardo
+field_assignable: E' possibile assegnare segnalazioni a questo ruolo
+field_redirect_existing_links: Redirige i collegamenti esistenti
+field_estimated_hours: Tempo stimato
field_default_value: Stato predefinito
setting_app_title: Titolo applicazione
@@ -172,7 +173,7 @@ setting_default_language: Lingua di default
setting_login_required: Autenticazione richiesta
setting_self_registration: Auto-registrazione abilitata
setting_attachment_max_size: Massima dimensione allegati
-setting_issues_export_limit: Limite esportazione contesti
+setting_issues_export_limit: Limite esportazione segnalazioni
setting_mail_from: Indirizzo sorgente e-mail
setting_host_name: Nome host
setting_text_formatting: Formattazione testo
@@ -182,9 +183,9 @@ setting_autofetch_changesets: Acquisisci automaticamente le commit
setting_sys_api_enabled: Abilita WS per la gestione del repository
setting_commit_ref_keywords: Referencing keywords
setting_commit_fix_keywords: Fixing keywords
-setting_autologin: Autologin
-setting_date_format: Date format
-setting_cross_project_issue_relations: Allow cross-project issue relations
+setting_autologin: Login automatico
+setting_date_format: Formato data
+setting_cross_project_issue_relations: Consenti la creazione di relazioni tra segnalazioni in progetti differenti
label_user: Utente
label_user_plural: Utenti
@@ -192,12 +193,12 @@ label_user_new: Nuovo utente
label_project: Progetto
label_project_new: Nuovo progetto
label_project_plural: Progetti
-label_project_all: All Projects
+label_project_all: Tutti i progetti
label_project_latest: Ultimi progetti registrati
-label_issue: Contesto
-label_issue_new: Nuovo contesto
-label_issue_plural: Contesti
-label_issue_view_all: Mostra tutti i contesti
+label_issue: Segnalazione
+label_issue_new: Nuova segnalazione
+label_issue_plural: Segnalazioni
+label_issue_view_all: Mostra tutte le segnalazioni
label_document: Documento
label_document_new: Nuovo documento
label_document_plural: Documenti
@@ -212,11 +213,11 @@ label_tracker: Tracker
label_tracker_plural: Tracker
label_tracker_new: Nuovo tracker
label_workflow: Workflow
-label_issue_status: Stato contesti
-label_issue_status_plural: Stati contesto
+label_issue_status: Stato segnalazioni
+label_issue_status_plural: Stati segnalazione
label_issue_status_new: Nuovo stato
-label_issue_category: Categorie contesti
-label_issue_category_plural: Categorie contesto
+label_issue_category: Categorie segnalazioni
+label_issue_category_plural: Categorie segnalazioni
label_issue_category_new: Nuova categoria
label_custom_field: Campo personalizzato
label_custom_field_plural: Campi personalizzati
@@ -236,8 +237,8 @@ label_administration: Amministrazione
label_login: Login
label_logout: Logout
label_help: Aiuto
-label_reported_issues: Contesti segnalati
-label_assigned_to_me_issues: I miei contesti
+label_reported_issues: Segnalazioni
+label_assigned_to_me_issues: Le mie segnalazioni
label_last_login: Ultimo collegamento
label_last_updates: Ultimo aggiornamento
label_last_updates_plural: %d ultimo aggiornamento
@@ -276,7 +277,7 @@ label_news_new: Aggiungi notizia
label_news_plural: Notizie
label_news_latest: Utime notizie
label_news_view_all: Tutte le notizie
-label_change_log: Change log
+label_change_log: Elenco modifiche
label_settings: Impostazioni
label_overview: Panoramica
label_version: Versione
@@ -314,7 +315,7 @@ label_comment_plural: Commenti
label_comment_add: Aggiungi un commento
label_comment_added: Commento aggiunto
label_comment_delete: Elimina commenti
-label_query: Custom query
+label_query: Query personalizzata
label_query_plural: Query personalizzate
label_query_new: Nuova query
label_filter_add: Aggiungi filtro
@@ -325,7 +326,7 @@ label_in_less_than: è minore di
label_in_more_than: è maggiore di
label_in: in
label_today: oggi
-label_this_week: this week
+label_this_week: questa settimana
label_less_than_ago: meno di giorni fa
label_more_than_ago: più di giorni fa
label_ago: giorni fa
@@ -333,7 +334,7 @@ label_contains: contiene
label_not_contains: non contiene
label_day_plural: giorni
label_repository: Repository
-label_browse: Browse
+label_browse: Sfoglia
label_modification: %d modifica
label_modification_plural: %d modifiche
label_revision: Versione
@@ -353,7 +354,7 @@ label_sort_lowest: Sposta in fondo
label_roadmap: Roadmap
label_roadmap_due_in: Da ultimare in
label_roadmap_overdue: %s late
-label_roadmap_no_issues: Nessun contesto per questa versione
+label_roadmap_no_issues: Nessuna segnalazione per questa versione
label_search: Ricerca
label_result_plural: Risultati
label_all_words: Tutte le parole
@@ -368,7 +369,7 @@ label_current_version: Versione corrente
label_preview: Anteprima
label_feed_plural: Feed
label_changes_details: Particolari di tutti i cambiamenti
-label_issue_tracking: tracking dei contesti
+label_issue_tracking: tracking delle segnalazioni
label_spent_time: Tempo impiegato
label_f_hour: %.2f ora
label_f_hour_plural: %.2f ore
@@ -378,53 +379,53 @@ label_statistics: Statistiche
label_commits_per_month: Commit per mese
label_commits_per_author: Commit per autore
label_view_diff: mostra differenze
-label_diff_inline: inline
-label_diff_side_by_side: side by side
+label_diff_inline: in linea
+label_diff_side_by_side: fianco a fianco
label_options: Opzioni
label_copy_workflow_from: Copia workflow da
label_permissions_report: Report permessi
-label_watched_issues: Watched issues
-label_related_issues: Related issues
-label_applied_status: Applied status
-label_loading: Loading...
-label_relation_new: New relation
-label_relation_delete: Delete relation
-label_relates_to: related to
-label_duplicates: duplicates
-label_blocks: blocks
-label_blocked_by: blocked by
-label_precedes: precedes
-label_follows: follows
+label_watched_issues: Segnalazioni osservate
+label_related_issues: Segnalazioni correlate
+label_applied_status: Stato applicato
+label_loading: Caricamento...
+label_relation_new: Nuova relazione
+label_relation_delete: Elimina relazione
+label_relates_to: correlato a
+label_duplicates: duplicati
+label_blocks: blocchi
+label_blocked_by: bloccato da
+label_precedes: precede
+label_follows: segue
label_end_to_start: end to start
label_end_to_end: end to end
label_start_to_start: start to start
label_start_to_end: start to end
-label_stay_logged_in: Stay logged in
-label_disabled: disabled
-label_show_completed_versions: Show completed versions
-label_me: me
+label_stay_logged_in: Rimani collegato
+label_disabled: disabilitato
+label_show_completed_versions: Mostra versioni completate
+label_me: io
label_board: Forum
-label_board_new: New forum
-label_board_plural: Forums
-label_topic_plural: Topics
-label_message_plural: Messages
-label_message_last: Last message
-label_message_new: New message
-label_reply_plural: Replies
-label_send_information: Send account information to the user
-label_year: Year
-label_month: Month
-label_week: Week
-label_date_from: From
-label_date_to: To
-label_language_based: Language based
-label_sort_by: Sort by %s
-label_send_test_email: Send a test email
-label_feeds_access_key_created_on: RSS access key created %s ago
-label_module_plural: Modules
-label_added_time_by: Added by %s %s ago
-label_updated_time: Updated %s ago
-label_jump_to_a_project: Jump to a project...
+label_board_new: Nuovo forum
+label_board_plural: Forum
+label_topic_plural: Argomenti
+label_message_plural: Messaggi
+label_message_last: Ultimo messaggio
+label_message_new: Nuovo messaggio
+label_reply_plural: Risposte
+label_send_information: Invia all'utente le informazioni relative all'account
+label_year: Anno
+label_month: Mese
+label_week: Settimana
+label_date_from: Da
+label_date_to: A
+label_language_based: Basato sul linguaggio
+label_sort_by: Ordina per %s
+label_send_test_email: Invia una e-mail di test
+label_feeds_access_key_created_on: chiave di accesso RSS creata %s fa
+label_module_plural: Moduli
+label_added_time_by: Aggiunto da %s %s fa
+label_updated_time: Aggiornato %s fa
+label_jump_to_a_project: Vai al progetto...
button_login: Login
button_submit: Invia
@@ -451,13 +452,13 @@ button_activate: Attiva
button_sort: Ordina
button_log_time: Registra tempo
button_rollback: Ripristina questa versione
-button_watch: Watch
-button_unwatch: Unwatch
-button_reply: Reply
-button_archive: Archive
-button_unarchive: Unarchive
+button_watch: Osserva
+button_unwatch: Dimentica
+button_reply: Rispondi
+button_archive: Archivia
+button_unarchive: Ripristina
button_reset: Reset
-button_rename: Rename
+button_rename: Rinomina
status_active: attivo
status_registered: registrato
@@ -475,32 +476,32 @@ text_journal_deleted: cancellato
text_tip_task_begin_day: attività che iniziano in questa giornata
text_tip_task_end_day: attività che terminano in questa giornata
text_tip_task_begin_end_day: attività che iniziano e terminano in questa giornata
-text_project_identifier_info: 'Lower case letters (a-z), numbers and dashes allowed.<br />Once saved, the identifier can not be changed.'
+text_project_identifier_info: "Lettere minuscole (a-z), numeri e trattini permessi.<br />Una volta salvato, l'identificativo non può essere modificato."
text_caracters_maximum: massimo %d caratteri.
text_length_between: Lunghezza compresa tra %d e %d caratteri.
text_tracker_no_workflow: Nessun workflow definito per questo tracker
-text_unallowed_characters: Unallowed characters
-text_comma_separated: Multiple values allowed (comma separated).
+text_unallowed_characters: Caratteri non permessi
+text_comma_separated: Valori multipli permessi (separati da virgola).
text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
text_issue_added: "E' stata segnalata l'anomalia %s da %s."
text_issue_updated: "L'anomalia %s e' stata aggiornata da %s."
text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
-text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ?
-text_issue_category_destroy_assignments: Remove category assignments
-text_issue_category_reassign_to: Reassing issues to this category
+text_issue_category_destroy_question: Alcune segnalazioni (%d) risultano assegnate a questa categoria. Cosa vuoi fare ?
+text_issue_category_destroy_assignments: Rimuovi gli assegnamenti a questa categoria
+text_issue_category_reassign_to: Riassegna segnalazioni a questa categoria
default_role_manager: Manager
default_role_developper: Sviluppatore
default_role_reporter: Reporter
-default_tracker_bug: Contesto
+default_tracker_bug: Segnalazione
default_tracker_feature: Funzione
default_tracker_support: Supporto
-default_issue_status_new: Nuovo/a
-default_issue_status_assigned: Assegnato/a
-default_issue_status_resolved: Risolto/a
+default_issue_status_new: Nuovo
+default_issue_status_assigned: Assegnato
+default_issue_status_resolved: Risolto
default_issue_status_feedback: Feedback
-default_issue_status_closed: Chiuso/a
-default_issue_status_rejected: Rifiutato/a
+default_issue_status_closed: Chiuso
+default_issue_status_rejected: Rifiutato
default_doc_category_user: Documentazione utente
default_doc_category_tech: Documentazione tecnica
default_priority_low: Bassa
@@ -508,113 +509,130 @@ default_priority_normal: Normale
default_priority_high: Alta
default_priority_urgent: Urgente
default_priority_immediate: Immediata
-default_activity_design: Design
-default_activity_development: Development
+default_activity_design: Progettazione
+default_activity_development: Sviluppo
-enumeration_issue_priorities: Priorità contesti
+enumeration_issue_priorities: Priorità segnalazioni
enumeration_doc_categories: Categorie di documenti
enumeration_activities: Attività (time tracking)
-label_file_plural: Files
-label_changeset_plural: Changesets
-field_column_names: Columns
-label_default_columns: Default columns
-setting_issue_list_default_columns: Default columns displayed on the issue list
-setting_repositories_encodings: Repositories encodings
-notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
-label_bulk_edit_selected_issues: Bulk edit selected issues
-label_no_change_option: (No change)
-notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
-label_theme: Theme
-label_default: Default
-label_search_titles_only: Search titles only
-label_nobody: nobody
-button_change_password: Change password
+label_file_plural: File
+label_changeset_plural: Changeset
+field_column_names: Colonne
+label_default_columns: Colonne predefinite
+setting_issue_list_default_columns: Colonne predefinite mostrate nell'elenco segnalazioni
+setting_repositories_encodings: Codifiche dei repository
+notice_no_issue_selected: "Nessuna segnalazione selezionata! Seleziona le segnalazioni che intendi modificare."
+label_bulk_edit_selected_issues: Modifica massiva delle segnalazioni selezionate
+label_no_change_option: (Nessuna modifica)
+notice_failed_to_save_issues: "Impossibile salvare %d segnalazioni su %d selezionate: %s."
+label_theme: Tema
+label_default: Predefinito
+label_search_titles_only: Cerca solo nei titoli
+label_nobody: nessuno
+button_change_password: Modifica password
text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
-label_user_mail_option_selected: "For any event on the selected projects only..."
-label_user_mail_option_all: "For any event on all my projects"
-label_user_mail_option_none: "Only for things I watch or I'm involved in"
-setting_emails_footer: Emails footer
+label_user_mail_option_selected: "Solo per gli eventi relativi ai progetti selezionati..."
+label_user_mail_option_all: "Per ogni evento relativo ad uno dei miei progetti"
+label_user_mail_option_none: "Solo per argomenti che osservo o che mi riguardano"
+setting_emails_footer: Piè di pagina e-mail
label_float: Float
-button_copy: Copy
-mail_body_account_information_external: You can use your "%s" account to log in.
-mail_body_account_information: Your account information
-setting_protocol: Protocol
-label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
-setting_time_format: Time format
-label_registration_activation_by_email: account activation by email
-mail_subject_account_activation_request: %s account activation request
-mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:'
-label_registration_automatic_activation: automatic account activation
-label_registration_manual_activation: manual account activation
-notice_account_pending: "Your account was created and is now pending administrator approval."
+button_copy: Copia
+mail_body_account_information_external: Puoi utilizzare il tuo account "%s" per accedere al sistema.
+mail_body_account_information: Le informazioni riguardanti il tuo account
+setting_protocol: Protocollo
+label_user_mail_no_self_notified: "Non voglio notifiche riguardanti modifiche da me apportate"
+setting_time_format: Formato ora
+label_registration_activation_by_email: attivazione account via e-mail
+mail_subject_account_activation_request: %s richiesta attivazione account
+mail_body_account_activation_request: 'Un nuovo utente (%s) ha effettuato la registrazione. Il suo account è in attesa di abilitazione da parte tua:'
+label_registration_automatic_activation: attivazione account automatica
+label_registration_manual_activation: attivazione account manuale
+notice_account_pending: "Il tuo account è stato creato ed è in attesa di attivazione da parte dell'amministratore."
field_time_zone: Time zone
-text_caracters_minimum: Must be at least %d characters long.
-setting_bcc_recipients: Blind carbon copy recipients (bcc)
-button_annotate: Annotate
-label_issues_by: Issues by %s
-field_searchable: Searchable
-label_display_per_page: 'Per page: %s'
-setting_per_page_options: Objects per page options
-label_age: Age
-notice_default_data_loaded: Default configuration successfully loaded.
-text_load_default_configuration: Load the default configuration
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
-error_can_t_load_default_data: "Default configuration could not be loaded: %s"
-button_update: Update
-label_change_properties: Change properties
-label_general: General
-label_repository_plural: Repositories
-label_associated_revisions: Associated revisions
-setting_user_format: Users display format
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+text_caracters_minimum: Deve essere lungo almeno %d caratteri.
+setting_bcc_recipients: Destinatari in copia nascosta (bcc)
+button_annotate: Annota
+label_issues_by: Segnalazioni di %s
+field_searchable: Ricercabile
+label_display_per_page: 'Per pagina: %s'
+setting_per_page_options: Opzioni oggetti per pagina
+label_age: Età
+notice_default_data_loaded: Configurazione di default caricata con successo.
+text_load_default_configuration: Carica la configurazione di default
+text_no_configuration_data: "Ruoli, tracker, stati delle segnalazioni e workflow non sono stati ancora configurati.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+error_can_t_load_default_data: "Non è stato possibile caricare la configurazione di default : %s"
+button_update: Aggiorna
+label_change_properties: Modifica le proprietà
+label_general: Generale
+label_repository_plural: Repository
+label_associated_revisions: Revisioni associate
+setting_user_format: Formato visualizzazione utenti
+text_status_changed_by_changeset: Applicata nel changeset %s.
+label_more: Altro
+text_issues_destroy_confirmation: 'Sei sicuro di voler eliminare le segnalazioni selezionate?'
label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
+text_select_project_modules: 'Seleziona i moduli abilitati per questo progetto:'
+label_issue_added: Segnalazioni aggiunte
+label_issue_updated: Segnalazioni aggiornate
+label_document_added: Documenti aggiunti
+label_message_posted: Messaggi aggiunti
+label_file_added: File aggiunti
+label_news_added: Notizie aggiunte
project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+project_module_issue_tracking: Tracking delle segnalazioni
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
+project_module_files: File
+project_module_documents: Documenti
project_module_repository: Repository
-project_module_news: News
+project_module_news: Notizie
project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
-label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+text_file_repository_writable: Repository dei file scrivibile
+text_default_administrator_account_changed: L'account amministrativo di default è stato modificato
+text_rmagick_available: RMagick disponibile (opzionale)
+button_configure: Configura
+label_plugins: Plugin
+label_ldap_authentication: Autenticazione LDAP
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_this_month: questo mese
+label_last_n_days: ultimi %d giorni
+label_all_time: sempre
+label_this_year: quest'anno
+label_date_range: Intervallo di date
+label_last_week: ultima settimana
+label_yesterday: ieri
+label_last_month: ultimo mese
+label_add_another_file: Aggiungi un altro file
+label_optional_description: Descrizione opzionale
+text_destroy_time_entries_question: %.02f ore risultano spese sulle segnalazioni che stai per cancellare. Cosa vuoi fare ?
+error_issue_not_found_in_project: 'La segnalazione non è stata trovata o non appartiene al progetto'
+text_assign_time_entries_to_project: Assegna le ore segnalate al progetto
+text_destroy_time_entries: Elimina le ore segnalate
+text_reassign_time_entries: 'Riassegna le ore a questa segnalazione:'
+setting_activity_days_default: Giorni mostrati sulle attività di progetto
+label_chronological_order: In ordine cronologico
+field_comments_sorting: Mostra commenti
+label_reverse_chronological_order: In ordine cronologico inverso
+label_preferences: Preferenze
+setting_display_subprojects_issues: Mostra le segnalazioni dei sottoprogetti nel progetto principale per default
+label_overall_activity: Attività generale
+setting_default_projects_public: I nuovi progetti sono pubblici per default
+error_scm_annotate: "L'oggetto non esiste o non può essere annotato."
+label_planning: Pianificazione
+text_subprojects_destroy_warning: 'Anche i suoi sottoprogetti: %s verranno eliminati.'
+label_and_its_subprojects: %s ed i suoi sottoprogetti
+mail_body_reminder: "%d segnalazioni che ti sono state assegnate scadranno nei prossimi %d giorni:"
+mail_subject_reminder: "%d segnalazioni in scadenza nei prossimi giorni"
+text_user_wrote: '%s ha scritto:'
+label_duplicated_by: duplicato da
+setting_enabled_scm: SCM abilitato
+text_enumeration_category_reassign_to: 'Riassegnale a questo valore:'
+text_enumeration_destroy_question: '%d oggetti hanno un assegnamento su questo valore.'
+label_incoming_emails: E-mail in arrivo
+label_generate_key: Genera una chiave
+setting_mail_handler_api_enabled: Abilita WS per le e-mail in arrivo
+setting_mail_handler_api_key: chiave API
+text_email_delivery_not_configured: "La consegna via e-mail non è configurata e le notifiche sono disabilitate.\nConfigura il tuo server SMTP in config/email.yml e riavvia l'applicazione per abilitarle."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/ja.yml b/groups/lang/ja.yml
index 680d29836..5a728fb02 100644
--- a/groups/lang/ja.yml
+++ b/groups/lang/ja.yml
@@ -49,6 +49,7 @@ general_text_no: 'ã„ã„ãˆ'
general_text_yes: 'ã¯ã„'
general_lang_name: 'Japanese (日本語)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: SJIS
general_pdf_encoding: UTF-8
general_day_names: 月曜日,ç«æ›œæ—¥,水曜日,木曜日,金曜日,土曜日,日曜日
@@ -619,3 +620,20 @@ setting_default_projects_public: ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§æ–°ã—ã„プロジェクトã
error_scm_annotate: "エントリãŒå­˜åœ¨ã—ãªã„ã€ã‚‚ã—ãã¯ã‚¢ãƒŽãƒ†ãƒ¼ãƒˆã§ãã¾ã›ã‚“。"
label_planning: 計画
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/ko.yml b/groups/lang/ko.yml
index 4281f3881..16bd65364 100644
--- a/groups/lang/ko.yml
+++ b/groups/lang/ko.yml
@@ -48,6 +48,7 @@ general_text_no: '아니오'
general_text_yes: '예'
general_lang_name: 'Korean (한국어)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: CP949
general_pdf_encoding: CP949
general_day_names: 월요ì¼,화요ì¼,수요ì¼,목요ì¼,금요ì¼,토요ì¼,ì¼ìš”ì¼
@@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/lt.yml b/groups/lang/lt.yml
index df7cd960b..2a75a95ea 100644
--- a/groups/lang/lt.yml
+++ b/groups/lang/lt.yml
@@ -48,6 +48,7 @@ general_text_no: 'ne'
general_text_yes: 'taip'
general_lang_name: 'Lithuanian (lietuvių)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: UTF-8
general_pdf_encoding: UTF-8
general_day_names: pirmadienis,antradienis,treÄiadienis,ketvirtadienis,penktadienis,Å¡eÅ¡tadienis,sekmadienis
@@ -554,7 +555,7 @@ enumeration_issue_priorities: Darbo prioritetai
enumeration_doc_categories: Dokumento kategorijos
enumeration_activities: Veiklos (laiko sekimas)
label_display_per_page: '%s įrašų puslapyje'
-setting_per_page_options: Objects per page options
+setting_per_page_options: Įrašų puslapyje nustatimas
notice_default_data_loaded: Numatytoji konfiguracija sėkmingai užkrauta.
label_age: Amžius
label_general: Bendri
@@ -578,44 +579,63 @@ label_document_added: Dokumentas pridÄ—tas
label_message_posted: Pranešimas pridėtas
label_file_added: Byla pridÄ—ta
label_news_added: Naujiena pridÄ—ta
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+project_module_boards: Forumai
+project_module_issue_tracking: Darbu pÄ—dsekys
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
+project_module_files: Rinkmenos
+project_module_documents: Dokumentai
+project_module_repository: Saugykla
+project_module_news: Žinios
+project_module_time_tracking: Laiko pÄ—dsekys
+text_file_repository_writable: Ä® rinkmenu saugyklÄ… galima saugoti (RW)
+text_default_administrator_account_changed: Administratoriaus numatyta paskyra pakeista
+text_rmagick_available: RMagick pasiekiamas (pasirinktinai)
+button_configure: Konfiguruoti
label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
-label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_ldap_authentication: LDAP autentifikacija
+label_downloads_abbr: siunt.
+label_this_month: šis menuo
+label_last_n_days: paskutinių %d dienų
+label_all_time: visas laikas
+label_this_year: šiemet
+label_date_range: Dienų diapazonas
+label_last_week: paskutinÄ— savaitÄ—
+label_yesterday: vakar
+label_last_month: paskutinis menuo
+label_add_another_file: PridÄ—ti kitÄ… bylÄ…
+label_optional_description: Apibūdinimas (laisvai pasirenkamas)
+text_destroy_time_entries_question: Naikinamam darbui paskelbta %.02f valandų. Ką jūs noryte su jomis daryti?
+error_issue_not_found_in_project: 'Darbas nerastas arba nesurištas su šiuo projektu'
+text_assign_time_entries_to_project: Priskirti valandas prie projekto
+text_destroy_time_entries: Ištrinti paskelbtas valandas
+text_reassign_time_entries: 'Priskirti paskelbtas valandas šiam darbui:'
+setting_activity_days_default: Atvaizduojamos dienos projekto veikloje
+label_chronological_order: Chronologine tvarka
+field_comments_sorting: rodyti komentarus
+label_reverse_chronological_order: Atbuline chronologine tvarka
+label_preferences: SavybÄ—s
+setting_display_subprojects_issues: Pagal nutylėjimą rodyti subprojektų darbus pagrindiniame projekte
+label_overall_activity: Visa veikla
+setting_default_projects_public: Naujas projektas viešas pagal nutylėjimą
+error_scm_annotate: "Įrašas neegzituoja arba negalima jo atvaizduoti."
+label_planning: Planavimas
+text_subprojects_destroy_warning: 'Šis(ie) subprojektas(ai): %s taip pat bus ištrintas(i).'
+label_and_its_subprojects: %s projektas ir jo subprojektai
+
+mail_body_reminder: "%d darbas(ai), kurie yra jums priskirti, baigiasi po %d dienų(os):"
+mail_subject_reminder: "%d darbas(ai) po kelių dienų"
+text_user_wrote: '%s parašė:'
+label_duplicated_by: susiejo
+setting_enabled_scm: Įgalintas SCM
+text_enumeration_category_reassign_to: 'Priskirti juos šiai reikšmei:'
+text_enumeration_destroy_question: '%d objektai priskirti šiai reikšmei.'
+label_incoming_emails: Įeinantys laiškai
+label_generate_key: Generuoti raktÄ…
+setting_mail_handler_api_enabled: Įgalinti WS įeinantiems laiškams
+setting_mail_handler_api_key: API raktas
+
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/nl.yml b/groups/lang/nl.yml
index e487a7a6d..f79e78994 100644
--- a/groups/lang/nl.yml
+++ b/groups/lang/nl.yml
@@ -48,6 +48,7 @@ general_text_no: 'nee'
general_text_yes: 'ja'
general_lang_name: 'Nederlands'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag, Zondag
@@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/no.yml b/groups/lang/no.yml
index 22b8c10af..6643f9c86 100644
--- a/groups/lang/no.yml
+++ b/groups/lang/no.yml
@@ -48,6 +48,7 @@ general_text_no: 'nei'
general_text_yes: 'ja'
general_lang_name: 'Norwegian (Norsk bokmål)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Mandag,Tirsdag,Onsdag,Torsdag,Fredag,Lørdag,Søndag
@@ -91,6 +92,8 @@ mail_body_account_information_external: Du kan bruke din "%s"-konto for å logge
mail_body_account_information: Informasjon om din konto
mail_subject_account_activation_request: %s kontoaktivering
mail_body_account_activation_request: 'En ny bruker (%s) er registrert, og avventer din godkjenning:'
+mail_subject_reminder: "%d sak(er) har frist de kommende dagene"
+mail_body_reminder: "%d sak(er) som er tildelt deg har frist de kommende %d dager:"
gui_validation_error: 1 feil
gui_validation_error_plural: %d feil
@@ -211,6 +214,7 @@ setting_per_page_options: Alternativer, objekter pr. side
setting_user_format: Visningsformat, brukere
setting_activity_days_default: Dager vist på prosjektaktivitet
setting_display_subprojects_issues: Vis saker fra underprosjekter på hovedprosjekt som standard
+setting_enabled_scm: Aktiviserte SCM
project_module_issue_tracking: Sakssporing
project_module_time_tracking: Tidssporing
@@ -291,6 +295,7 @@ label_auth_source: Autentifikasjonsmodus
label_auth_source_new: Ny autentifikasjonmodus
label_auth_source_plural: Autentifikasjonsmoduser
label_subproject_plural: Underprosjekter
+label_and_its_subprojects: %s og dets underprosjekter
label_min_max_length: Min.-maks. lengde
label_list: Liste
label_date: Dato
@@ -444,7 +449,8 @@ label_loading: Laster...
label_relation_new: Ny relasjon
label_relation_delete: Slett relasjon
label_relates_to: relatert til
-label_duplicates: duplikater
+label_duplicates: dupliserer
+label_duplicated_by: duplisert av
label_blocks: blokkerer
label_blocked_by: blokkert av
label_precedes: kommer før
@@ -557,7 +563,7 @@ text_select_mail_notifications: Velg hendelser som skal varsles med e-post.
text_regexp_info: eg. ^[A-Z0-9]+$
text_min_max_length_info: 0 betyr ingen begrensning
text_project_destroy_confirmation: Er du sikker på at du vil slette dette prosjekter og alle relatert data ?
-text_subprojects_destroy_warning: 'Underprojekt(ene): %s vil også bli slettet.'
+text_subprojects_destroy_warning: 'Underprojekt(ene): %s vil også bli slettet.'
text_workflow_edit: Velg en rolle og en sakstype for å endre arbeidsflyten
text_are_you_sure: Er du sikker ?
text_journal_changed: endret fra %s til %s
@@ -593,6 +599,7 @@ text_destroy_time_entries_question: %.02f timer er ført på sakene du er i ferd
text_destroy_time_entries: Slett førte timer
text_assign_time_entries_to_project: Overfør førte timer til prosjektet
text_reassign_time_entries: 'Overfør førte timer til denne saken:'
+text_user_wrote: '%s skrev:'
default_role_manager: Leder
default_role_developper: Utvikler
@@ -619,3 +626,14 @@ default_activity_development: Utvikling
enumeration_issue_priorities: Sakssprioriteringer
enumeration_doc_categories: Dokument-kategorier
enumeration_activities: Aktiviteter (tidssporing)
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/pl.yml b/groups/lang/pl.yml
index 81f03a62f..2df921b71 100644
--- a/groups/lang/pl.yml
+++ b/groups/lang/pl.yml
@@ -48,6 +48,7 @@ general_text_no: 'nie'
general_text_yes: 'tak'
general_lang_name: 'Polski'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-2
general_pdf_encoding: ISO-8859-2
general_day_names: Poniedziałek,Wtorek,Środa,Czwartek,Piątek,Sobota,Niedziela
@@ -70,7 +71,7 @@ notice_file_not_found: Strona do której próbujesz się dostać nie istnieje lu
notice_locking_conflict: Dane poprawione przez innego użytkownika.
notice_not_authorized: Nie jesteś autoryzowany by zobaczyć stronę.
-error_scm_not_found: "Wejście i/lub zmiana nie istnieje w repozytorium."
+error_scm_not_found: "Obiekt lub wersja nie zostały znalezione w repozytorium."
error_scm_command_failed: "An error occurred when trying to access the repository: %s"
mail_subject_lost_password: Twoje hasło do %s
@@ -114,12 +115,12 @@ field_subject: Temat
field_due_date: Data oddania
field_assigned_to: Przydzielony do
field_priority: Priorytet
-field_fixed_version: Target version
+field_fixed_version: Wersja docelowa
field_user: Użytkownik
field_role: Rola
field_homepage: Strona www
field_is_public: Publiczny
-field_parent: Podprojekt
+field_parent: Nadprojekt
field_is_in_chlog: Zagadnienie pokazywane w zapisie zmian
field_is_in_roadmap: Zagadnienie pokazywane na mapie
field_login: Login
@@ -172,10 +173,10 @@ setting_host_name: Nazwa hosta
setting_text_formatting: Formatowanie tekstu
setting_wiki_compression: Kompresja historii Wiki
setting_feeds_limit: Limit danych RSS
-setting_autofetch_changesets: Auto-odświeżanie CVS
+setting_autofetch_changesets: Automatyczne pobieranie zmian
setting_sys_api_enabled: Włączenie WS do zarządzania repozytorium
-setting_commit_ref_keywords: Terminy odnoszÄ…ce (CVS)
-setting_commit_fix_keywords: Terminy ustalajÄ…ce (CVS)
+setting_commit_ref_keywords: Słowa tworzące powiązania
+setting_commit_fix_keywords: Słowa zmieniające status
setting_autologin: Auto logowanie
setting_date_format: Format daty
@@ -328,14 +329,14 @@ label_repository: Repozytorium
label_browse: PrzeglÄ…d
label_modification: %d modyfikacja
label_modification_plural: %d modyfikacja
-label_revision: Zmiana
-label_revision_plural: Zmiany
+label_revision: Rewizja
+label_revision_plural: Rewizje
label_added: dodane
-label_modified: zmodufikowane
+label_modified: zmodyfikowane
label_deleted: usunięte
-label_latest_revision: Ostatnia zmiana
-label_latest_revision_plural: Ostatnie zmiany
-label_view_revisions: Pokaż zmiany
+label_latest_revision: Najnowsza rewizja
+label_latest_revision_plural: Najnowsze rewizje
+label_view_revisions: Pokaż rewizje
label_max_size: Maksymalny rozmiar
label_on: 'z'
label_sort_highest: Przesuń na górę
@@ -366,8 +367,8 @@ label_f_hour_plural: %.2f godzin
label_time_tracking: Åšledzenie czasu
label_change_plural: Zmiany
label_statistics: Statystyki
-label_commits_per_month: Wrzutek CVS w miesiÄ…cu
-label_commits_per_author: Wrzutek CVS przez autora
+label_commits_per_month: Zatwierdzenia według miesięcy
+label_commits_per_author: Zatwierdzenia według autorów
label_view_diff: Pokaż różnice
label_diff_inline: w linii
label_diff_side_by_side: obok siebie
@@ -463,13 +464,13 @@ text_length_between: Długość pomiędzy %d i %d znaków.
text_tracker_no_workflow: Brak przepływu zefiniowanego dla tego typu zagadnienia
text_unallowed_characters: Niedozwolone znaki
text_comma_separated: Wielokrotne wartości dozwolone (rozdzielone przecinkami).
-text_issues_ref_in_commit_messages: Zagadnienia odnoszÄ…ce i ustalajÄ…ce we wrzutkach CVS
+text_issues_ref_in_commit_messages: Odwołania do zagadnień w komentarzach zatwierdzeń
default_role_manager: Kierownik
default_role_developper: Programista
default_role_reporter: Wprowadzajacy
default_tracker_bug: Błąd
-default_tracker_feature: Cecha
+default_tracker_feature: Zadanie
default_tracker_support: Wsparcie
default_issue_status_new: Nowy
default_issue_status_assigned: Przypisany
@@ -483,7 +484,7 @@ default_priority_low: Niski
default_priority_normal: Normalny
default_priority_high: Wysoki
default_priority_urgent: Pilny
-default_priority_immediate: Natyczmiastowy
+default_priority_immediate: Natychmiastowy
default_activity_design: Projektowanie
default_activity_development: Rozwój
@@ -618,3 +619,20 @@ setting_default_projects_public: Nowe projekty są domyślnie publiczne
error_scm_annotate: "Wpis nie istnieje lub nie można do niego dodawać adnotacji."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/pt-br.yml b/groups/lang/pt-br.yml
index 9facd8d19..8cd171b72 100644
--- a/groups/lang/pt-br.yml
+++ b/groups/lang/pt-br.yml
@@ -1,140 +1,141 @@
_gloc_rule_default: '|n| n==1 ? "" : "_plural" '
actionview_datehelper_select_day_prefix:
-actionview_datehelper_select_month_names: Janeiro,Fevereiro,Marco,Abrill,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro
+actionview_datehelper_select_month_names: Janeiro,Fevereiro,Março,Abrill,Maio,Junho,Julho,Agosto,Setembro,Outubro,Novembro,Dezembro
actionview_datehelper_select_month_names_abbr: Jan,Fev,Mar,Abr,Mai,Jun,Jul,Ago,Set,Out,Nov,Dez
actionview_datehelper_select_month_prefix:
actionview_datehelper_select_year_prefix:
actionview_datehelper_time_in_words_day: 1 dia
actionview_datehelper_time_in_words_day_plural: %d dias
-actionview_datehelper_time_in_words_hour_about: sobre uma hora
-actionview_datehelper_time_in_words_hour_about_plural: sobra %d horas
-actionview_datehelper_time_in_words_hour_about_single: sobre uma hora
+actionview_datehelper_time_in_words_hour_about: aproximadamente uma hora
+actionview_datehelper_time_in_words_hour_about_plural: aproximadamente %d horas
+actionview_datehelper_time_in_words_hour_about_single: aproximadamente uma hora
actionview_datehelper_time_in_words_minute: 1 minuto
actionview_datehelper_time_in_words_minute_half: meio minuto
-actionview_datehelper_time_in_words_minute_less_than: menos que um minuto
+actionview_datehelper_time_in_words_minute_less_than: menos de um minuto
actionview_datehelper_time_in_words_minute_plural: %d minutos
actionview_datehelper_time_in_words_minute_single: 1 minuto
-actionview_datehelper_time_in_words_second_less_than: menos que um segundo
-actionview_datehelper_time_in_words_second_less_than_plural: menos que %d segundos
+actionview_datehelper_time_in_words_second_less_than: menos de um segundo
+actionview_datehelper_time_in_words_second_less_than_plural: menos de %d segundos
actionview_instancetag_blank_option: Selecione
-activerecord_error_inclusion: nao esta incluido na lista
-activerecord_error_exclusion: esta reservado
-activerecord_error_invalid: e invalido
-activerecord_error_confirmation: confirmacao nao confere
+activerecord_error_inclusion: não está incluso na lista
+activerecord_error_exclusion: está reservado
+activerecord_error_invalid: é inválido
+activerecord_error_confirmation: confirmação não confere
activerecord_error_accepted: deve ser aceito
-activerecord_error_empty: nao pode ser vazio
-activerecord_error_blank: nao pode estar em branco
-activerecord_error_too_long: e muito longo
-activerecord_error_too_short: e muito comprido
-activerecord_error_wrong_length: esta com o comprimento errado
-activerecord_error_taken: ja esta examinado
-activerecord_error_not_a_number: nao e um numero
-activerecord_error_not_a_date: nao e uma data valida
+activerecord_error_empty: não pode ser vazio
+activerecord_error_blank: não pode estar em branco
+activerecord_error_too_long: é muito longo
+activerecord_error_too_short: é muito curto
+activerecord_error_wrong_length: esta com o tamanho errado
+activerecord_error_taken: já foi obtido
+activerecord_error_not_a_number: não é um numero
+activerecord_error_not_a_date: não é uma data valida
activerecord_error_greater_than_start_date: deve ser maior que a data inicial
-activerecord_error_not_same_project: doesn't belong to the same project
-activerecord_error_circular_dependency: This relation would create a circular dependency
+activerecord_error_not_same_project: não pode pertencer ao mesmo projeto
+activerecord_error_circular_dependency: Esta relação geraria uma dependência circular
-general_fmt_age: %d yr
-general_fmt_age_plural: %d yrs
-general_fmt_date: %%m/%%d/%%Y
-general_fmt_datetime: %%m/%%d/%%Y %%I:%%M %%p
+general_fmt_age: %d ano
+general_fmt_age_plural: %d anos
+general_fmt_date: %%d/%%m/%%Y
+general_fmt_datetime: %%d/%%m/%%Y %%I:%%M %%p
general_fmt_datetime_short: %%b %%d, %%I:%%M %%p
general_fmt_time: %%I:%%M %%p
-general_text_No: 'Nao'
+general_text_No: 'Não'
general_text_Yes: 'Sim'
-general_text_no: 'nao'
+general_text_no: 'não'
general_text_yes: 'sim'
-general_lang_name: 'Portugues Brasileiro'
+general_lang_name: 'Português(Brasil)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
-general_day_names: Segunda,Terca,Quarta,Quinta,Sexta,Sabado,Domingo
+general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sabado,Domingo
general_first_day_of_week: '1'
notice_account_updated: Conta foi alterada com sucesso.
-notice_account_invalid_creditentials: Usuario ou senha invalido.
-notice_account_password_updated: Senha foi alterada com sucesso.
-notice_account_wrong_password: Senha errada.
-notice_account_register_done: Conta foi criada com sucesso.
-notice_account_unknown_email: Usuario desconhecido.
-notice_can_t_change_password: Esta conta usa autenticacao externa. E impossivel trocar a senha.
-notice_account_lost_email_sent: Um email com instrucoes para escolher uma nova senha foi enviado para voce.
-notice_account_activated: Sua conta foi ativada. Voce pode logar agora
+notice_account_invalid_creditentials: Usuário ou senha inválido.
+notice_account_password_updated: Senha alterada com sucesso.
+notice_account_wrong_password: Senha inválida.
+notice_account_register_done: Conta criada com sucesso.
+notice_account_unknown_email: Usuário desconhecido.
+notice_can_t_change_password: Esta conta usa autenticação externa. E impossível alterar a senha.
+notice_account_lost_email_sent: Um email com instruções para escolher uma nova senha foi enviado para você.
+notice_account_activated: Sua conta foi ativada. Você pode acessá-la agora.
notice_successful_create: Criado com sucesso.
notice_successful_update: Alterado com sucesso.
-notice_successful_delete: Apagado com sucesso.
+notice_successful_delete: Excluído com sucesso.
notice_successful_connection: Conectado com sucesso.
-notice_file_not_found: A pagina que voce esta tentando acessar nao existe ou foi excluida.
-notice_locking_conflict: Os dados foram atualizados por um outro usuario.
-notice_not_authorized: You are not authorized to access this page.
-notice_email_sent: An email was sent to %s
-notice_email_error: An error occurred while sending mail (%s)
-notice_feeds_access_key_reseted: Your RSS access key was reseted.
+notice_file_not_found: A página que você está tentando acessar não existe ou foi excluída.
+notice_locking_conflict: Os dados foram atualizados por outro usuário.
+notice_not_authorized: Você não está autorizado a acessar esta página.
+notice_email_sent: Um email foi enviado para %s
+notice_email_error: Um erro ocorreu ao enviar o email (%s)
+notice_feeds_access_key_reseted: Sua chave RSS foi reconfigurada.
-error_scm_not_found: "A entrada e/ou a revisao nao existem no repositorio."
-error_scm_command_failed: "An error occurred when trying to access the repository: %s"
+error_scm_not_found: "A entrada e/ou a revisão não existe no repositório."
+error_scm_command_failed: "Ocorreu um erro ao tentar acessar o repositório: %s"
mail_subject_lost_password: Sua senha do %s.
mail_body_lost_password: 'Para mudar sua senha, clique no link abaixo:'
-mail_subject_register: Ativacao de conta do %s.
+mail_subject_register: Ativação de conta do %s.
mail_body_register: 'Para ativar sua conta, clique no link abaixo:'
gui_validation_error: 1 erro
gui_validation_error_plural: %d erros
field_name: Nome
-field_description: Descricao
-field_summary: Sumario
-field_is_required: Obrigatorio
+field_description: Descrição
+field_summary: Resumo
+field_is_required: Obrigatório
field_firstname: Primeiro nome
-field_lastname: Ultimo nome
+field_lastname: Último nome
field_mail: Email
field_filename: Arquivo
field_filesize: Tamanho
field_downloads: Downloads
field_author: Autor
-field_created_on: Criado
-field_updated_on: Alterado
+field_created_on: Criado em
+field_updated_on: Alterado em
field_field_format: Formato
field_is_for_all: Para todos os projetos
-field_possible_values: Possiveis valores
-field_regexp: Expressao regular
-field_min_length: Tamanho minimo
-field_max_length: Tamanho maximo
+field_possible_values: Possíveis valores
+field_regexp: Expressão regular
+field_min_length: Tamanho mínimo
+field_max_length: Tamanho máximo
field_value: Valor
field_category: Categoria
-field_title: Titulo
+field_title: Título
field_project: Projeto
-field_issue: Tarefa
+field_issue: Ticket
field_status: Status
field_notes: Notas
-field_is_closed: Tarefa fechada
-field_is_default: Status padrao
+field_is_closed: Ticket fechado
+field_is_default: Status padrão
field_tracker: Tipo
-field_subject: Titulo
-field_due_date: Data devida
-field_assigned_to: Atribuido para
+field_subject: Título
+field_due_date: Data prevista
+field_assigned_to: Atribuído para
field_priority: Prioridade
-field_fixed_version: Target version
-field_user: Usuario
-field_role: Regra
-field_homepage: Pagina inicial
-field_is_public: Publico
+field_fixed_version: Versão
+field_user: Usuário
+field_role: Papel
+field_homepage: Página inicial
+field_is_public: Público
field_parent: Sub-projeto de
-field_is_in_chlog: Tarefas mostradas no changelog
-field_is_in_roadmap: Tarefas mostradas no roadmap
+field_is_in_chlog: Tarefas exibidas no registro de alterações
+field_is_in_roadmap: Tarefas exibidas no planejamento
field_login: Login
-field_mail_notification: Notificacoes por email
+field_mail_notification: Notificações por email
field_admin: Administrador
-field_last_login_on: Ultima conexao
-field_language: Lingua
+field_last_login_on: Última conexão
+field_language: Idioma
field_effective_date: Data
field_password: Senha
field_new_password: Nova senha
-field_password_confirmation: Confirmacao
-field_version: Versao
+field_password_confirmation: Confirmação
+field_version: Versão
field_type: Tipo
field_host: Servidor
field_port: Porta
@@ -142,116 +143,116 @@ field_account: Conta
field_base_dn: Base DN
field_attr_login: Atributo login
field_attr_firstname: Atributo primeiro nome
-field_attr_lastname: Atributo ultimo nome
+field_attr_lastname: Atributo último nome
field_attr_mail: Atributo email
-field_onthefly: Criacao de usuario on-the-fly
-field_start_date: Inicio
+field_onthefly: Criação automática de usuário
+field_start_date: Início
field_done_ratio: %% Terminado
-field_auth_source: Modo de autenticacao
-field_hide_mail: Esconder meu email
-field_comments: Comentario
+field_auth_source: Modo de autenticação
+field_hide_mail: Ocultar meu email
+field_comments: Comentário
field_url: URL
-field_start_page: Pagina inicial
+field_start_page: Página inicial
field_subproject: Sub-projeto
field_hours: Horas
field_activity: Atividade
field_spent_on: Data
field_identifier: Identificador
-field_is_filter: Used as a filter
-field_issue_to_id: Related issue
-field_delay: Delay
-field_assignable: Issues can be assigned to this role
-field_redirect_existing_links: Redirect existing links
-field_estimated_hours: Estimated time
-field_default_value: Padrao
+field_is_filter: É um filtro
+field_issue_to_id: Ticket relacionado
+field_delay: Espera
+field_assignable: Tickets podem ser atribuídos para este papel
+field_redirect_existing_links: Redirecionar links existentes
+field_estimated_hours: Tempo estimado
+field_default_value: Padrão
-setting_app_title: Titulo da aplicacao
-setting_app_subtitle: Sub-titulo da aplicacao
-setting_welcome_text: Texto de boa-vinda
-setting_default_language: Lingua padrao
-setting_login_required: Autenticacao obrigatoria
-setting_self_registration: Registro de si mesmo permitido
-setting_attachment_max_size: Tamanho maximo do anexo
-setting_issues_export_limit: Limite de exportacao das tarefas
+setting_app_title: Título da aplicação
+setting_app_subtitle: Sub-título da aplicação
+setting_welcome_text: Texto de boas-vindas
+setting_default_language: Idioma padrão
+setting_login_required: Autenticação obrigatória
+setting_self_registration: Permitido Auto-registro
+setting_attachment_max_size: Tamanho máximo do anexo
+setting_issues_export_limit: Limite de exportação das tarefas
setting_mail_from: Email enviado de
setting_host_name: Servidor
setting_text_formatting: Formato do texto
-setting_wiki_compression: Compactacao do historio do Wiki
+setting_wiki_compression: Compactação de histórico do Wiki
setting_feeds_limit: Limite do Feed
-setting_autofetch_changesets: Autofetch commits
-setting_sys_api_enabled: Ativa WS para gerenciamento do repositorio
-setting_commit_ref_keywords: Referencing keywords
-setting_commit_fix_keywords: Fixing keywords
-setting_autologin: Autologin
-setting_date_format: Date format
-setting_cross_project_issue_relations: Allow cross-project issue relations
+setting_autofetch_changesets: Auto-obter commits
+setting_sys_api_enabled: Ativa WS para gerenciamento do repositório
+setting_commit_ref_keywords: Palavras de referência
+setting_commit_fix_keywords: Palavras de fechamento
+setting_autologin: Auto-login
+setting_date_format: Formato da data
+setting_cross_project_issue_relations: Permitir relacionar tickets entre projetos
-label_user: Usuario
-label_user_plural: Usuarios
-label_user_new: Novo usuario
+label_user: Usuário
+label_user_plural: Usuários
+label_user_new: Novo usuário
label_project: Projeto
label_project_new: Novo projeto
label_project_plural: Projetos
-label_project_all: All Projects
-label_project_latest: Ultimos projetos
-label_issue: Tarefa
-label_issue_new: Nova tarefa
-label_issue_plural: Tarefas
-label_issue_view_all: Ver todas as tarefas
+label_project_all: Todos os projetos
+label_project_latest: Últimos projetos
+label_issue: Ticket
+label_issue_new: Novo ticket
+label_issue_plural: Tickets
+label_issue_view_all: Ver todos os tickets
label_document: Documento
label_document_new: Novo documento
label_document_plural: Documentos
-label_role: Regra
-label_role_plural: Regras
-label_role_new: Nova regra
-label_role_and_permissions: Regras e permissoes
+label_role: Papel
+label_role_plural: Papéis
+label_role_new: Novo papel
+label_role_and_permissions: Papéis e permissões
label_member: Membro
label_member_new: Novo membro
label_member_plural: Membros
-label_tracker: Tipo
-label_tracker_plural: Tipos
+label_tracker: Tipo de ticket
+label_tracker_plural: Tipos de ticket
label_tracker_new: Novo tipo
label_workflow: Workflow
-label_issue_status: Status da tarefa
-label_issue_status_plural: Status das tarefas
+label_issue_status: Status do ticket
+label_issue_status_plural: Status dos tickets
label_issue_status_new: Novo status
-label_issue_category: Categoria de tarefa
-label_issue_category_plural: Categorias de tarefa
+label_issue_category: Categoria de ticket
+label_issue_category_plural: Categorias de tickets
label_issue_category_new: Nova categoria
label_custom_field: Campo personalizado
-label_custom_field_plural: Campos personalizado
+label_custom_field_plural: Campos personalizados
label_custom_field_new: Novo campo personalizado
-label_enumerations: Enumeracao
-label_enumeration_new: Novo valor
-label_information: Informacao
-label_information_plural: Informacoes
-label_please_login: Efetue login
+label_enumerations: 'Tipos & Categorias'
+label_enumeration_new: Novo
+label_information: Informação
+label_information_plural: Informações
+label_please_login: Efetue o login
label_register: Registre-se
-label_password_lost: Perdi a senha
-label_home: Pagina inicial
-label_my_page: Minha pagina
+label_password_lost: Perdi minha senha
+label_home: Página inicial
+label_my_page: Minha página
label_my_account: Minha conta
label_my_projects: Meus projetos
-label_administration: Administracao
-label_login: Login
-label_logout: Logout
+label_administration: Administração
+label_login: Entrar
+label_logout: Sair
label_help: Ajuda
-label_reported_issues: Tarefas reportadas
-label_assigned_to_me_issues: Tarefas atribuidas a mim
-label_last_login: Utima conexao
-label_last_updates: Ultima alteracao
-label_last_updates_plural: %d Ultimas alteracoes
+label_reported_issues: Tickets reportados
+label_assigned_to_me_issues: Meus tickets
+label_last_login: Última conexao
+label_last_updates: Última alteração
+label_last_updates_plural: %d Últimas alterações
label_registered_on: Registrado em
label_activity: Atividade
label_new: Novo
-label_logged_as: Logado como
+label_logged_as: "Acessando como:"
label_environment: Ambiente
-label_authentication: Autenticacao
-label_auth_source: Modo de autenticacao
-label_auth_source_new: Novo modo de autenticacao
-label_auth_source_plural: Modos de autenticacao
+label_authentication: Autenticação
+label_auth_source: Modo de autenticação
+label_auth_source_new: Novo modo de autenticação
+label_auth_source_plural: Modos de autenticação
label_subproject_plural: Sub-projetos
-label_min_max_length: Tamanho min-max
+label_min_max_length: Tamanho mín-máx
label_list: Lista
label_date: Data
label_integer: Inteiro
@@ -262,169 +263,169 @@ label_attribute: Atributo
label_attribute_plural: Atributos
label_download: %d Download
label_download_plural: %d Downloads
-label_no_data: Sem dados para mostrar
-label_change_status: Mudar status
-label_history: Historico
+label_no_data: Nenhuma informação disponível
+label_change_status: Alterar status
+label_history: Histórico
label_attachment: Arquivo
label_attachment_new: Novo arquivo
label_attachment_delete: Apagar arquivo
label_attachment_plural: Arquivos
-label_report: Relatorio
-label_report_plural: Relatorio
-label_news: Noticias
-label_news_new: Adicionar noticias
-label_news_plural: Noticias
-label_news_latest: Ultimas noticias
-label_news_view_all: Ver todas as noticias
-label_change_log: Change log
-label_settings: Ajustes
-label_overview: Visao geral
-label_version: Versao
-label_version_new: Nova versao
-label_version_plural: Versoes
-label_confirmation: Confirmacao
+label_report: Relatório
+label_report_plural: Relatório
+label_news: Notícia
+label_news_new: Adicionar notícias
+label_news_plural: Notícias
+label_news_latest: Últimas notícias
+label_news_view_all: Ver todas as notícias
+label_change_log: Registro de alterações
+label_settings: Configurações
+label_overview: Visão geral
+label_version: Versão
+label_version_new: Nova versão
+label_version_plural: Versões
+label_confirmation: Confirmação
label_export_to: Exportar para
label_read: Ler...
-label_public_projects: Projetos publicos
+label_public_projects: Projetos públicos
label_open_issues: Aberto
-label_open_issues_plural: Abertos
+label_open_issues_plural: Abertos
label_closed_issues: Fechado
label_closed_issues_plural: Fechados
label_total: Total
-label_permissions: Permissoes
+label_permissions: Permissões
label_current_status: Status atual
label_new_statuses_allowed: Novo status permitido
label_all: todos
label_none: nenhum
-label_next: Proximo
+label_next: Próximo
label_previous: Anterior
label_used_by: Usado por
label_details: Detalhes
label_add_note: Adicionar nota
-label_per_page: Por pagina
-label_calendar: Calendario
-label_months_from: Meses de
+label_per_page: Por página
+label_calendar: Calendário
+label_months_from: meses a partir de
label_gantt: Gantt
label_internal: Interno
-label_last_changes: utlimas %d mudancas
-label_change_view_all: Mostrar todas as mudancas
-label_personalize_page: Personalizar esta pagina
-label_comment: Comentario
-label_comment_plural: Comentarios
-label_comment_add: Adicionar comentario
-label_comment_added: Comentario adicionado
-label_comment_delete: Apagar comentario
+label_last_changes: últimas %d alteraçoes
+label_change_view_all: Mostrar todas as alteraçoes
+label_personalize_page: Personalizar esta página
+label_comment: Comentário
+label_comment_plural: Comentários
+label_comment_add: Adicionar comentário
+label_comment_added: Comentário adicionado
+label_comment_delete: Apagar comentário
label_query: Consulta personalizada
label_query_plural: Consultas personalizadas
label_query_new: Nova consulta
label_filter_add: Adicionar filtro
label_filter_plural: Filtros
-label_equals: e
-label_not_equals: nao e
-label_in_less_than: e maior que
-label_in_more_than: e menor que
+label_equals: é
+label_not_equals: não é
+label_in_less_than: é maior que
+label_in_more_than: é menor que
label_in: em
label_today: hoje
-label_this_week: this week
+label_this_week: esta semana
label_less_than_ago: faz menos de
label_more_than_ago: faz mais de
-label_ago: dias atras
-label_contains: contem
-label_not_contains: nao contem
+label_ago: dias atrás
+label_contains: contém
+label_not_contains: não contem
label_day_plural: dias
-label_repository: Repository
-label_browse: Browse
-label_modification: %d change
-label_modification_plural: %d changes
-label_revision: Revision
-label_revision_plural: Revisions
-label_added: added
-label_modified: modified
-label_deleted: deleted
-label_latest_revision: Latest revision
-label_latest_revision_plural: Latest revisions
-label_view_revisions: View revisions
-label_max_size: Maximum size
+label_repository: Repositório
+label_browse: Procurar
+label_modification: %d alteração
+label_modification_plural: %d alterações
+label_revision: Revisão
+label_revision_plural: Revisões
+label_added: adicionado
+label_modified: modificado
+label_deleted: excluído
+label_latest_revision: Última revisão
+label_latest_revision_plural: Últimas revisões
+label_view_revisions: Visualizar revisões
+label_max_size: Tamanho máximo
label_on: 'em'
-label_sort_highest: Mover para o inicio
+label_sort_highest: Mover para o início
label_sort_higher: Mover para cima
label_sort_lower: Mover para baixo
label_sort_lowest: Mover para o fim
-label_roadmap: Roadmap
-label_roadmap_due_in: Due in
-label_roadmap_overdue: %s late
-label_roadmap_no_issues: Sem tarefas para essa versao
+label_roadmap: Planejamento
+label_roadmap_due_in: Previsão em
+label_roadmap_overdue: %s atrasado
+label_roadmap_no_issues: Sem tickets para esta versão
label_search: Busca
label_result_plural: Resultados
label_all_words: Todas as palavras
label_wiki: Wiki
-label_wiki_edit: Wiki edit
-label_wiki_edit_plural: Wiki edits
-label_wiki_page: Wiki page
-label_wiki_page_plural: Wiki pages
-label_index_by_title: Index by title
-label_index_by_date: Index by date
-label_current_version: Versao atual
-label_preview: Previa
+label_wiki_edit: Editar Wiki
+label_wiki_edit_plural: Edições Wiki
+label_wiki_page: Página Wiki
+label_wiki_page_plural: Páginas Wiki
+label_index_by_title: Ãndice por título
+label_index_by_date: Ãndice por data
+label_current_version: Versão atual
+label_preview: Pré-visualizar
label_feed_plural: Feeds
-label_changes_details: Detalhes de todas as mudancas
-label_issue_tracking: Tarefas
+label_changes_details: Detalhes de todas as alterações
+label_issue_tracking: Tickets
label_spent_time: Tempo gasto
label_f_hour: %.2f hora
label_f_hour_plural: %.2f horas
label_time_tracking: Tempo trabalhado
-label_change_plural: Mudancas
-label_statistics: Estatisticas
-label_commits_per_month: Commits por mes
+label_change_plural: Mudanças
+label_statistics: Estatísticas
+label_commits_per_month: Commits por mês
label_commits_per_author: Commits por autor
-label_view_diff: Ver diferencas
+label_view_diff: Ver diferenças
label_diff_inline: inline
-label_diff_side_by_side: side by side
-label_options: Opcoes
+label_diff_side_by_side: lado a lado
+label_options: Opções
label_copy_workflow_from: Copiar workflow de
-label_permissions_report: Relatorio de permissoes
-label_watched_issues: Watched issues
-label_related_issues: Related issues
-label_applied_status: Applied status
-label_loading: Loading...
-label_relation_new: New relation
-label_relation_delete: Delete relation
-label_relates_to: related to
-label_duplicates: duplicates
-label_blocks: blocks
-label_blocked_by: blocked by
-label_precedes: precedes
-label_follows: follows
-label_end_to_start: end to start
-label_end_to_end: end to end
-label_start_to_start: start to start
-label_start_to_end: start to end
-label_stay_logged_in: Stay logged in
-label_disabled: disabled
-label_show_completed_versions: Show completed versions
-label_me: me
-label_board: Forum
-label_board_new: New forum
-label_board_plural: Forums
-label_topic_plural: Topics
-label_message_plural: Messages
-label_message_last: Last message
-label_message_new: New message
-label_reply_plural: Replies
-label_send_information: Send account information to the user
-label_year: Year
-label_month: Month
-label_week: Week
-label_date_from: From
-label_date_to: To
-label_language_based: Language based
-label_sort_by: Sort by %s
-label_send_test_email: Send a test email
-label_feeds_access_key_created_on: RSS access key created %s ago
-label_module_plural: Modules
-label_added_time_by: Added by %s %s ago
-label_updated_time: Updated %s ago
-label_jump_to_a_project: Jump to a project...
+label_permissions_report: Relatório de permissões
+label_watched_issues: Tickes acompanhados
+label_related_issues: Tickets relacionados
+label_applied_status: Status aplicado
+label_loading: Carregando...
+label_relation_new: Nova relação
+label_relation_delete: Excluir relação
+label_relates_to: relacionado a
+label_duplicates: duplicado de
+label_blocks: bloqueia
+label_blocked_by: bloqueado por
+label_precedes: precede
+label_follows: segue
+label_end_to_start: fim para o início
+label_end_to_end: fim para fim
+label_start_to_start: início para início
+label_start_to_end: início para fim
+label_stay_logged_in: Permanecer logado
+label_disabled: desabilitado
+label_show_completed_versions: Exibir versões completas
+label_me: eu
+label_board: Fórum
+label_board_new: Novo fórum
+label_board_plural: Fóruns
+label_topic_plural: Tópicos
+label_message_plural: Mensagens
+label_message_last: Última mensagem
+label_message_new: Nova mensagem
+label_reply_plural: Respostas
+label_send_information: Enviar informação de conta para o usuário
+label_year: Ano
+label_month: Mês
+label_week: Semana
+label_date_from: De
+label_date_to: Para
+label_language_based: Com base no idioma
+label_sort_by: Ordenar por %s
+label_send_test_email: Enviar um email de teste
+label_feeds_access_key_created_on: chave de acesso RSS criada %s atrás
+label_module_plural: Módulos
+label_added_time_by: Adicionado por %s %s atrás
+label_updated_time: Atualizado %s atrás
+label_jump_to_a_project: Ir para o projeto...
button_login: Login
button_submit: Enviar
@@ -436,7 +437,7 @@ button_create: Criar
button_test: Testar
button_edit: Editar
button_add: Adicionar
-button_change: Mudar
+button_change: Alterar
button_apply: Aplicar
button_clear: Limpar
button_lock: Bloquear
@@ -450,59 +451,59 @@ button_cancel: Cancelar
button_activate: Ativar
button_sort: Ordenar
button_log_time: Tempo de trabalho
-button_rollback: Voltar para esta versao
-button_watch: Watch
-button_unwatch: Unwatch
-button_reply: Reply
-button_archive: Archive
-button_unarchive: Unarchive
-button_reset: Reset
-button_rename: Rename
+button_rollback: Voltar para esta versão
+button_watch: Acompanhar
+button_unwatch: Não Acompanhar
+button_reply: Responder
+button_archive: Arquivar
+button_unarchive: Desarquivar
+button_reset: Redefinir
+button_rename: Renomear
status_active: ativo
status_registered: registrado
status_locked: bloqueado
-text_select_mail_notifications: Selecionar acoes para ser enviado uma notificacao por email
-text_regexp_info: eg. ^[A-Z0-9]+$
-text_min_max_length_info: 0 siginifica sem restricao
-text_project_destroy_confirmation: Voce tem certeza que deseja deletar este projeto e todas os dados relacionados?
+text_select_mail_notifications: Selecionar ações para ser enviado uma notificação por email
+text_regexp_info: ex. ^[A-Z0-9]+$
+text_min_max_length_info: 0 siginifica sem restrição
+text_project_destroy_confirmation: Você tem certeza que deseja excluir este projeto e todos os dados relacionados?
text_workflow_edit: Selecione uma regra e um tipo de tarefa para editar o workflow
-text_are_you_sure: Voce tem certeza ?
+text_are_you_sure: Você tem certeza?
text_journal_changed: alterado de %s para %s
text_journal_set_to: setar para %s
text_journal_deleted: apagado
-text_tip_task_begin_day: tarefa comeca neste dia
+text_tip_task_begin_day: tarefa inicia neste dia
text_tip_task_end_day: tarefa termina neste dia
-text_tip_task_begin_end_day: tarefa comeca e termina neste dia
-text_project_identifier_info: 'Letras minusculas (a-z), numeros e tracos permitido.<br />Uma vez salvo, o identificador nao pode ser mudado.'
-text_caracters_maximum: %d maximo de caracteres
+text_tip_task_begin_end_day: tarefa inicia e termina neste dia
+text_project_identifier_info: 'Letras minúsculas (a-z), números e traços permitidos.<br />Uma vez salvo, o identificador não pode ser alterado.'
+text_caracters_maximum: máximo %d caracteres
text_length_between: Tamanho entre %d e %d caracteres.
text_tracker_no_workflow: Sem workflow definido para este tipo.
-text_unallowed_characters: Unallowed characters
-text_comma_separated: Multiple values allowed (comma separated).
-text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
-text_issue_added: Tarefa %s foi incluída (by %s).
-text_issue_updated: Tarefa %s foi alterada (by %s).
-text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ?
-text_issue_category_destroy_question: Some issues (%d) are assigned to this category. What do you want to do ?
-text_issue_category_destroy_assignments: Remove category assignments
-text_issue_category_reassign_to: Reassing issues to this category
+text_unallowed_characters: Caracteres não permitidos
+text_comma_separated: Múltiplos valores são permitidos (separados por vírgula).
+text_issues_ref_in_commit_messages: Referenciando e fixando tickets nas mensagens de commit
+text_issue_added: Tarefa %s foi incluída (por %s).
+text_issue_updated: Tarefa %s foi alterada (por %s).
+text_wiki_destroy_confirmation: Você tem certeza que deseja excluir este wiki e todo o seu conteúdo?
+text_issue_category_destroy_question: Alguns tickets (%d) estão atribuídos a esta categoria. O que você deseja fazer?
+text_issue_category_destroy_assignments: Remover atribuições da categoria
+text_issue_category_reassign_to: Redefinir tickets para esta categoria
-default_role_manager: Analista de Negocio ou Gerente de Projeto
+default_role_manager: Gerente
default_role_developper: Desenvolvedor
-default_role_reporter: Analista de Suporte
-default_tracker_bug: Bug
-default_tracker_feature: Implementacao
+default_role_reporter: Informante
+default_tracker_bug: Problema
+default_tracker_feature: Implementação
default_tracker_support: Suporte
default_issue_status_new: Novo
-default_issue_status_assigned: Atribuido
+default_issue_status_assigned: Atribuído
default_issue_status_resolved: Resolvido
default_issue_status_feedback: Feedback
default_issue_status_closed: Fechado
default_issue_status_rejected: Rejeitado
-default_doc_category_user: Documentacao do usuario
-default_doc_category_tech: Documentacao do tecnica
+default_doc_category_user: Documentação do usuário
+default_doc_category_tech: Documentação técnica
default_priority_low: Baixo
default_priority_normal: Normal
default_priority_high: Alto
@@ -514,107 +515,124 @@ default_activity_development: Desenvolvimento
enumeration_issue_priorities: Prioridade das tarefas
enumeration_doc_categories: Categorias de documento
enumeration_activities: Atividades (time tracking)
-label_file_plural: Files
+label_file_plural: Arquivos
label_changeset_plural: Changesets
-field_column_names: Columns
-label_default_columns: Default columns
-setting_issue_list_default_columns: Default columns displayed on the issue list
-setting_repositories_encodings: Repositories encodings
-notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
-label_bulk_edit_selected_issues: Bulk edit selected issues
-label_no_change_option: (No change)
-notice_failed_to_save_issues: "Failed to save %d issue(s) on %d selected: %s."
-label_theme: Theme
-label_default: Default
-label_search_titles_only: Search titles only
-label_nobody: nobody
-button_change_password: Change password
-text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
-label_user_mail_option_selected: "For any event on the selected projects only..."
-label_user_mail_option_all: "For any event on all my projects"
-label_user_mail_option_none: "Only for things I watch or I'm involved in"
-setting_emails_footer: Emails footer
-label_float: Float
-button_copy: Copy
-mail_body_account_information_external: You can use your "%s" account to log in.
-mail_body_account_information: Your account information
-setting_protocol: Protocol
-label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
-setting_time_format: Time format
-label_registration_activation_by_email: account activation by email
-mail_subject_account_activation_request: %s account activation request
-mail_body_account_activation_request: 'A new user (%s) has registered. His account his pending your approval:'
-label_registration_automatic_activation: automatic account activation
-label_registration_manual_activation: manual account activation
-notice_account_pending: "Your account was created and is now pending administrator approval."
-field_time_zone: Time zone
-text_caracters_minimum: Must be at least %d characters long.
-setting_bcc_recipients: Blind carbon copy recipients (bcc)
-button_annotate: Annotate
-label_issues_by: Issues by %s
-field_searchable: Searchable
-label_display_per_page: 'Per page: %s'
-setting_per_page_options: Objects per page options
-label_age: Age
-notice_default_data_loaded: Default configuration successfully loaded.
-text_load_default_configuration: Load the default configuration
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
-error_can_t_load_default_data: "Default configuration could not be loaded: %s"
-button_update: Update
-label_change_properties: Change properties
-label_general: General
-label_repository_plural: Repositories
-label_associated_revisions: Associated revisions
-setting_user_format: Users display format
-text_status_changed_by_changeset: Applied in changeset %s.
-label_more: More
-text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?'
+field_column_names: Colunas
+label_default_columns: Colunas padrão
+setting_issue_list_default_columns: Colunas padrão visíveis na lista de tickets
+setting_repositories_encodings: Codificação dos repositórios
+notice_no_issue_selected: "Nenhum ticket está selecionado! Por favor, marque os tickets que você deseja alterar."
+label_bulk_edit_selected_issues: Edição em massa dos tickets selecionados.
+label_no_change_option: (Sem alteração)
+notice_failed_to_save_issues: "Problema ao salvar %d ticket(s) no %d selecionado: %s."
+label_theme: Tema
+label_default: Padrão
+label_search_titles_only: Pesquisar somente títulos
+label_nobody: ninguém
+button_change_password: Alterar senha
+text_user_mail_option: "Para projetos não selecionados, você somente receberá notificações sobre o que você acompanha ou está envolvido (ex. tickets que você é autor ou está atribuído)"
+label_user_mail_option_selected: "Para qualquer evento somente no(s) projeto(s) selecionado(s)..."
+label_user_mail_option_all: "Para qualquer evento em todos os meus projetos"
+label_user_mail_option_none: "Somente eventos que eu acompanho ou estou envolvido"
+setting_emails_footer: Rodapé dos emails
+label_float: Flutuante
+button_copy: Copiar
+mail_body_account_information_external: Você pode usar sua conta "%s" para entrar.
+mail_body_account_information: Informações de sua conta
+setting_protocol: Protocolo
+label_user_mail_no_self_notified: "Eu não desejo ser notificado de minhas próprias modificações"
+setting_time_format: Formato de data
+label_registration_activation_by_email: ativação de conta por email
+mail_subject_account_activation_request: %s requisição de ativação de conta
+mail_body_account_activation_request: 'Um novo usuário (%s) se registrou. A conta está aguardando sua aprovação:'
+label_registration_automatic_activation: ativação automática de conta
+label_registration_manual_activation: ativação manual de conta
+notice_account_pending: "Sua conta foi criada e está aguardando aprovação do administrador."
+field_time_zone: Fuso-horário
+text_caracters_minimum: Precisa ter ao menos %d caracteres.
+setting_bcc_recipients: Destinatários com cópia oculta (cco)
+button_annotate: Anotar
+label_issues_by: Tickets por %s
+field_searchable: Pesquisável
+label_display_per_page: 'Por página: %s'
+setting_per_page_options: Opções de itens por página
+notice_default_data_loaded: Configuração padrão carregada com sucesso.
+text_load_default_configuration: Carregar a configuração padrão
+text_no_configuration_data: "Os Papéis, tipos de tickets, status de tickets e workflows não foram configurados ainda.\nÉ altamente recomendado carregar as configurações padrão. Você poderá modificar estas configurações assim que carregadas."
+error_can_t_load_default_data: "Configuração padrão não pôde ser carregada: %s"
+button_update: Atualizar
+label_change_properties: Alterar propriedades
+label_general: Geral
+label_repository_plural: Repositórios
+label_associated_revisions: Revisões associadas
+setting_user_format: Formato de visualização dos usuários
+text_status_changed_by_changeset: Aplicado no changeset %s.
+label_more: Mais
+text_issues_destroy_confirmation: 'Você tem certeza que deseja excluir o(s) ticket(s) selecionado(s)?'
label_scm: SCM
-text_select_project_modules: 'Select modules to enable for this project:'
-label_issue_added: Issue added
-label_issue_updated: Issue updated
-label_document_added: Document added
-label_message_posted: Message added
-label_file_added: File added
-label_news_added: News added
-project_module_boards: Boards
-project_module_issue_tracking: Issue tracking
+text_select_project_modules: 'Selecione módulos para habilitar para este projeto:'
+label_issue_added: Ticket adicionado
+label_issue_updated: Ticket atualizado
+label_document_added: Documento adicionado
+label_message_posted: Mensagem enviada
+label_file_added: Arquivo adicionado
+label_news_added: Notícia adicionada
+project_module_boards: Fóruns
+project_module_issue_tracking: Gerenciamento de Tickets
project_module_wiki: Wiki
-project_module_files: Files
-project_module_documents: Documents
-project_module_repository: Repository
-project_module_news: News
-project_module_time_tracking: Time tracking
-text_file_repository_writable: File repository writable
-text_default_administrator_account_changed: Default administrator account changed
-text_rmagick_available: RMagick available (optional)
-button_configure: Configure
+project_module_files: Arquivos
+project_module_documents: Documentos
+project_module_repository: Repositório
+project_module_news: Notícias
+project_module_time_tracking: Gerenciamento de tempo
+text_file_repository_writable: Repositório de arquivos gravável
+text_default_administrator_account_changed: Conta de administrador padrão modificada
+text_rmagick_available: RMagick disponível (opcional)
+button_configure: Configuração
label_plugins: Plugins
-label_ldap_authentication: LDAP authentication
+label_ldap_authentication: autenticação LDAP
label_downloads_abbr: D/L
-label_this_month: this month
-label_last_n_days: last %d days
-label_all_time: all time
-label_this_year: this year
-label_date_range: Date range
-label_last_week: last week
-label_yesterday: yesterday
-label_last_month: last month
-label_add_another_file: Add another file
-label_optional_description: Optional description
-text_destroy_time_entries_question: %.02f hours were reported on the issues you are about to delete. What do you want to do ?
-error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
-text_assign_time_entries_to_project: Assign reported hours to the project
-text_destroy_time_entries: Delete reported hours
-text_reassign_time_entries: 'Reassign reported hours to this issue:'
-setting_activity_days_default: Days displayed on project activity
-label_chronological_order: In chronological order
-field_comments_sorting: Display comments
-label_reverse_chronological_order: In reverse chronological order
-label_preferences: Preferences
-setting_display_subprojects_issues: Display subprojects issues on main projects by default
-label_overall_activity: Overall activity
-setting_default_projects_public: New projects are public by default
-error_scm_annotate: "The entry does not exist or can not be annotated."
-label_planning: Planning
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_this_month: este mês
+label_last_n_days: últimos %d dias
+label_all_time: todo o tempo
+label_this_year: este ano
+label_date_range: Intervalo de datas
+label_last_week: última semana
+label_yesterday: ontem
+label_last_month: último mês
+label_add_another_file: Adicionar outro arquivo
+label_optional_description: Descrição opcional
+text_destroy_time_entries_question: %.02f horas foram reportadas neste ticket que você está excluindo. O que você deseja fazer?
+error_issue_not_found_in_project: 'O ticket não foi encontrado ou não pertence a este projeto'
+text_assign_time_entries_to_project: Atribuir horas reportadas para o projeto
+text_destroy_time_entries: Excluir horas reportadas
+text_reassign_time_entries: 'Redefinir horas reportadas para este ticket:'
+setting_activity_days_default: Dias visualizados na atividade do projeto
+label_chronological_order: Em ordem cronológica
+field_comments_sorting: Visualizar comentários
+label_reverse_chronological_order: Em order cronológica reversa
+label_preferences: Preferências
+setting_display_subprojects_issues: Visualizar tickets dos subprojetos nos projetos principais por padrão
+label_overall_activity: Atividade geral
+setting_default_projects_public: Novos projetos são públicos por padrão
+error_scm_annotate: "Esta entrada não existe ou não pode ser anotada."
+label_planning: Planejamento
+text_subprojects_destroy_warning: 'Seu(s) subprojeto(s): %s também serão excluídos.'
+label_age: Age
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/pt.yml b/groups/lang/pt.yml
index 6f51c8ed2..5562ca4ac 100644
--- a/groups/lang/pt.yml
+++ b/groups/lang/pt.yml
@@ -48,6 +48,7 @@ general_text_no: 'não'
general_text_yes: 'sim'
general_lang_name: 'Português'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Segunda,Terça,Quarta,Quinta,Sexta,Sábado,Domingo
@@ -489,7 +490,7 @@ text_issue_category_destroy_question: Some issues (%d) are assigned to this cate
text_issue_category_destroy_assignments: Remove category assignments
text_issue_category_reassign_to: Reassing issues to this category
-default_role_manager: Analista de Negócio ou Gerente de Projeto
+default_role_manager: Gerente de Projeto
default_role_developper: Desenvolvedor
default_role_reporter: Analista de Suporte
default_tracker_bug: Bug
@@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/ro.yml b/groups/lang/ro.yml
index 59edfeb70..5bb49ecec 100644
--- a/groups/lang/ro.yml
+++ b/groups/lang/ro.yml
@@ -48,6 +48,7 @@ general_text_no: 'nu'
general_text_yes: 'da'
general_lang_name: 'Română'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Luni,Marti,Miercuri,Joi,Vineri,Sambata,Duminica
@@ -497,7 +498,7 @@ default_issue_status_new: Nou
default_issue_status_assigned: Atribuit
default_issue_status_resolved: Rezolvat
default_issue_status_feedback: Feedback
-default_issue_status_closed: Rezolvat
+default_issue_status_closed: Closed
default_issue_status_rejected: Respins
default_doc_category_user: Documentatie
default_doc_category_tech: Documentatie tehnica
@@ -618,3 +619,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/ru.yml b/groups/lang/ru.yml
index f69009847..01cdcd478 100644
--- a/groups/lang/ru.yml
+++ b/groups/lang/ru.yml
@@ -48,6 +48,7 @@ general_text_no: 'Ðет'
general_text_yes: 'Да'
general_lang_name: 'Russian (РуÑÑкий)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: UTF-8
general_pdf_encoding: UTF-8
general_day_names: Понедельник,Вторник,Среда,Четверг,ПÑтница,Суббота,ВоÑкреÑенье
@@ -108,7 +109,7 @@ field_author: Ðвтор
field_created_on: Создано
field_updated_on: Обновлено
field_field_format: Формат
-field_is_for_all: Ð”Ð»Ñ Ð²Ñех форматов
+field_is_for_all: Ð”Ð»Ñ Ð²Ñех проектов
field_possible_values: Возможные значениÑ
field_regexp: РегулÑрное выражение
field_min_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð°
@@ -163,7 +164,7 @@ field_comments: Комментарий
field_url: URL
field_start_page: Ð¡Ñ‚Ð°Ñ€Ñ‚Ð¾Ð²Ð°Ñ Ñтраница
field_subproject: Подпроект
-field_hours: ЧаÑ(а)(ов)
+field_hours: ЧаÑ(а,ов)
field_activity: ДеÑтельноÑть
field_spent_on: Дата
field_identifier: Ун. идентификатор
@@ -239,9 +240,9 @@ label_issue_status_new: Ðовый ÑтатуÑ
label_issue_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸
label_issue_category_plural: Категории задачи
label_issue_category_new: ÐÐ¾Ð²Ð°Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ
-label_custom_field: Поле клиента
-label_custom_field_plural: ÐŸÐ¾Ð»Ñ ÐºÐ»Ð¸ÐµÐ½Ñ‚Ð°
-label_custom_field_new: Ðовое поле клиента
+label_custom_field: ÐаÑтраиваемое поле
+label_custom_field_plural: ÐаÑтраиваемые полÑ
+label_custom_field_new: Ðовое наÑтраиваемое поле
label_enumerations: Справочники
label_enumeration_new: Ðовое значение
label_information: ИнформациÑ
@@ -276,7 +277,7 @@ label_min_max_length: ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ - МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½
label_list: СпиÑок
label_date: Дата
label_integer: Целый
-label_float: Свободный
+label_float: С плавающей точкой
label_boolean: ЛогичеÑкий
label_string: ТекÑÑ‚
label_text: Длинный текÑÑ‚
@@ -332,13 +333,13 @@ label_internal: Внутренний
label_last_changes: менее %d изменений
label_change_view_all: ПроÑмотреть вÑе изменениÑ
label_personalize_page: ПерÑонализировать данную Ñтраницу
-label_comment: Комментировать
+label_comment: комментарий
label_comment_plural: Комментарии
label_comment_add: ОÑтавить комментарий
label_comment_added: Добавленный комментарий
label_comment_delete: Удалить комментарии
-label_query: Ð—Ð°Ð¿Ñ€Ð¾Ñ ÐºÐ»Ð¸ÐµÐ½Ñ‚Ð°
-label_query_plural: ЗапроÑÑ‹ клиентов
+label_query: Сохраненный запроÑ
+label_query_plural: Сохраненные запроÑÑ‹
label_query_new: Ðовый запроÑ
label_filter_add: Добавить фильтр
label_filter_plural: Фильтры
@@ -406,7 +407,7 @@ label_diff_side_by_side: Ñ€Ñдом
label_options: Опции
label_copy_workflow_from: Скопировать поÑледовательноÑть дейÑтвий из
label_permissions_report: Отчет о правах доÑтупа
-label_watched_issues: ПроÑмотренные задачи
+label_watched_issues: ПроÑматриваемые задачи
label_related_issues: СвÑзанные задачи
label_applied_status: Применимый ÑтатуÑ
label_loading: Загрузка...
@@ -461,7 +462,7 @@ label_user_mail_option_selected: "Ð”Ð»Ñ Ð²Ñех Ñобытий только в
label_user_mail_option_none: "Только Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, что Ñ Ð¿Ñ€Ð¾Ñматриваю или в чем Ñ ÑƒÑ‡Ð°Ñтвую"
label_user_mail_no_self_notified: "Ðе извещать об изменениÑÑ… которые Ñ Ñделал Ñам"
label_registration_activation_by_email: Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей по email
-label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡Ñ‚ÐµÐ½Ñ‹Ñ… запиÑей
+label_registration_automatic_activation: автоматичеÑÐºÐ°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей
label_registration_manual_activation: активировать учетные запиÑи вручную
label_age: ВозраÑÑ‚
label_change_properties: Изменить ÑвойÑтва
@@ -604,7 +605,7 @@ label_date_range: временной интервал
label_last_week: поÑледнÑÑ Ð½ÐµÐ´ÐµÐ»ÑŽ
label_yesterday: вчера
label_last_month: поÑледний меÑÑц
-label_add_another_file: Добавить ещё один файл
+label_add_another_file: Добавить ещё один файл
label_optional_description: ОпиÑание (выборочно)
text_destroy_time_entries_question: Ð’Ñ‹ ÑобираетеÑÑŒ удалить %.02f чаÑа(ов) прикрепленных за Ñтой задачей.
error_issue_not_found_in_project: Задача не была найдена или не прикреплена к Ñтому проекту
@@ -621,4 +622,21 @@ label_overall_activity: Ð¡Ð²Ð¾Ð´Ð½Ð°Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ð¾Ñть
setting_default_projects_public: Ðовые проекты ÑвлÑÑŽÑ‚ÑÑ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸
error_scm_annotate: "Данные отÑутÑтвуют или не могут быть подпиÑаны."
label_planning: Планирование
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+text_subprojects_destroy_warning: 'Подпроекты: %s также будут удалены.'
+label_and_its_subprojects: %s и вÑе подпроекты
+mail_body_reminder: "%d назначенных на Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ на Ñледующие %d дней:"
+mail_subject_reminder: "%d назначенных на Ð²Ð°Ñ Ð·Ð°Ð´Ð°Ñ‡ в ближайшие дни"
+text_user_wrote: '%s напиÑал:'
+label_duplicated_by: дублируетÑÑ
+setting_enabled_scm: ЗадейÑтвовать SCM
+text_enumeration_category_reassign_to: 'Ðазначить им Ñледующее значение:'
+text_enumeration_destroy_question: '%d объект(а,ов) ÑвÑзаны Ñ Ñтим значением.'
+label_incoming_emails: Приём Ñообщений
+label_generate_key: Сгенерировать ключ
+setting_mail_handler_api_enabled: Включить веб-ÑÐµÑ€Ð²Ð¸Ñ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñщих Ñообщений
+setting_mail_handler_api_key: API ключ
+text_email_delivery_not_configured: "Параметры работы Ñ Ð¿Ð¾Ñ‡Ñ‚Ð¾Ð²Ñ‹Ð¼ Ñервером не наÑтроены и Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ email не активна.\nÐаÑтроить параметры Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ SMTP Ñервера вы можете в файле config/email.yml. Ð”Ð»Ñ Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹ перезапуÑтите приложение."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/sr.yml b/groups/lang/sr.yml
index d9869c362..566a46d9f 100644
--- a/groups/lang/sr.yml
+++ b/groups/lang/sr.yml
@@ -48,6 +48,7 @@ general_text_no: 'ne'
general_text_yes: 'da'
general_lang_name: 'Srpski'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Ponedeljak, Utorak, Sreda, Äetvrtak, Petak, Subota, Nedelja
@@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/sv.yml b/groups/lang/sv.yml
index c0f691230..4cb1f073b 100644
--- a/groups/lang/sv.yml
+++ b/groups/lang/sv.yml
@@ -48,6 +48,7 @@ general_text_no: 'nej'
general_text_yes: 'ja'
general_lang_name: 'Svenska'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: ISO-8859-1
general_pdf_encoding: ISO-8859-1
general_day_names: Måndag,Tisdag,Onsdag,Torsdag,Fredag,Lördag,Söndag
@@ -619,3 +620,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/th.yml b/groups/lang/th.yml
new file mode 100644
index 000000000..2c3977de2
--- /dev/null
+++ b/groups/lang/th.yml
@@ -0,0 +1,641 @@
+_gloc_rule_default: '|n| n==1 ? "" : "_plural" '
+
+actionview_datehelper_select_day_prefix:
+actionview_datehelper_select_month_names: มà¸à¸£à¸²à¸„ม,à¸à¸¸à¸¡à¸ à¸²à¸žà¸±à¸™à¸˜à¹Œ,มีนาคม,เมษายน,พฤษภาคม,มิถุนายน,à¸à¸£à¸à¸Žà¸²à¸„ม,สิงหาคม,à¸à¸±à¸™à¸¢à¸²à¸¢à¸™,ตุลาคม,พฤศจิà¸à¸²à¸¢à¸™,ธันวาคม
+actionview_datehelper_select_month_names_abbr: ม.ค.,à¸.พ.,มี.ค.,เม.ย.,พ.ค.,มิ.ย.,à¸.ค.,ส.ค.,à¸.ย.,ต.ค.,พ.ย.,ธ.ค.
+actionview_datehelper_select_month_prefix:
+actionview_datehelper_select_year_prefix:
+actionview_datehelper_time_in_words_day: 1 วัน
+actionview_datehelper_time_in_words_day_plural: %d วัน
+actionview_datehelper_time_in_words_hour_about: ประมาณ 1 ชั่วโมง
+actionview_datehelper_time_in_words_hour_about_plural: ประมาณ %d ชั่วโมง
+actionview_datehelper_time_in_words_hour_about_single: ประมาณ 1 ชั่วโมง
+actionview_datehelper_time_in_words_minute: 1 นาที
+actionview_datehelper_time_in_words_minute_half: ครึ่งนาที
+actionview_datehelper_time_in_words_minute_less_than: ไม่ถึงนาที
+actionview_datehelper_time_in_words_minute_plural: %d นาที
+actionview_datehelper_time_in_words_minute_single: 1 นาที
+actionview_datehelper_time_in_words_second_less_than: ไม่ถึงวินาที
+actionview_datehelper_time_in_words_second_less_than_plural: ไม่ถึง %d วินาที
+actionview_instancetag_blank_option: à¸à¸£à¸¸à¸“าเลือà¸
+
+activerecord_error_inclusion: ไม่อยู่ในรายà¸à¸²à¸£
+activerecord_error_exclusion: ถูà¸à¸ªà¸‡à¸§à¸™à¹„ว้
+activerecord_error_invalid: ไม่ถูà¸à¸•้อง
+activerecord_error_confirmation: พิมพ์ไม่เหมือนเดิม
+activerecord_error_accepted: ต้องยอมรับ
+activerecord_error_empty: ต้องเติม
+activerecord_error_blank: ต้องเติม
+activerecord_error_too_long: ยาวเà¸à¸´à¸™à¹„ป
+activerecord_error_too_short: สั้นเà¸à¸´à¸™à¹„ป
+activerecord_error_wrong_length: ความยาวไม่ถูà¸à¸•้อง
+activerecord_error_taken: ถูà¸à¹ƒà¸Šà¹‰à¹„ปà¹à¸¥à¹‰à¸§
+activerecord_error_not_a_number: ไม่ใช่ตัวเลข
+activerecord_error_not_a_date: ไม่ใช่วันที่ ที่ถูà¸à¸•้อง
+activerecord_error_greater_than_start_date: ต้องมาà¸à¸à¸§à¹ˆà¸²à¸§à¸±à¸™à¹€à¸£à¸´à¹ˆà¸¡
+activerecord_error_not_same_project: ไม่ได้อยู่ในโครงà¸à¸²à¸£à¹€à¸”ียวà¸à¸±à¸™
+activerecord_error_circular_dependency: ความสัมพันธ์อ้างอิงเป็นวงà¸à¸¥à¸¡
+
+general_fmt_age: %d ปี
+general_fmt_age_plural: %d ปี
+general_fmt_date: %%d/%%B/%%Y
+general_fmt_datetime: %%d/%%B/%%Y %%H:%%M
+general_fmt_datetime_short: %%d %%b, %%H:%%M
+general_fmt_time: %%H:%%M
+general_text_No: 'ไม่'
+general_text_Yes: 'ใช่'
+general_text_no: 'ไม่'
+general_text_yes: 'ใช่'
+general_lang_name: 'Thai (ไทย)'
+general_csv_separator: ','
+general_csv_decimal_separator: '.'
+general_csv_encoding: Windows-874
+general_pdf_encoding: cp874
+general_day_names: จันทร์,อังคาร,พุธ,พฤหัสบดี,ศุà¸à¸£à¹Œ,เสาร์,อาทิตย์
+general_first_day_of_week: '1'
+
+notice_account_updated: บัà¸à¸Šà¸µà¹„ด้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§.
+notice_account_invalid_creditentials: ชื้ผู้ใช้หรือรหัสผ่านไม่ถูà¸à¸•้อง
+notice_account_password_updated: รหัสได้ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹à¸¥à¹‰à¸§.
+notice_account_wrong_password: รหัสผ่านไม่ถูà¸à¸•้อง
+notice_account_register_done: บัà¸à¸Šà¸µà¸–ูà¸à¸ªà¸£à¹‰à¸²à¸‡à¹à¸¥à¹‰à¸§. à¸à¸£à¸¸à¸“าเช็คเมล์ à¹à¸¥à¹‰à¸§à¸„ลิ๊à¸à¸—ี่ลิงค์ในอีเมล์เพื่อเปิดใช้บัà¸à¸Šà¸µ
+notice_account_unknown_email: ไม่มีผู้ใช้ที่ใช้อีเมล์นี้.
+notice_can_t_change_password: บัà¸à¸Šà¸µà¸™à¸µà¹‰à¹ƒà¸Šà¹‰à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนจาà¸à¹à¸«à¸¥à¹ˆà¸‡à¸ à¸²à¸¢à¸™à¸­à¸. ไม่สามารถปลี่ยนรหัสผ่านได้.
+notice_account_lost_email_sent: เราได้ส่งอีเมล์พร้อมวิธีà¸à¸²à¸£à¸ªà¸£à¹‰à¸²à¸‡à¸£à¸«à¸±à¸µà¸ªà¸œà¹ˆà¸²à¸™à¹ƒà¸«à¸¡à¹ˆà¹ƒà¸«à¹‰à¸„ุณà¹à¸¥à¹‰à¸§ à¸à¸£à¸¸à¸“าเช็คเมล์.
+notice_account_activated: บัà¸à¸Šà¸µà¸‚องคุณได้เปิดใช้à¹à¸¥à¹‰à¸§. ตอนนี้คุณสามารถเข้าสู่ระบบได้à¹à¸¥à¹‰à¸§.
+notice_successful_create: สร้างเสร็จà¹à¸¥à¹‰à¸§.
+notice_successful_update: ปรับปรุงเสร็จà¹à¸¥à¹‰à¸§.
+notice_successful_delete: ลบเสร็จà¹à¸¥à¹‰à¸§.
+notice_successful_connection: ติดต่อสำเร็จà¹à¸¥à¹‰à¸§.
+notice_file_not_found: หน้าที่คุณต้องà¸à¸²à¸£à¸”ูไม่มีอยู่จริง หรือถูà¸à¸¥à¸šà¹„ปà¹à¸¥à¹‰à¸§.
+notice_locking_conflict: ข้อมูลถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดยผู้ใช้คนอื่น.
+notice_not_authorized: คุณไม่มีสิทธิเข้าถึงหน้านี้.
+notice_email_sent: อีเมล์ได้ถูà¸à¸ªà¹ˆà¸‡à¸–ึง %s
+notice_email_error: เà¸à¸´à¸”ความผิดพลาดขณะà¸à¸³à¸ªà¹ˆà¸‡à¸­à¸µà¹€à¸¡à¸¥à¹Œ (%s)
+notice_feeds_access_key_reseted: RSS access key ของคุณถูภreset à¹à¸¥à¹‰à¸§.
+notice_failed_to_save_issues: "%d ปัà¸à¸«à¸²à¸ˆà¸²à¸ %d ปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸à¹„ม่สามารถจัดเà¸à¹‡à¸š: %s."
+notice_no_issue_selected: "ไม่มีปัà¸à¸«à¸²à¸—ี่ถูà¸à¹€à¸¥à¸·à¸­à¸! à¸à¸£à¸¸à¸“าเลือà¸à¸›à¸±à¸à¸«à¸²à¸—ี่คุณต้องà¸à¸²à¸£à¹à¸à¹‰à¹„ข."
+notice_account_pending: "บัà¸à¸Šà¸µà¸‚องคุณสร้างเสร็จà¹à¸¥à¹‰à¸§ ขณะนี้รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิจาà¸à¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£."
+notice_default_data_loaded: ค่าเริ่มต้นโหลดเสร็จà¹à¸¥à¹‰à¸§.
+
+error_can_t_load_default_data: "ค่าเริ่มต้นโหลดไม่สำเร็จ: %s"
+error_scm_not_found: "ไม่พบรุ่นที่ต้องà¸à¸²à¸£à¹ƒà¸™à¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ."
+error_scm_command_failed: "เà¸à¸´à¸”ความผิดพลาดในà¸à¸²à¸£à¹€à¸‚้าถึงà¹à¸«à¸¥à¹ˆà¸‡à¹€à¸à¹‡à¸šà¸•้นฉบับ: %s"
+error_scm_annotate: "entry ไม่มีอยู่จริง หรือไม่สามารถเขียนหมายเหตุประà¸à¸­à¸š."
+error_issue_not_found_in_project: 'ไม่พบปัà¸à¸«à¸²à¸™à¸µà¹‰ หรือปัà¸à¸«à¸²à¹„ม่ได้อยู่ในโครงà¸à¸²à¸£à¸™à¸µà¹‰'
+
+mail_subject_lost_password: รหัสผ่าน %s ของคุณ
+mail_body_lost_password: 'คลิ๊à¸à¸—ี่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:'
+mail_subject_register: เปิดบัà¸à¸Šà¸µ %s ของคุณ
+mail_body_register: 'คลิ๊à¸à¸—ี่ลิงค์ต่อไปนี้เพื่อเปลี่ยนรหัสผ่าน:'
+mail_body_account_information_external: คุณสามารถใช้บัà¸à¸Šà¸µ "%s" เพื่อเข้าสู่ระบบ.
+mail_body_account_information: ข้อมูลบัà¸à¸Šà¸µà¸‚องคุณ
+mail_subject_account_activation_request: à¸à¸£à¸¸à¸“าเปิดบัà¸à¸Šà¸µ %s
+mail_body_account_activation_request: 'ผู้ใช้ใหม่ (%s) ได้ลงทะเบียน. บัà¸à¸Šà¸µà¸‚องเขาà¸à¸³à¸¥à¸±à¸‡à¸£à¸­à¸­à¸™à¸¸à¸¡à¸±à¸•ิ:'
+
+gui_validation_error: 1 ข้อผิดพลาด
+gui_validation_error_plural: %d ข้อผิดพลาด
+
+field_name: ชื่อ
+field_description: รายละเอียด
+field_summary: สรุปย่อ
+field_is_required: ต้องใส่
+field_firstname: ชื่อ
+field_lastname: นามสà¸à¸¸à¸¥
+field_mail: อีเมล์
+field_filename: à¹à¸Ÿà¹‰à¸¡
+field_filesize: ขนาด
+field_downloads: ดาวน์โหลด
+field_author: ผู้à¹à¸•่ง
+field_created_on: สร้าง
+field_updated_on: ปรับปรุง
+field_field_format: รูปà¹à¸šà¸š
+field_is_for_all: สำหรับทุà¸à¹‚ครงà¸à¸²à¸£
+field_possible_values: ค่าที่เป็นไปได้
+field_regexp: Regular expression
+field_min_length: สั้นสุด
+field_max_length: ยาวสุด
+field_value: ค่า
+field_category: ประเภท
+field_title: ชื่อเรื่อง
+field_project: โครงà¸à¸²à¸£
+field_issue: ปัà¸à¸«à¸²
+field_status: สถานะ
+field_notes: บันทึà¸
+field_is_closed: ปัà¸à¸«à¸²à¸ˆà¸š
+field_is_default: ค่าเริ่มต้น
+field_tracker: à¸à¸²à¸£à¸•ิดตาม
+field_subject: เรื่อง
+field_due_date: วันครบà¸à¸³à¸«à¸™à¸”
+field_assigned_to: มอบหมายให้
+field_priority: ความสำคัà¸
+field_fixed_version: รุ่น
+field_user: ผู้ใช้
+field_role: บทบาท
+field_homepage: หน้าà¹à¸£à¸
+field_is_public: สาธารณะ
+field_parent: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢à¸‚อง
+field_is_in_chlog: ปัà¸à¸«à¸²à¹à¸ªà¸”งใน รายà¸à¸²à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡
+field_is_in_roadmap: ปัà¸à¸«à¸²à¹à¸ªà¸”งใน à¹à¸œà¸™à¸‡à¸²à¸™
+field_login: ชื่อที่ใช้เข้าระบบ
+field_mail_notification: à¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนทางอีเมล์
+field_admin: ผู้บริหารจัดà¸à¸²à¸£
+field_last_login_on: เข้าระบบครั้งสุดท้าย
+field_language: ภาษา
+field_effective_date: วันที่
+field_password: รหัสผ่าน
+field_new_password: รหัสผ่านใหม่
+field_password_confirmation: ยืนยันรหัสผ่าน
+field_version: รุ่น
+field_type: ชนิด
+field_host: โฮสต์
+field_port: พอร์ต
+field_account: บัà¸à¸Šà¸µ
+field_base_dn: Base DN
+field_attr_login: เข้าระบบ attribute
+field_attr_firstname: ชื่อ attribute
+field_attr_lastname: นามสà¸à¸¸à¸¥ attribute
+field_attr_mail: อีเมล์ attribute
+field_onthefly: สร้างผู้ใช้ทันที
+field_start_date: เริ่ม
+field_done_ratio: %% สำเร็จ
+field_auth_source: วิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน
+field_hide_mail: ซ่อนอีเมล์ของฉัน
+field_comments: ความเห็น
+field_url: URL
+field_start_page: หน้าเริ่มต้น
+field_subproject: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢
+field_hours: ชั่วโมง
+field_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡
+field_spent_on: วันที่
+field_identifier: ชื่อเฉพาะ
+field_is_filter: ใช้เป็นตัวà¸à¸£à¸­à¸‡
+field_issue_to_id: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง
+field_delay: เลื่อน
+field_assignable: ปัà¸à¸«à¸²à¸ªà¸²à¸¡à¸²à¸£à¸–มอบหมายให้คนที่ทำบทบาทนี้
+field_redirect_existing_links: ย้ายจุดเชื่อมโยงนี้
+field_estimated_hours: เวลาที่ใช้โดยประมาณ
+field_column_names: สดมภ์
+field_time_zone: ย่านเวลา
+field_searchable: ค้นหาได้
+field_default_value: ค่าเริ่มต้น
+field_comments_sorting: à¹à¸ªà¸”งความเห็น
+
+setting_app_title: ชื่อโปรà¹à¸à¸£à¸¡
+setting_app_subtitle: ชื่อโปรà¹à¸à¸£à¸¡à¸£à¸­à¸‡
+setting_welcome_text: ข้อความต้อนรับ
+setting_default_language: ภาษาเริ่มต้น
+setting_login_required: ต้องป้อนผู้ใช้-รหัสผ่าน
+setting_self_registration: ลงทะเบียนด้วยตนเอง
+setting_attachment_max_size: ขนาดà¹à¸Ÿà¹‰à¸¡à¹à¸™à¸šà¸ªà¸¹à¸‡à¸ªà¸¸à¸”
+setting_issues_export_limit: à¸à¸²à¸£à¸ªà¹ˆà¸‡à¸­à¸­à¸à¸›à¸±à¸à¸«à¸²à¸ªà¸¹à¸‡à¸ªà¸¸à¸”
+setting_mail_from: อีเมล์ที่ใช้ส่ง
+setting_bcc_recipients: ไม่ระบุชื่อผู้รับ (bcc)
+setting_host_name: ชื่อโฮสต์
+setting_text_formatting: à¸à¸²à¸£à¸ˆà¸±à¸”รูปà¹à¸šà¸šà¸‚้อความ
+setting_wiki_compression: บีบอัดประวัติ Wiki
+setting_feeds_limit: จำนวน Feed
+setting_default_projects_public: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆà¸¡à¸µà¸„่าเริ่มต้นเป็น สาธารณะ
+setting_autofetch_changesets: ดึง commits อัตโนมัติ
+setting_sys_api_enabled: เปิดใช้ WS สำหรับà¸à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸—ี่เà¸à¹‡à¸šà¸•้นฉบับ
+setting_commit_ref_keywords: คำสำคัภReferencing
+setting_commit_fix_keywords: คำสำคัภFixing
+setting_autologin: เข้าระบบอัตโนมัติ
+setting_date_format: รูปà¹à¸šà¸šà¸§à¸±à¸™à¸—ี่
+setting_time_format: รูปà¹à¸šà¸šà¹€à¸§à¸¥à¸²
+setting_cross_project_issue_relations: อนุà¸à¸²à¸•ให้ระบุปัà¸à¸«à¸²à¸‚้ามโครงà¸à¸²à¸£
+setting_issue_list_default_columns: สดมภ์เริ่มต้นà¹à¸ªà¸”งในรายà¸à¸²à¸£à¸›à¸±à¸à¸«à¸²
+setting_repositories_encodings: à¸à¸²à¸£à¹€à¸‚้ารหัสที่เà¸à¹‡à¸šà¸•้นฉบับ
+setting_emails_footer: คำลงท้ายอีเมล์
+setting_protocol: Protocol
+setting_per_page_options: ตัวเลือà¸à¸ˆà¸³à¸™à¸§à¸™à¸•่อหน้า
+setting_user_format: รูปà¹à¸šà¸šà¸à¸²à¸£à¹à¸ªà¸”งชื่อผู้ใช้
+setting_activity_days_default: จำนวนวันที่à¹à¸ªà¸”งในà¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸‚องโครงà¸à¸²à¸£
+setting_display_subprojects_issues: à¹à¸ªà¸”งปัà¸à¸«à¸²à¸‚องโครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢à¹ƒà¸™à¹‚ครงà¸à¸²à¸£à¸«à¸¥à¸±à¸
+
+project_module_issue_tracking: à¸à¸²à¸£à¸•ิดตามปัà¸à¸«à¸²
+project_module_time_tracking: à¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸²
+project_module_news: ข่าว
+project_module_documents: เอà¸à¸ªà¸²à¸£
+project_module_files: à¹à¸Ÿà¹‰à¸¡
+project_module_wiki: Wiki
+project_module_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ
+project_module_boards: à¸à¸£à¸°à¸”านข้อความ
+
+label_user: ผู้ใช้
+label_user_plural: ผู้ใช้
+label_user_new: ผู้ใช้ใหม่
+label_project: โครงà¸à¸²à¸£
+label_project_new: โครงà¸à¸²à¸£à¹ƒà¸«à¸¡à¹ˆ
+label_project_plural: โครงà¸à¸²à¸£
+label_project_all: โครงà¸à¸²à¸£à¸—ั้งหมด
+label_project_latest: โครงà¸à¸²à¸£à¸¥à¹ˆà¸²à¸ªà¸¸à¸”
+label_issue: ปัà¸à¸«à¸²
+label_issue_new: ปัà¸à¸«à¸²à¹ƒà¸«à¸¡à¹ˆ
+label_issue_plural: ปัà¸à¸«à¸²
+label_issue_view_all: ดูปัà¸à¸«à¸²à¸—ั้งหมด
+label_issues_by: ปัà¸à¸«à¸²à¹‚ดย %s
+label_issue_added: ปัà¸à¸«à¸²à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_issue_updated: ปัà¸à¸«à¸²à¸–ูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡
+label_document: เอà¸à¸ªà¸²à¸£
+label_document_new: เอà¸à¸ªà¸²à¸£à¹ƒà¸«à¸¡à¹ˆ
+label_document_plural: เอà¸à¸ªà¸²à¸£
+label_document_added: เอà¸à¸ªà¸²à¸£à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_role: บทบาท
+label_role_plural: บทบาท
+label_role_new: บทบาทใหม่
+label_role_and_permissions: บทบาทà¹à¸¥à¸°à¸ªà¸´à¸—ธิ
+label_member: สมาชิà¸
+label_member_new: สมาชิà¸à¹ƒà¸«à¸¡à¹ˆ
+label_member_plural: สมาชิà¸
+label_tracker: à¸à¸²à¸£à¸•ิดตาม
+label_tracker_plural: à¸à¸²à¸£à¸•ิดตาม
+label_tracker_new: à¸à¸²à¸£à¸•ิดตามใหม่
+label_workflow: ลำดับงาน
+label_issue_status: สถานะของปัà¸à¸«à¸²
+label_issue_status_plural: สถานะของปัà¸à¸«à¸²
+label_issue_status_new: สถานะใหม
+label_issue_category: ประเภทของปัà¸à¸«à¸²
+label_issue_category_plural: ประเภทของปัà¸à¸«à¸²
+label_issue_category_new: ประเภทใหม่
+label_custom_field: เขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡
+label_custom_field_plural: เขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡
+label_custom_field_new: สร้างเขตข้อมูลà¹à¸šà¸šà¸£à¸°à¸šà¸¸à¹€à¸­à¸‡
+label_enumerations: รายà¸à¸²à¸£
+label_enumeration_new: สร้างใหม่
+label_information: ข้อมูล
+label_information_plural: ข้อมูล
+label_please_login: à¸à¸£à¸¸à¸“าเข้าระบบà¸à¹ˆà¸­à¸™
+label_register: ลงทะเบียน
+label_password_lost: ลืมรหัสผ่าน
+label_home: หน้าà¹à¸£à¸
+label_my_page: หน้าของฉัน
+label_my_account: บัà¸à¸Šà¸µà¸‚องฉัน
+label_my_projects: โครงà¸à¸²à¸£à¸‚องฉัน
+label_administration: บริหารจัดà¸à¸²à¸£
+label_login: เข้าระบบ
+label_logout: ออà¸à¸£à¸°à¸šà¸š
+label_help: ช่วยเหลือ
+label_reported_issues: ปัà¸à¸«à¸²à¸—ี่à¹à¸ˆà¹‰à¸‡à¹„ว้
+label_assigned_to_me_issues: ปัà¸à¸«à¸²à¸—ี่มอบหมายให้ฉัน
+label_last_login: ติดต่อครั้งสุดท้าย
+label_last_updates: ปรับปรุงครั้งสุดท้าย
+label_last_updates_plural: %d ปรับปรุงครั้งสุดท้าย
+label_registered_on: ลงทะเบียนเมื่อ
+label_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡
+label_activity_plural: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡
+label_activity_latest: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¸¥à¹ˆà¸²à¸ªà¸¸à¸”
+label_overall_activity: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡à¹‚ดยรวม
+label_new: ใหม่
+label_logged_as: เข้าระบบในชื่อ
+label_environment: สภาพà¹à¸§à¸”ล้อม
+label_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน
+label_auth_source: วิธีà¸à¸²à¸£à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตน
+label_auth_source_new: สร้างวิธีà¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนใหม่
+label_auth_source_plural: วิธีà¸à¸²à¸£ Authentication
+label_subproject_plural: โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢
+label_min_max_length: สั้น-ยาว สุดที่
+label_list: รายà¸à¸²à¸£
+label_date: วันที่
+label_integer: จำนวนเต็ม
+label_float: จำนวนจริง
+label_boolean: ถูà¸à¸œà¸´à¸”
+label_string: ข้อความ
+label_text: ข้อความขนาดยาว
+label_attribute: คุณลัà¸à¸©à¸“ะ
+label_attribute_plural: คุณลัà¸à¸©à¸“ะ
+label_download: %d ดาวน์โหลด
+label_download_plural: %d ดาวน์โหลด
+label_no_data: จำนวนข้อมูลที่à¹à¸ªà¸”ง
+label_change_status: เปลี่ยนสถานะ
+label_history: ประวัติ
+label_attachment: à¹à¸Ÿà¹‰à¸¡
+label_attachment_new: à¹à¸Ÿà¹‰à¸¡à¹ƒà¸«à¸¡à¹ˆ
+label_attachment_delete: ลบà¹à¸Ÿà¹‰à¸¡
+label_attachment_plural: à¹à¸Ÿà¹‰à¸¡
+label_file_added: à¹à¸Ÿà¹‰à¸¡à¸–ูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_report: รายงาน
+label_report_plural: รายงาน
+label_news: ข่าว
+label_news_new: เพิ่มข่าว
+label_news_plural: ข่าว
+label_news_latest: ข่าวล่าสุด
+label_news_view_all: ดูข่าวทั้งหมด
+label_news_added: ข่าวถูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_change_log: บันทึà¸à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡
+label_settings: ปรับà¹à¸•่ง
+label_overview: ภาพรวม
+label_version: รุ่น
+label_version_new: รุ่นใหม่
+label_version_plural: รุ่น
+label_confirmation: ยืนยัน
+label_export_to: 'รูปà¹à¸šà¸šà¸­à¸·à¹ˆà¸™à¹† :'
+label_read: อ่าน...
+label_public_projects: โครงà¸à¸²à¸£à¸ªà¸²à¸˜à¸²à¸£à¸“ะ
+label_open_issues: เปิด
+label_open_issues_plural: เปิด
+label_closed_issues: ปิด
+label_closed_issues_plural: ปิด
+label_total: จำนวนรวม
+label_permissions: สิทธิ
+label_current_status: สถานะปัจจุบัน
+label_new_statuses_allowed: อนุà¸à¸²à¸•ให้มีสถานะใหม่
+label_all: ทั้งหมด
+label_none: ไม่มี
+label_nobody: ไม่มีใคร
+label_next: ต่อไป
+label_previous: à¸à¹ˆà¸­à¸™à¸«à¸™à¹‰à¸²
+label_used_by: ถูà¸à¹ƒà¸Šà¹‰à¹‚ดย
+label_details: รายละเอียด
+label_add_note: เพิ่มบันทึà¸
+label_per_page: ต่อหน้า
+label_calendar: ปà¸à¸´à¸—ิน
+label_months_from: เดือนจาà¸
+label_gantt: Gantt
+label_internal: ภายใน
+label_last_changes: last %d เปลี่ยนà¹à¸›à¸¥à¸‡
+label_change_view_all: ดูà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด
+label_personalize_page: ปรับà¹à¸•่งหน้านี้
+label_comment: ความเห็น
+label_comment_plural: ความเห็น
+label_comment_add: เพิ่มความเห็น
+label_comment_added: ความเห็นถูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_comment_delete: ลบความเห็น
+label_query: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เอง
+label_query_plural: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามà¹à¸šà¸šà¸à¸³à¸«à¸™à¸”เอง
+label_query_new: à¹à¸šà¸šà¸ªà¸­à¸šà¸–ามใหม่
+label_filter_add: เพิ่มตัวà¸à¸£à¸­à¸‡
+label_filter_plural: ตัวà¸à¸£à¸­à¸‡
+label_equals: คือ
+label_not_equals: ไม่ใช่
+label_in_less_than: น้อยà¸à¸§à¹ˆà¸²
+label_in_more_than: มาà¸à¸à¸§à¹ˆà¸²
+label_in: ในช่วง
+label_today: วันนี้
+label_all_time: ตลอดเวลา
+label_yesterday: เมื่อวาน
+label_this_week: อาทิตย์นี้
+label_last_week: อาทิตย์ที่à¹à¸¥à¹‰à¸§
+label_last_n_days: %d วันย้อนหลัง
+label_this_month: เดือนนี้
+label_last_month: เดือนที่à¹à¸¥à¹‰à¸§
+label_this_year: ปีนี้
+label_date_range: ช่วงวันที่
+label_less_than_ago: น้อยà¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™
+label_more_than_ago: มาà¸à¸à¸§à¹ˆà¸²à¸«à¸™à¸¶à¹ˆà¸‡à¸§à¸±à¸™
+label_ago: วันผ่านมาà¹à¸¥à¹‰à¸§
+label_contains: มี...
+label_not_contains: ไม่มี...
+label_day_plural: วัน
+label_repository: ที่เà¸à¹‡à¸šà¸•้นฉบับ
+label_repository_plural: ที่เà¸à¹‡à¸šà¸•้นฉบับ
+label_browse: เปิดหา
+label_modification: %d เปลี่ยนà¹à¸›à¸¥à¸‡
+label_modification_plural: %d เปลี่ยนà¹à¸›à¸¥à¸‡
+label_revision: à¸à¸²à¸£à¹à¸à¹‰à¹„ข
+label_revision_plural: à¸à¸²à¸£à¹à¸à¹‰à¹„ข
+label_associated_revisions: à¸à¸²à¸£à¹à¸à¹‰à¹„ขที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง
+label_added: ถูà¸à¹€à¸žà¸´à¹ˆà¸¡
+label_modified: ถูà¸à¹à¸à¹‰à¹„ข
+label_deleted: ถูà¸à¸¥à¸š
+label_latest_revision: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด
+label_latest_revision_plural: รุ่นà¸à¸²à¸£à¹à¸à¹‰à¹„ขล่าสุด
+label_view_revisions: ดูà¸à¸²à¸£à¹à¸à¹‰à¹„ข
+label_max_size: ขนาดใหà¸à¹ˆà¸ªà¸¸à¸”
+label_on: 'ใน'
+label_sort_highest: ย้ายไปบนสุด
+label_sort_higher: ย้ายขึ้น
+label_sort_lower: ย้ายลง
+label_sort_lowest: ย้ายไปล่างสุด
+label_roadmap: à¹à¸œà¸™à¸‡à¸²à¸™
+label_roadmap_due_in: ถึงà¸à¸³à¸«à¸™à¸”ใน
+label_roadmap_overdue: %s ช้าà¸à¸§à¹ˆà¸²à¸à¸³à¸«à¸™à¸”
+label_roadmap_no_issues: ไม่มีปัà¸à¸«à¸²à¸ªà¸³à¸«à¸£à¸±à¸šà¸£à¸¸à¹ˆà¸™à¸™à¸µà¹‰
+label_search: ค้นหา
+label_result_plural: ผลà¸à¸²à¸£à¸„้นหา
+label_all_words: ทุà¸à¸„ำ
+label_wiki: Wiki
+label_wiki_edit: à¹à¸à¹‰à¹„ข Wiki
+label_wiki_edit_plural: à¹à¸à¹‰à¹„ข Wiki
+label_wiki_page: หน้า Wiki
+label_wiki_page_plural: หน้า Wiki
+label_index_by_title: เรียงตามชื่อเรื่อง
+label_index_by_date: เรียงตามวัน
+label_current_version: รุ่นปัจจุบัน
+label_preview: ตัวอย่างà¸à¹ˆà¸­à¸™à¸ˆà¸±à¸”เà¸à¹‡à¸š
+label_feed_plural: Feeds
+label_changes_details: รายละเอียดà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸—ั้งหมด
+label_issue_tracking: ติดตามปัà¸à¸«à¸²
+label_spent_time: เวลาที่ใช้
+label_f_hour: %.2f ชั่วโมง
+label_f_hour_plural: %.2f ชั่วโมง
+label_time_tracking: ติดตามà¸à¸²à¸£à¹ƒà¸Šà¹‰à¹€à¸§à¸¥à¸²
+label_change_plural: เปลี่ยนà¹à¸›à¸¥à¸‡
+label_statistics: สถิติ
+label_commits_per_month: Commits ต่อเดือน
+label_commits_per_author: Commits ต่อผู้à¹à¸•่ง
+label_view_diff: ดูความà¹à¸•à¸à¸•่าง
+label_diff_inline: inline
+label_diff_side_by_side: side by side
+label_options: ตัวเลือà¸
+label_copy_workflow_from: คัดลอà¸à¸¥à¸³à¸”ับงานจาà¸
+label_permissions_report: รายงานสิทธิ
+label_watched_issues: เà¸à¹‰à¸²à¸”ูปัà¸à¸«à¸²
+label_related_issues: ปัà¸à¸«à¸²à¸—ี่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง
+label_applied_status: จัดเà¸à¹‡à¸šà¸ªà¸–านะ
+label_loading: à¸à¸³à¸¥à¸±à¸‡à¹‚หลด...
+label_relation_new: ความสัมพันธ์ใหม่
+label_relation_delete: ลบความสัมพันธ์
+label_relates_to: สัมพันธ์à¸à¸±à¸š
+label_duplicates: ซ้ำ
+label_blocks: à¸à¸µà¸”à¸à¸±à¸™
+label_blocked_by: à¸à¸µà¸”à¸à¸±à¸™à¹‚ดย
+label_precedes: นำหน้า
+label_follows: ตามหลัง
+label_end_to_start: จบ-เริ่ม
+label_end_to_end: จบ-จบ
+label_start_to_start: เริ่ม-เริ่ม
+label_start_to_end: เริ่ม-จบ
+label_stay_logged_in: อยู่ในระบบต่อ
+label_disabled: ไม่ใช้งาน
+label_show_completed_versions: à¹à¸ªà¸”งรุ่นที่สมบูรณ์
+label_me: ฉัน
+label_board: สภาà¸à¸²à¹à¸Ÿ
+label_board_new: สร้างสภาà¸à¸²à¹à¸Ÿ
+label_board_plural: สภาà¸à¸²à¹à¸Ÿ
+label_topic_plural: หัวข้อ
+label_message_plural: ข้อความ
+label_message_last: ข้อความล่าสุด
+label_message_new: เขียนข้อความใหม่
+label_message_posted: ข้อความถูà¸à¹€à¸žà¸´à¹ˆà¸¡à¹à¸¥à¹‰à¸§
+label_reply_plural: ตอบà¸à¸¥à¸±à¸š
+label_send_information: ส่งรายละเอียดของบัà¸à¸Šà¸µà¹ƒà¸«à¹‰à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰
+label_year: ปี
+label_month: เดือน
+label_week: สัปดาห์
+label_date_from: จาà¸
+label_date_to: ถึง
+label_language_based: ขึ้นอยู่à¸à¸±à¸šà¸ à¸²à¸©à¸²à¸‚องผู้ใช้
+label_sort_by: เรียงโดย %s
+label_send_test_email: ส่งจดหมายทดสอบ
+label_feeds_access_key_created_on: RSS access key สร้างเมื่อ %s ที่ผ่านมา
+label_module_plural: ส่วนประà¸à¸­à¸š
+label_added_time_by: เพิ่มโดย %s %s ที่ผ่านมา
+label_updated_time: ปรับปรุง %s ที่ผ่านมา
+label_jump_to_a_project: ไปที่โครงà¸à¸²à¸£...
+label_file_plural: à¹à¸Ÿà¹‰à¸¡
+label_changeset_plural: à¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡
+label_default_columns: สดมภ์เริ่มต้น
+label_no_change_option: (ไม่เปลี่ยนà¹à¸›à¸¥à¸‡)
+label_bulk_edit_selected_issues: à¹à¸à¹‰à¹„ขปัà¸à¸«à¸²à¸—ี่เลือà¸à¸—ั้งหมด
+label_theme: ชุดรูปà¹à¸šà¸š
+label_default: ค่าเริ่มต้น
+label_search_titles_only: ค้นหาจาà¸à¸Šà¸·à¹ˆà¸­à¹€à¸£à¸·à¹ˆà¸­à¸‡à¹€à¸—่านั้น
+label_user_mail_option_all: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸‚องฉัน"
+label_user_mail_option_selected: "ทุà¸à¹† เหตุà¸à¸²à¸£à¸“์ในโครงà¸à¸²à¸£à¸—ี่เลือà¸..."
+label_user_mail_option_none: "เฉพาะสิ่งที่ฉันเลือà¸à¸«à¸£à¸·à¸­à¸¡à¸µà¸ªà¹ˆà¸§à¸™à¹€à¸à¸µà¹ˆà¸¢à¸§à¸‚้อง"
+label_user_mail_no_self_notified: "ฉันไม่ต้องà¸à¸²à¸£à¹„ด้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนในสิ่งที่ฉันทำเอง"
+label_registration_activation_by_email: เปิดบัà¸à¸Šà¸µà¸œà¹ˆà¸²à¸™à¸­à¸µà¹€à¸¡à¸¥à¹Œ
+label_registration_manual_activation: อนุมัติโดยผู้บริหารจัดà¸à¸²à¸£
+label_registration_automatic_activation: เปิดบัà¸à¸Šà¸µà¸­à¸±à¸•โนมัติ
+label_display_per_page: 'ต่อหน้า: %s'
+label_age: อายุ
+label_change_properties: เปลี่ยนคุณสมบัติ
+label_general: ทั่วๆ ไป
+label_more: อื่น ๆ
+label_scm: ตัวจัดà¸à¸²à¸£à¸•้นฉบับ
+label_plugins: ส่วนเสริม
+label_ldap_authentication: à¸à¸²à¸£à¸¢à¸·à¸™à¸¢à¸±à¸™à¸•ัวตนโดยใช้ LDAP
+label_downloads_abbr: D/L
+label_optional_description: รายละเอียดเพิ่มเติม
+label_add_another_file: เพิ่มà¹à¸Ÿà¹‰à¸¡à¸­à¸·à¹ˆà¸™à¹†
+label_preferences: ค่าที่ชอบใจ
+label_chronological_order: เรียงจาà¸à¹€à¸à¹ˆà¸²à¹„ปใหม่
+label_reverse_chronological_order: เรียงจาà¸à¹ƒà¸«à¸¡à¹ˆà¹„ปเà¸à¹ˆà¸²
+label_planning: à¸à¸²à¸£à¸§à¸²à¸‡à¹à¸œà¸™
+
+button_login: เข้าระบบ
+button_submit: จัดส่งข้อมูล
+button_save: จัดเà¸à¹‡à¸š
+button_check_all: เลือà¸à¸—ั้งหมด
+button_uncheck_all: ไม่เลือà¸à¸—ั้งหมด
+button_delete: ลบ
+button_create: สร้าง
+button_test: ทดสอบ
+button_edit: à¹à¸à¹‰à¹„ข
+button_add: เพิ่ม
+button_change: เปลี่ยนà¹à¸›à¸¥à¸‡
+button_apply: ประยุà¸à¸•์ใช้
+button_clear: ล้างข้อความ
+button_lock: ล็อค
+button_unlock: ยà¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¥à¹‡à¸­à¸„
+button_download: ดาวน์โหลด
+button_list: รายà¸à¸²à¸£
+button_view: มุมมอง
+button_move: ย้าย
+button_back: à¸à¸¥à¸±à¸š
+button_cancel: ยà¸à¹€à¸¥à¸´à¸
+button_activate: เปิดใช้
+button_sort: จัดเรียง
+button_log_time: บันทึà¸à¹€à¸§à¸¥à¸²
+button_rollback: ถอยà¸à¸¥à¸±à¸šà¸¡à¸²à¸—ี่รุ่นนี้
+button_watch: เà¸à¹‰à¸²à¸”ู
+button_unwatch: เลิà¸à¹€à¸à¹‰à¸²à¸”ู
+button_reply: ตอบà¸à¸¥à¸±à¸š
+button_archive: เà¸à¹‡à¸šà¹€à¸‚้าโà¸à¸”ัง
+button_unarchive: เอาออà¸à¸ˆà¸²à¸à¹‚à¸à¸”ัง
+button_reset: เริ่มใหมท
+button_rename: เปลี่ยนชื่อ
+button_change_password: เปลี่ยนรหัสผ่าน
+button_copy: คัดลอà¸
+button_annotate: หมายเหตุประà¸à¸­à¸š
+button_update: ปรับปรุง
+button_configure: ปรับà¹à¸•่ง
+
+status_active: เปิดใช้งานà¹à¸¥à¹‰à¸§
+status_registered: รอà¸à¸²à¸£à¸­à¸™à¸¸à¸¡à¸±à¸•ิ
+status_locked: ล็อค
+
+text_select_mail_notifications: เลือà¸à¸à¸²à¸£à¸à¸£à¸°à¸—ำที่ต้องà¸à¸²à¸£à¹ƒà¸«à¹‰à¸ªà¹ˆà¸‡à¸­à¸µà¹€à¸¡à¸¥à¹Œà¹à¸ˆà¹‰à¸‡.
+text_regexp_info: ตัวอย่าง ^[A-Z0-9]+$
+text_min_max_length_info: 0 หมายถึงไม่จำà¸à¸±à¸”
+text_project_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้องà¸à¸²à¸£à¸¥à¸šà¹‚ครงà¸à¸²à¸£à¹à¸¥à¸°à¸‚้อมูลที่เà¸à¸µà¹ˆà¸¢à¸§à¸‚้่อง ?
+text_subprojects_destroy_warning: 'โครงà¸à¸²à¸£à¸¢à¹ˆà¸­à¸¢: %s จะถูà¸à¸¥à¸šà¸”้วย.'
+text_workflow_edit: เลือà¸à¸šà¸—บาทà¹à¸¥à¸°à¸à¸²à¸£à¸•ิดตาม เพื่อà¹à¸à¹‰à¹„ขลำดับงาน
+text_are_you_sure: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หม ?
+text_journal_changed: เปลี่ยนà¹à¸›à¸¥à¸‡à¸ˆà¸²à¸ %s เป็น %s
+text_journal_set_to: ตั้งค่าเป็น %s
+text_journal_deleted: ถูà¸à¸¥à¸š
+text_tip_task_begin_day: งานที่เริ่มวันนี้
+text_tip_task_end_day: งานที่จบวันนี้
+text_tip_task_begin_end_day: งานที่เริ่มà¹à¸¥à¸°à¸ˆà¸šà¸§à¸±à¸™à¸™à¸µà¹‰
+text_project_identifier_info: 'ภาษาอังà¸à¸¤à¸©à¸•ัวเล็à¸(a-z), ตัวเลข(0-9) à¹à¸¥à¸°à¸‚ีด (-) เท่านั้น.<br />เมื่อจัดเà¸à¹‡à¸šà¹à¸¥à¹‰à¸§, ชื่อเฉพาะไม่สามารถเปลี่ยนà¹à¸›à¸¥à¸‡à¹„ด้'
+text_caracters_maximum: สูงสุด %d ตัวอัà¸à¸©à¸£.
+text_caracters_minimum: ต้องยาวอย่างน้อย %d ตัวอัà¸à¸©à¸£.
+text_length_between: ความยาวระหว่าง %d ถึง %d ตัวอัà¸à¸©à¸£.
+text_tracker_no_workflow: ไม่ได้บัà¸à¸à¸±à¸•ิลำดับงานสำหรับà¸à¸²à¸£à¸•ิดตามนี้
+text_unallowed_characters: ตัวอัà¸à¸©à¸£à¸•้องห้าม
+text_comma_separated: ใส่ได้หลายค่า โดยคั่นด้วยลูà¸à¸™à¹‰à¸³( ,).
+text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
+text_issue_added: ปัà¸à¸«à¸² %s ถูà¸à¹à¸ˆà¹‰à¸‡à¹‚ดย %s.
+text_issue_updated: ปัà¸à¸«à¸² %s ถูà¸à¸›à¸£à¸±à¸šà¸›à¸£à¸¸à¸‡à¹‚ดย %s.
+text_wiki_destroy_confirmation: คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¸§à¹ˆà¸²à¸•้องà¸à¸²à¸£à¸¥à¸š wiki นี้พร้อมทั้งเนี้อหา?
+text_issue_category_destroy_question: บางปัà¸à¸«à¸² (%d) อยู่ในประเภทนี้. คุณต้องà¸à¸²à¸£à¸—ำอย่างไร ?
+text_issue_category_destroy_assignments: ลบประเภทนี้
+text_issue_category_reassign_to: ระบุปัà¸à¸«à¸²à¹ƒà¸™à¸›à¸£à¸°à¹€à¸ à¸—นี้
+text_user_mail_option: "ในโครงà¸à¸²à¸£à¸—ี่ไม่ได้เลือà¸, คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸ªà¸´à¹ˆà¸‡à¸—ี่คุณเà¸à¹‰à¸²à¸”ูหรือมีส่วนเà¸à¸µà¹ˆà¸¢à¸§à¸‚้อง (เช่นปัà¸à¸«à¸²à¸—ี่คุณà¹à¸ˆà¹‰à¸‡à¹„ว้หรือได้รับมอบหมาย)."
+text_no_configuration_data: "บทบาท, à¸à¸²à¸£à¸•ิดตาม, สถานะปัà¸à¸«à¸² à¹à¸¥à¸°à¸¥à¸³à¸”ับงานยังไม่ได้ถูà¸à¸•ั้งค่า.\nขอà¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹‚หลดค่าเริ่มต้น. คุณสามารถà¹à¸à¹‰à¹„ขค่าได้หลังจาà¸à¹‚หลดà¹à¸¥à¹‰à¸§."
+text_load_default_configuration: โหลดค่าเริ่มต้น
+text_status_changed_by_changeset: ประยุà¸à¸•์ใช้ในà¸à¸¥à¸¸à¹ˆà¸¡à¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡ %s.
+text_issues_destroy_confirmation: 'คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¹„หมว่าต้องà¸à¸²à¸£à¸¥à¸šà¸›à¸±à¸à¸«à¸²(ทั้งหลาย)ที่เลือà¸à¹„ว้?'
+text_select_project_modules: 'เลือà¸à¸ªà¹ˆà¸§à¸™à¸›à¸£à¸°à¸à¸­à¸šà¸—ี่ต้องà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸ªà¸³à¸«à¸£à¸±à¸šà¹‚ครงà¸à¸²à¸£à¸™à¸µà¹‰:'
+text_default_administrator_account_changed: ค่าเริ่มต้นของบัà¸à¸Šà¸µà¸œà¸¹à¹‰à¸šà¸£à¸´à¸«à¸²à¸£à¸ˆà¸±à¸”à¸à¸²à¸£à¸–ูà¸à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡
+text_file_repository_writable: ที่เà¸à¹‡à¸šà¸•้นฉบับสามารถเขียนได้
+text_rmagick_available: RMagick มีให้ใช้ (เป็นตัวเลือà¸)
+text_destroy_time_entries_question: %.02f ชั่วโมงที่ถูà¸à¹à¸ˆà¹‰à¸‡à¹ƒà¸™à¸›à¸±à¸à¸«à¸²à¸™à¸µà¹‰à¸ˆà¸°à¹‚ดนลบ. คุณต้องà¸à¸²à¸£à¸—ำอย่างไร?
+text_destroy_time_entries: ลบเวลาที่รายงานไว้
+text_assign_time_entries_to_project: ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹‰
+text_reassign_time_entries: 'ระบุเวลาที่ใช้ในโครงà¸à¸²à¸£à¸™à¸µà¹ˆà¸­à¸µà¸à¸„รั้ง:'
+
+default_role_manager: ผู้จัดà¸à¸²à¸£
+default_role_developper: ผู้พัฒนา
+default_role_reporter: ผู้รายงาน
+default_tracker_bug: บั๊à¸
+default_tracker_feature: ลัà¸à¸©à¸“ะเด่น
+default_tracker_support: สนับสนุน
+default_issue_status_new: เà¸à¸´à¸”ขึ้น
+default_issue_status_assigned: รับมอบหมาย
+default_issue_status_resolved: ดำเนินà¸à¸²à¸£
+default_issue_status_feedback: รอคำตอบ
+default_issue_status_closed: จบ
+default_issue_status_rejected: ยà¸à¹€à¸¥à¸´à¸
+default_doc_category_user: เอà¸à¸ªà¸²à¸£à¸‚องผู้ใช้
+default_doc_category_tech: เอà¸à¸ªà¸²à¸£à¸—างเทคนิค
+default_priority_low: ต่ำ
+default_priority_normal: ปà¸à¸•ิ
+default_priority_high: สูง
+default_priority_urgent: เร่งด่วน
+default_priority_immediate: ด่วนมาà¸
+default_activity_design: ออà¸à¹à¸šà¸š
+default_activity_development: พัฒนา
+
+enumeration_issue_priorities: ความสำคัà¸à¸‚องปัà¸à¸«à¸²
+enumeration_doc_categories: ประเภทเอà¸à¸ªà¸²à¸£
+enumeration_activities: à¸à¸´à¸ˆà¸à¸£à¸£à¸¡ (ใช้ในà¸à¸²à¸£à¸•ิดตามเวลา)
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/uk.yml b/groups/lang/uk.yml
index a52a05603..7ba152413 100644
--- a/groups/lang/uk.yml
+++ b/groups/lang/uk.yml
@@ -48,6 +48,7 @@ general_text_no: 'ÐÑ–'
general_text_yes: 'Так'
general_lang_name: 'Ukrainian (УкраїнÑька)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: UTF-8
general_pdf_encoding: UTF-8
general_day_names: Понеділок,Вівторок,Середа,Четвер,П'ÑтницÑ,Субота,ÐеділÑ
@@ -620,3 +621,20 @@ setting_default_projects_public: New projects are public by default
error_scm_annotate: "The entry does not exist or can not be annotated."
label_planning: Planning
text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+label_and_its_subprojects: %s and its subprojects
+mail_body_reminder: "%d issue(s) that are assigned to you are due in the next %d days:"
+mail_subject_reminder: "%d issue(s) due in the next days"
+text_user_wrote: '%s wrote:'
+label_duplicated_by: duplicated by
+setting_enabled_scm: Enabled SCM
+text_enumeration_category_reassign_to: 'Reassign them to this value:'
+text_enumeration_destroy_question: '%d objects are assigned to this value.'
+label_incoming_emails: Incoming emails
+label_generate_key: Generate a key
+setting_mail_handler_api_enabled: Enable WS for incoming emails
+setting_mail_handler_api_key: API key
+text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/email.yml and restart the application to enable them."
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/zh-tw.yml b/groups/lang/zh-tw.yml
index a0c7fafb3..6a1441364 100644
--- a/groups/lang/zh-tw.yml
+++ b/groups/lang/zh-tw.yml
@@ -48,6 +48,7 @@ general_text_no: 'å¦'
general_text_yes: '是'
general_lang_name: 'Traditional Chinese (ç¹é«”中文)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: Big5
general_pdf_encoding: Big5
general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日
@@ -91,6 +92,8 @@ mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨ "%s" 帳號登入 Redmin
mail_body_account_information: 您的 Redmine 帳號資訊
mail_subject_account_activation_request: Redmine 帳號啟用需求通知
mail_body_account_activation_request: 'æœ‰ä½æ–°ç”¨æˆ¶ (%s) 已經完æˆè¨»å†Šï¼Œæ­£ç­‰å€™æ‚¨çš„審核:'
+mail_subject_reminder: "您有 %d 個項目å³å°‡åˆ°æœŸ"
+mail_body_reminder: "%d 個指派給您的項目,將於 %d 天之內到期:"
gui_validation_error: 1 個錯誤
gui_validation_error_plural: %d 個錯誤
@@ -110,7 +113,7 @@ field_created_on: 建立日期
field_updated_on: æ›´æ–°
field_field_format: æ ¼å¼
field_is_for_all: 給所有專案
-field_possible_values: Possible values
+field_possible_values: å¯èƒ½å€¼
field_regexp: æ­£è¦è¡¨ç¤ºå¼
field_min_length: 最å°é•·åº¦
field_max_length: 最大長度
@@ -153,9 +156,9 @@ field_account: 帳戶
field_base_dn: Base DN
field_attr_login: 登入屬性
field_attr_firstname: å字屬性
-field_attr_lastname: Lastname attribute
-field_attr_mail: Email attribute
-field_onthefly: On-the-fly user creation
+field_attr_lastname: å§“æ°å±¬æ€§
+field_attr_mail: é›»å­éƒµä»¶ä¿¡ç®±å±¬æ€§
+field_onthefly: 峿™‚建立使用者
field_start_date: 開始日期
field_done_ratio: 完æˆç™¾åˆ†æ¯”
field_auth_source: èªè­‰æ¨¡å¼
@@ -168,13 +171,13 @@ field_hours: å°æ™‚
field_activity: 活動
field_spent_on: 日期
field_identifier: 代碼
-field_is_filter: Used as a filter
-field_issue_to_id: Related issue
+field_is_filter: ç”¨ä¾†ä½œç‚ºéŽæ¿¾å™¨
+field_issue_to_id: 相關項目
field_delay: 逾期
field_assignable: é …ç›®å¯è¢«åˆ†æ´¾è‡³æ­¤è§’色
-field_redirect_existing_links: Redirect existing links
+field_redirect_existing_links: 釿–°å°Žå‘ç¾æœ‰é€£çµ
field_estimated_hours: é ä¼°å·¥æ™‚
-field_column_names: Columns
+field_column_names: 欄ä½
field_time_zone: 時å€
field_searchable: å¯ç”¨åšæœå°‹æ¢ä»¶
field_default_value: é è¨­å€¼
@@ -193,7 +196,7 @@ setting_bcc_recipients: 使用密件副本 (BCC)
setting_host_name: 主機å稱
setting_text_formatting: 文字格å¼
setting_wiki_compression: 壓縮 Wiki æ­·å²æ–‡ç« 
-setting_feeds_limit: Feed content limit
+setting_feeds_limit: RSS æ–°èžé™åˆ¶
setting_autofetch_changesets: 自動å–å¾—é€äº¤ç‰ˆæ¬¡
setting_default_projects_public: 新建立之專案é è¨­ç‚ºã€Œå…¬é–‹ã€
setting_sys_api_enabled: 啟用管ç†ç‰ˆæœ¬åº«ä¹‹ç¶²é æœå‹™ (Web Service)
@@ -210,7 +213,10 @@ setting_protocol: å”定
setting_per_page_options: æ¯é é¡¯ç¤ºå€‹æ•¸é¸é …
setting_user_format: 使用者顯示格å¼
setting_activity_days_default: 專案活動顯示天數
-setting_display_subprojects_issues: é è¨­æ–¼ä¸»æŽ§å°ˆæ¡ˆä¸­é¡¯ç¤ºå¾žå±¬å°ˆæ¡ˆçš„é …ç›®
+setting_display_subprojects_issues: é è¨­æ–¼çˆ¶å°ˆæ¡ˆä¸­é¡¯ç¤ºå­å°ˆæ¡ˆçš„é …ç›®
+setting_enabled_scm: 啟用的 SCM
+setting_mail_handler_api_enabled: 啟用處ç†å‚³å…¥é›»å­éƒµä»¶çš„æœå‹™
+setting_mail_handler_api_key: API 金鑰
project_module_issue_tracking: 項目追蹤
project_module_time_tracking: 工時追蹤
@@ -291,6 +297,7 @@ label_auth_source: èªè­‰æ¨¡å¼
label_auth_source_new: 建立新èªè­‰æ¨¡å¼
label_auth_source_plural: èªè­‰æ¨¡å¼æ¸…å–®
label_subproject_plural: å­å°ˆæ¡ˆ
+label_and_its_subprojects: %s 與其å­å°ˆæ¡ˆ
label_min_max_length: æœ€å° - 最大 長度
label_list: 清單
label_date: 日期
@@ -327,7 +334,7 @@ label_version_new: 建立新的版本
label_version_plural: 版本
label_confirmation: 確èª
label_export_to: 匯出至
-label_read: Read...
+label_read: 讀å–...
label_public_projects: 公開專案
label_open_issues: 進行中
label_open_issues_plural: 進行中
@@ -339,7 +346,7 @@ label_current_status: ç›®å‰ç‹€æ…‹
label_new_statuses_allowed: å¯è®Šæ›´è‡³ä»¥ä¸‹ç‹€æ…‹
label_all: 全部
label_none: 空值
-label_nobody: nobody
+label_nobody: ç„¡å
label_next: 下一é 
label_previous: 上一é 
label_used_by: Used by
@@ -349,7 +356,7 @@ label_per_page: æ¯é 
label_calendar: 日曆
label_months_from: 個月, 開始月份
label_gantt: 甘特圖
-label_internal: Internal
+label_internal: 內部
label_last_changes: 最近 %d 個變更
label_change_view_all: 檢視所有變更
label_personalize_page: 自訂版é¢
@@ -426,7 +433,7 @@ label_issue_tracking: 項目追蹤
label_spent_time: 耗用時間
label_f_hour: %.2f å°æ™‚
label_f_hour_plural: %.2f å°æ™‚
-label_time_tracking: Time tracking
+label_time_tracking: 工時追蹤
label_change_plural: 變更
label_statistics: 統計資訊
label_commits_per_month: 便œˆä»½çµ±è¨ˆé€äº¤æ¬¡æ•¸
@@ -445,14 +452,15 @@ label_relation_new: 建立新關è¯
label_relation_delete: 刪除關è¯
label_relates_to: é—œè¯è‡³
label_duplicates: å·²é‡è¤‡
+label_duplicated_by: èˆ‡å¾Œé¢æ‰€åˆ—é …ç›®é‡è¤‡
label_blocks: 阻擋
label_blocked_by: 被阻擋
label_precedes: 優先於
label_follows: 跟隨於
-label_end_to_start: end to start
-label_end_to_end: end to end
-label_start_to_start: start to start
-label_start_to_end: start to end
+label_end_to_start: çµæŸâ”€é–‹å§‹
+label_end_to_end: çµæŸâ”€çµæŸ
+label_start_to_start: 開始─開始
+label_start_to_end: é–‹å§‹â”€çµæŸ
label_stay_logged_in: ç¶­æŒå·²ç™»å…¥ç‹€æ…‹
label_disabled: 關閉
label_show_completed_versions: 顯示已完æˆçš„版本
@@ -496,7 +504,7 @@ label_registration_activation_by_email: é€éŽé›»å­éƒµä»¶å•Ÿç”¨å¸³æˆ¶
label_registration_manual_activation: 手動啟用帳戶
label_registration_automatic_activation: 自動啟用帳戶
label_display_per_page: 'æ¯é é¡¯ç¤º: %s 個'
-label_age: Age
+label_age: 年齡
label_change_properties: 變更屬性
label_general: 一般
label_more: 更多 »
@@ -510,6 +518,8 @@ label_preferences: å好é¸é …
label_chronological_order: 以時間由é è‡³è¿‘排åº
label_reverse_chronological_order: ä»¥æ™‚é–“ç”±è¿‘è‡³é æŽ’åº
label_planning: 計劃表
+label_incoming_emails: 傳入的電å­éƒµä»¶
+label_generate_key: 產生金鑰
button_login: 登入
button_submit: é€å‡º
@@ -527,10 +537,10 @@ button_clear: 清除
button_lock: 鎖定
button_unlock: 解除鎖定
button_download: 下載
-button_list: List
+button_list: 清單
button_view: 檢視
button_move: 移動
-button_back: Back
+button_back: 返回
button_cancel: å–æ¶ˆ
button_activate: 啟用
button_sort: 排åº
@@ -557,6 +567,7 @@ text_select_mail_notifications: 鏿“‡æ¬²å¯„é€æé†’é€šçŸ¥éƒµä»¶ä¹‹å‹•ä½œ
text_regexp_info: eg. ^[A-Z0-9]+$
text_min_max_length_info: 0 代表「ä¸é™åˆ¶ã€
text_project_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹å°ˆæ¡ˆå’Œå…¶ä»–相關資料?
+text_subprojects_destroy_warning: '下列å­å°ˆæ¡ˆï¼š %s 將一併被刪除。'
text_workflow_edit: 鏿“‡è§’色與追蹤標籤以設定其工作æµç¨‹
text_are_you_sure: 確定執行?
text_journal_changed: 從 %s 變更為 %s
@@ -579,8 +590,8 @@ text_wiki_destroy_confirmation: 您確定è¦åˆªé™¤é€™å€‹ wiki 和其中的所有
text_issue_category_destroy_question: 有 (%d) 個項目被指派到此分類. è«‹é¸æ“‡æ‚¨æƒ³è¦çš„動作?
text_issue_category_destroy_assignments: 移除這些項目的分類
text_issue_category_reassign_to: 釿–°æŒ‡æ´¾é€™äº›é …目至其它分類
-text_user_mail_option: "For unselected projects, you will only receive notifications about things you watch or you're involved in (eg. issues you're the author or assignee)."
-text_no_configuration_data: "Roles, trackers, issue statuses and workflow have not been configured yet.\nIt is highly recommended to load the default configuration. You will be able to modify it once loaded."
+text_user_mail_option: "å°æ–¼é‚£äº›æœªè¢«é¸æ“‡çš„å°ˆæ¡ˆï¼Œå°‡åªæœƒæŽ¥æ”¶åˆ°æ‚¨æ­£åœ¨è§€å¯Ÿä¸­ï¼Œæˆ–是åƒèˆ‡ä¸­çš„項目通知。(「åƒèˆ‡ä¸­çš„é …ç›®ã€åŒ…嫿‚¨å»ºç«‹çš„æˆ–是指派給您的項目)"
+text_no_configuration_data: "角色ã€è¿½è¹¤å™¨ã€é …目狀態與æµç¨‹å°šæœªè¢«è¨­å®šå®Œæˆã€‚\n強烈建議您先載入é è¨­çš„è¨­å®šï¼Œç„¶å¾Œä¿®æ”¹æˆæ‚¨æƒ³è¦çš„設定。"
text_load_default_configuration: 載入é è¨­çµ„æ…‹
text_status_changed_by_changeset: 已套用至變更集 %s.
text_issues_destroy_confirmation: 'ç¢ºå®šåˆªé™¤å·²é¸æ“‡çš„項目?'
@@ -592,6 +603,10 @@ text_destroy_time_entries_question: 您å³å°‡åˆªé™¤çš„項目已報工 %.02f å°æ
text_destroy_time_entries: 刪除已報工的時數
text_assign_time_entries_to_project: 指定已報工的時數至專案中
text_reassign_time_entries: '釿–°æŒ‡å®šå·²å ±å·¥çš„æ™‚數至此項目:'
+text_user_wrote: '%s å…ˆå‰æåˆ°:'
+text_enumeration_destroy_question: 'ç›®å‰æœ‰ %d 個物件使用此列舉值。'
+text_enumeration_category_reassign_to: '釿–°è¨­å®šå…¶åˆ—舉值為:'
+text_email_delivery_not_configured: "您尚未設定電å­éƒµä»¶å‚³é€æ–¹å¼ï¼Œå› æ­¤æé†’é¸é …已被åœç”¨ã€‚\n請在 config/email.yml 中設定 SMTP ä¹‹å¾Œï¼Œé‡æ–°å•Ÿå‹• Redmine,以啟用電å­éƒµä»¶æé†’é¸é …。"
default_role_manager: 管ç†äººå“¡
default_role_developper: 開發人員
@@ -618,4 +633,7 @@ default_activity_development: 開發
enumeration_issue_priorities: 項目優先權
enumeration_doc_categories: 文件分類
enumeration_activities: 活動 (時間追蹤)
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+field_parent_title: Parent page
+label_issue_watchers: Watchers
+setting_commit_logs_encoding: Commit messages encoding
+button_quote: Quote
diff --git a/groups/lang/zh.yml b/groups/lang/zh.yml
index 12fb8cb3e..bfe551093 100644
--- a/groups/lang/zh.yml
+++ b/groups/lang/zh.yml
@@ -48,6 +48,7 @@ general_text_no: 'å¦'
general_text_yes: '是'
general_lang_name: 'Simplified Chinese (简体中文)'
general_csv_separator: ','
+general_csv_decimal_separator: '.'
general_csv_encoding: gb2312
general_pdf_encoding: gb2312
general_day_names: 星期一,星期二,星期三,星期四,星期五,星期六,星期日
@@ -91,6 +92,8 @@ mail_body_account_information_external: 您å¯ä»¥ä½¿ç”¨æ‚¨çš„ "%s" å¸å·æ¥ç™»å
mail_body_account_information: 您的å¸å·ä¿¡æ¯
mail_subject_account_activation_request: %så¸å·æ¿€æ´»è¯·æ±‚
mail_body_account_activation_request: '新用户(%sï¼‰å·²å®Œæˆæ³¨å†Œï¼Œæ­£åœ¨ç­‰å€™æ‚¨çš„审核:'
+mail_subject_reminder: "%d 个问题需è¦å°½å¿«è§£å†³"
+mail_body_reminder: "指派给您的 %d 个问题需è¦åœ¨ %d 天内完æˆï¼š"
gui_validation_error: 1 个错误
gui_validation_error_plural: %d 个错误
@@ -179,6 +182,7 @@ field_time_zone: 时区
field_searchable: å¯ç”¨ä½œæœç´¢æ¡ä»¶
field_default_value: 默认值
field_comments_sorting: 显示注释
+field_parent_title: 上级页é¢
setting_app_title: åº”ç”¨ç¨‹åºæ ‡é¢˜
setting_app_subtitle: 应用程åºå­æ ‡é¢˜
@@ -198,19 +202,23 @@ setting_default_projects_public: 新建项目默认为公开项目
setting_autofetch_changesets: 自动获å–程åºå˜æ›´
setting_sys_api_enabled: å¯ç”¨ç”¨äºŽç‰ˆæœ¬åº“管ç†çš„Web Service
setting_commit_ref_keywords: 用于引用问题的关键字
-setting_commit_fix_keywords: 用于修订问题的关键字
+setting_commit_fix_keywords: 用于解决问题的关键字
setting_autologin: 自动登录
setting_date_format: 日期格å¼
setting_time_format: æ—¶é—´æ ¼å¼
setting_cross_project_issue_relations: å…许ä¸åŒé¡¹ç›®ä¹‹é—´çš„问题关è”
setting_issue_list_default_columns: 问题列表中显示的默认列
setting_repositories_encodings: 版本库编ç 
+setting_commit_logs_encoding: æäº¤æ³¨é‡Šçš„ç¼–ç 
setting_emails_footer: 邮件签å
setting_protocol: åè®®
setting_per_page_options: æ¯é¡µæ˜¾ç¤ºæ¡ç›®ä¸ªæ•°çš„设置
setting_user_format: 用户显示格å¼
setting_activity_days_default: 在项目活动中显示的天数
setting_display_subprojects_issues: 在项目页é¢ä¸Šé»˜è®¤æ˜¾ç¤ºå­é¡¹ç›®çš„问题
+setting_enabled_scm: å¯ç”¨ SCM
+setting_mail_handler_api_enabled: å¯ç”¨ç”¨äºŽæŽ¥æ”¶é‚®ä»¶çš„Web Service
+setting_mail_handler_api_key: API key
project_module_issue_tracking: 问题跟踪
project_module_time_tracking: 时间跟踪
@@ -257,9 +265,9 @@ label_issue_status_new: 新建问题状æ€
label_issue_category: 问题类别
label_issue_category_plural: 问题类别
label_issue_category_new: 新建问题类别
-label_custom_field: 自定义字段
-label_custom_field_plural: 自定义字段
-label_custom_field_new: 新建自定义字段
+label_custom_field: 自定义属性
+label_custom_field_plural: 自定义属性
+label_custom_field_new: 新建自定义属性
label_enumerations: 枚举值
label_enumeration_new: 新建枚举值
label_information: ä¿¡æ¯
@@ -291,6 +299,7 @@ label_auth_source: è®¤è¯æ¨¡å¼
label_auth_source_new: æ–°å»ºè®¤è¯æ¨¡å¼
label_auth_source_plural: è®¤è¯æ¨¡å¼
label_subproject_plural: å­é¡¹ç›®
+label_and_its_subprojects: %s åŠå…¶å­é¡¹ç›®
label_min_max_length: æœ€å° - 最大 长度
label_list: 列表
label_date: 日期
@@ -445,6 +454,7 @@ label_relation_new: 新建关è”
label_relation_delete: 删除关è”
label_relates_to: å…³è”到
label_duplicates: é‡å¤
+label_duplicated_by: 与其é‡å¤
label_blocks: 阻挡
label_blocked_by: 被阻挡
label_precedes: 优先于
@@ -510,6 +520,9 @@ label_preferences: 首选项
label_chronological_order: 按时间顺åº
label_reverse_chronological_order: 按时间顺åºï¼ˆå€’åºï¼‰
label_planning: 计划
+label_incoming_emails: 接收邮件
+label_generate_key: 生æˆä¸€ä¸ªkey
+label_issue_watchers: 跟踪者
button_login: 登录
button_submit: æäº¤
@@ -554,9 +567,10 @@ status_registered: 已注册
status_locked: å·²é”定
text_select_mail_notifications: 选择需è¦å‘é€é‚®ä»¶é€šçŸ¥çš„动作
-text_regexp_info: eg. ^[A-Z0-9]+$
+text_regexp_info: 例如:^[A-Z0-9]+$
text_min_max_length_info: 0 表示没有é™åˆ¶
text_project_destroy_confirmation: 您确信è¦åˆ é™¤è¿™ä¸ªé¡¹ç›®ä»¥åŠæ‰€æœ‰ç›¸å…³çš„æ•°æ®å—?
+text_subprojects_destroy_warning: '以下å­é¡¹ç›®ä¹Ÿå°†è¢«åŒæ—¶åˆ é™¤ï¼š%s'
text_workflow_edit: 选择角色和跟踪标签æ¥ç¼–辑工作æµç¨‹
text_are_you_sure: 您确定?
text_journal_changed: 从 %s å˜æ›´ä¸º %s
@@ -572,7 +586,7 @@ text_length_between: 长度必须在 %d 到 %d 个字符之间。
text_tracker_no_workflow: 此跟踪标签未定义工作æµç¨‹
text_unallowed_characters: éžæ³•字符
text_comma_separated: å¯ä»¥ä½¿ç”¨å¤šä¸ªå€¼ï¼ˆç”¨é€—å·,分开)。
-text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸­å¼•用和修订问题
+text_issues_ref_in_commit_messages: 在æäº¤ä¿¡æ¯ä¸­å¼•用和解决问题
text_issue_added: 问题 %s 已由 %s æäº¤ã€‚
text_issue_updated: 问题 %s 已由 %s 更新。
text_wiki_destroy_confirmation: 您确定è¦åˆ é™¤è¿™ä¸ª wiki åŠå…¶æ‰€æœ‰å†…容å—?
@@ -592,6 +606,10 @@ text_destroy_time_entries_question: 您è¦åˆ é™¤çš„问题已ç»ä¸ŠæŠ¥äº† %.02f å
text_destroy_time_entries: 删除上报的工作é‡
text_assign_time_entries_to_project: å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æäº¤åˆ°é¡¹ç›®ä¸­
text_reassign_time_entries: 'å°†å·²ä¸ŠæŠ¥çš„å·¥ä½œé‡æŒ‡å®šåˆ°æ­¤é—®é¢˜ï¼š'
+text_user_wrote: '%s 写到:'
+text_enumeration_category_reassign_to: '将它们关è”到新的枚举值:'
+text_enumeration_destroy_question: '%d 个对象被关è”到了这个枚举值。'
+text_email_delivery_not_configured: "邮件傿•°å°šæœªé…置,因此邮件通知功能已被ç¦ç”¨ã€‚\n请在config/email.yml中é…置您的SMTPæœåŠ¡å™¨ä¿¡æ¯å¹¶é‡æ–°å¯åŠ¨ä»¥ä½¿å…¶ç”Ÿæ•ˆã€‚"
default_role_manager: 管ç†äººå‘˜
default_role_developper: å¼€å‘人员
@@ -618,4 +636,4 @@ default_activity_development: å¼€å‘
enumeration_issue_priorities: 问题优先级
enumeration_doc_categories: 文档类别
enumeration_activities: 活动(时间跟踪)
-text_subprojects_destroy_warning: 'Its subproject(s): %s will be also deleted.'
+button_quote: Quote
diff --git a/groups/lib/SVG/Graph/Graph.rb b/groups/lib/SVG/Graph/Graph.rb
index 403a0202b..a5e1ea732 100644
--- a/groups/lib/SVG/Graph/Graph.rb
+++ b/groups/lib/SVG/Graph/Graph.rb
@@ -829,7 +829,7 @@ module SVG
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
if style_sheet && style_sheet != ''
- @doc << ProcessingInstruction.new( "xml-stylesheet",
+ @doc << Instruction.new( "xml-stylesheet",
%Q{href="#{style_sheet}" type="text/css"} )
end
@root = @doc.add_element( "svg", {
diff --git a/groups/lib/redcloth.rb b/groups/lib/redcloth.rb
index 7e0c71839..df19de22d 100644
--- a/groups/lib/redcloth.rb
+++ b/groups/lib/redcloth.rb
@@ -299,6 +299,8 @@ class RedCloth < String
hard_break text
unless @lite_mode
refs text
+ # need to do this before text is split by #blocks
+ block_textile_quotes text
blocks text
end
inline text
@@ -376,13 +378,13 @@ class RedCloth < String
re =
case rtype
when :limit
- /(^|[>\s])
+ /(^|[>\s\(])
(#{rcq})
(#{C})
(?::(\S+?))?
([^\s\-].*?[^\s\-]|\w)
#{rcq}
- (?=[[:punct:]]|\s|$)/x
+ (?=[[:punct:]]|\s|\)|$)/x
else
/(#{rcq})
(#{C})
@@ -502,26 +504,19 @@ class RedCloth < String
tatts = shelve( tatts ) if tatts
rows = []
- fullrow.
- split( /\|$/m ).
- delete_if { |x| x.empty? }.
- each do |row|
-
+ fullrow.each_line do |row|
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
-
cells = []
- #row.split( /\(?!\[\[[^\]])|(?![^\[]\]\])/ ).each do |cell|
- row.split( /\|(?![^\[\|]*\]\])/ ).each do |cell|
+ row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
+ next if cell == '|'
ctyp = 'd'
ctyp = 'h' if cell =~ /^_/
catts = ''
catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
- unless cell.strip.empty?
- catts = shelve( catts ) if catts
- cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
- end
+ catts = shelve( catts ) if catts
+ cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
end
ratts = shelve( ratts ) if ratts
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
@@ -576,6 +571,29 @@ class RedCloth < String
lines.join( "\n" )
end
end
+
+ QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
+ QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
+
+ def block_textile_quotes( text )
+ text.gsub!( QUOTES_RE ) do |match|
+ lines = match.split( /\n/ )
+ quotes = ''
+ indent = 0
+ lines.each do |line|
+ line =~ QUOTES_CONTENT_RE
+ bq,content = $1, $2
+ l = bq.count('>')
+ if l != indent
+ quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
+ indent = l
+ end
+ quotes << (content + "\n")
+ end
+ quotes << ("\n" + '</blockquote>' * indent + "\n\n")
+ quotes
+ end
+ end
CODE_RE = /(\W)
@
@@ -726,7 +744,7 @@ class RedCloth < String
end
MARKDOWN_RULE_RE = /^(#{
- ['*', '-', '_'].collect { |ch| '( ?' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
+ ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
})$/
def block_markdown_rule( text )
@@ -764,11 +782,11 @@ class RedCloth < String
([\s\[{(]|[#{PUNCT}])? # $pre
" # start
(#{C}) # $atts
- ([^"]+?) # $text
+ ([^"\n]+?) # $text
\s?
(?:\(([^)]+?)\)(?="))? # $title
":
- (\S+?) # $url
+ ([\w\/]\S+?) # $url
(\/)? # $slash
([^\w\/;]*?) # $post
(?=<|\s|$)
@@ -1131,10 +1149,10 @@ class RedCloth < String
end
end
- ALLOWED_TAGS = %w(redpre pre code)
+ ALLOWED_TAGS = %w(redpre pre code notextile)
def escape_html_tags(text)
- text.gsub!(%r{<(\/?(\w+)[^>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' if $3}" }
+ text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
end
end
diff --git a/groups/lib/redmine.rb b/groups/lib/redmine.rb
index 2697e8f5f..33d33752b 100644
--- a/groups/lib/redmine.rb
+++ b/groups/lib/redmine.rb
@@ -1,5 +1,6 @@
require 'redmine/access_control'
require 'redmine/menu_manager'
+require 'redmine/activity'
require 'redmine/mime_type'
require 'redmine/core_ext'
require 'redmine/themes'
@@ -11,7 +12,7 @@ rescue LoadError
# RMagick is not available
end
-REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
+REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
# Permissions
Redmine::AccessControl.map do |map|
@@ -32,9 +33,9 @@ Redmine::AccessControl.map do |map|
:queries => :index,
:reports => :issue_report}, :public => true
map.permission :add_issues, {:issues => :new}
- map.permission :edit_issues, {:issues => [:edit, :bulk_edit, :destroy_attachment]}
+ map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
- map.permission :add_issue_notes, {:issues => :edit}
+ map.permission :add_issue_notes, {:issues => [:edit, :reply]}
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
map.permission :move_issues, {:issues => :move}, :require => :loggedin
@@ -45,6 +46,9 @@ Redmine::AccessControl.map do |map|
# Gantt & calendar
map.permission :view_gantt, :projects => :gantt
map.permission :view_calendar, :projects => :calendar
+ # Watchers
+ map.permission :view_issue_watchers, {}
+ map.permission :add_issue_watchers, {:watchers => :new}
end
map.project_module :time_tracking do |map|
@@ -76,6 +80,7 @@ Redmine::AccessControl.map do |map|
map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
map.permission :view_wiki_pages, :wiki => [:index, :history, :diff, :annotate, :special]
map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment, :destroy_attachment]
+ map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
end
map.project_module :repository do |map|
@@ -87,25 +92,25 @@ Redmine::AccessControl.map do |map|
map.project_module :boards do |map|
map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
- map.permission :add_messages, {:messages => [:new, :reply]}
+ map.permission :add_messages, {:messages => [:new, :reply, :quote]}
map.permission :edit_messages, {:messages => :edit}, :require => :member
map.permission :delete_messages, {:messages => :destroy}, :require => :member
end
end
Redmine::MenuManager.map :top_menu do |menu|
- menu.push :home, :home_url, :html => { :class => 'home' }
+ menu.push :home, :home_path, :html => { :class => 'home' }
menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
- menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }
- menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }
+ menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true
+ menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true
end
Redmine::MenuManager.map :account_menu do |menu|
- menu.push :login, :signin_url, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
+ menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
- menu.push :logout, :signout_url, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
+ menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
end
Redmine::MenuManager.map :application_menu do |menu|
@@ -129,5 +134,15 @@ Redmine::MenuManager.map :project_menu do |menu|
menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
menu.push :repository, { :controller => 'repositories', :action => 'show' },
:if => Proc.new { |p| p.repository && !p.repository.new_record? }
- menu.push :settings, { :controller => 'projects', :action => 'settings' }
+ menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
+end
+
+Redmine::Activity.map do |activity|
+ activity.register :issues, :class_name => %w(Issue Journal)
+ activity.register :changesets
+ activity.register :news
+ activity.register :documents, :class_name => %w(Document Attachment)
+ activity.register :files, :class_name => 'Attachment'
+ activity.register :wiki_pages, :class_name => 'WikiContent::Version', :default => false
+ activity.register :messages, :default => false
end
diff --git a/groups/lib/redmine/activity.rb b/groups/lib/redmine/activity.rb
new file mode 100644
index 000000000..565a53f36
--- /dev/null
+++ b/groups/lib/redmine/activity.rb
@@ -0,0 +1,46 @@
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Activity
+
+ mattr_accessor :available_event_types, :default_event_types, :providers
+
+ @@available_event_types = []
+ @@default_event_types = []
+ @@providers = Hash.new {|h,k| h[k]=[] }
+
+ class << self
+ def map(&block)
+ yield self
+ end
+
+ # Registers an activity provider
+ def register(event_type, options={})
+ options.assert_valid_keys(:class_name, :default)
+
+ event_type = event_type.to_s
+ providers = options[:class_name] || event_type.classify
+ providers = ([] << providers) unless providers.is_a?(Array)
+
+ @@available_event_types << event_type unless @@available_event_types.include?(event_type)
+ @@default_event_types << event_type unless options[:default] == false
+ @@providers[event_type] += providers
+ end
+ end
+ end
+end
diff --git a/groups/lib/redmine/activity/fetcher.rb b/groups/lib/redmine/activity/fetcher.rb
new file mode 100644
index 000000000..adaead564
--- /dev/null
+++ b/groups/lib/redmine/activity/fetcher.rb
@@ -0,0 +1,79 @@
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Activity
+ # Class used to retrieve activity events
+ class Fetcher
+ attr_reader :user, :project, :scope
+
+ # Needs to be unloaded in development mode
+ @@constantized_providers = Hash.new {|h,k| h[k] = Redmine::Activity.providers[k].collect {|t| t.constantize } }
+
+ def initialize(user, options={})
+ options.assert_valid_keys(:project, :with_subprojects)
+ @user = user
+ @project = options[:project]
+ @options = options
+
+ @scope = event_types
+ end
+
+ # Returns an array of available event types
+ def event_types
+ return @event_types unless @event_types.nil?
+
+ @event_types = Redmine::Activity.available_event_types
+ @event_types = @event_types.select {|o| @user.allowed_to?("view_#{o}".to_sym, @project)} if @project
+ @event_types
+ end
+
+ # Yields to filter the activity scope
+ def scope_select(&block)
+ @scope = @scope.select {|t| yield t }
+ end
+
+ # Sets the scope
+ def scope=(s)
+ @scope = s & event_types
+ end
+
+ # Resets the scope to the default scope
+ def default_scope!
+ @scope = Redmine::Activity.default_event_types
+ end
+
+ # Returns an array of events for the given date range
+ def events(from, to)
+ e = []
+
+ @scope.each do |event_type|
+ constantized_providers(event_type).each do |provider|
+ e += provider.find_events(event_type, @user, from, to, @options)
+ end
+ end
+ e
+ end
+
+ private
+
+ def constantized_providers(event_type)
+ @@constantized_providers[event_type]
+ end
+ end
+ end
+end
diff --git a/groups/lib/redmine/core_ext/string/conversions.rb b/groups/lib/redmine/core_ext/string/conversions.rb
index 7444445b0..41149f5ea 100644
--- a/groups/lib/redmine/core_ext/string/conversions.rb
+++ b/groups/lib/redmine/core_ext/string/conversions.rb
@@ -32,7 +32,7 @@ module Redmine #:nodoc:
end
# 2,5 => 2.5
s.gsub!(',', '.')
- s.to_f
+ begin; Kernel.Float(s); rescue; nil; end
end
end
end
diff --git a/groups/lib/redmine/imap.rb b/groups/lib/redmine/imap.rb
new file mode 100644
index 000000000..a6cd958cd
--- /dev/null
+++ b/groups/lib/redmine/imap.rb
@@ -0,0 +1,51 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'net/imap'
+
+module Redmine
+ module IMAP
+ class << self
+ def check(imap_options={}, options={})
+ host = imap_options[:host] || '127.0.0.1'
+ port = imap_options[:port] || '143'
+ ssl = !imap_options[:ssl].nil?
+ folder = imap_options[:folder] || 'INBOX'
+
+ imap = Net::IMAP.new(host, port, ssl)
+ imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil?
+ imap.select(folder)
+ imap.search(['NOT', 'SEEN']).each do |message_id|
+ msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
+ logger.debug "Receiving message #{message_id}" if logger && logger.debug?
+ if MailHandler.receive(msg, options)
+ imap.store(message_id, "+FLAGS", [:Seen, :Deleted])
+ else
+ imap.store(message_id, "+FLAGS", [:Seen])
+ end
+ end
+ imap.expunge
+ end
+
+ private
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+ end
+ end
+end
diff --git a/groups/lib/redmine/menu_manager.rb b/groups/lib/redmine/menu_manager.rb
index af54b41fe..f6431928e 100644
--- a/groups/lib/redmine/menu_manager.rb
+++ b/groups/lib/redmine/menu_manager.rb
@@ -80,9 +80,10 @@ module Redmine
else
item.url
end
- #url = (project && item.url.is_a?(Hash)) ? {item.param => project}.merge(item.url) : (item.url.is_a?(Symbol) ? send(item.url) : item.url)
+ caption = item.caption(project)
+ caption = l(caption) if caption.is_a?(Symbol)
links << content_tag('li',
- link_to(l(item.caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options)))
+ link_to(h(caption), url, (current_menu_item == item.name ? item.html_options.merge(:class => 'selected') : item.html_options)))
end
end
links.empty? ? nil : content_tag('ul', links.join("\n"))
@@ -91,11 +92,9 @@ module Redmine
class << self
def map(menu_name)
- mapper = Mapper.new
- yield mapper
@items ||= {}
- @items[menu_name.to_sym] ||= []
- @items[menu_name.to_sym] += mapper.items
+ mapper = Mapper.new(menu_name.to_sym, @items)
+ yield mapper
end
def items(menu_name)
@@ -108,17 +107,46 @@ module Redmine
end
class Mapper
+ def initialize(menu, items)
+ items[menu] ||= []
+ @menu = menu
+ @menu_items = items[menu]
+ end
+
+ @@last_items_count = Hash.new {|h,k| h[k] = 0}
+
# Adds an item at the end of the menu. Available options:
# * param: the parameter name that is used for the project id (default is :id)
- # * if: a proc that is called before rendering the item, the item is displayed only if it returns true
- # * caption: the localized string key that is used as the item label
+ # * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
+ # * caption that can be:
+ # * a localized string Symbol
+ # * a String
+ # * a Proc that can take the project as argument
+ # * before, after: specify where the menu item should be inserted (eg. :after => :activity)
+ # * last: menu item will stay at the end (eg. :last => true)
# * html_options: a hash of html options that are passed to link_to
def push(name, url, options={})
- items << MenuItem.new(name, url, options)
+ options = options.dup
+
+ # menu item position
+ if before = options.delete(:before)
+ position = @menu_items.collect(&:name).index(before)
+ elsif after = options.delete(:after)
+ position = @menu_items.collect(&:name).index(after)
+ position += 1 unless position.nil?
+ elsif options.delete(:last)
+ position = @menu_items.size
+ @@last_items_count[@menu] += 1
+ end
+ # default position
+ position ||= @menu_items.size - @@last_items_count[@menu]
+
+ @menu_items.insert(position, MenuItem.new(name, url, options))
end
- def items
- @items ||= []
+ # Removes a menu item
+ def delete(name)
+ @menu_items.delete_if {|i| i.name == name}
end
end
@@ -133,13 +161,19 @@ module Redmine
@url = url
@condition = options[:if]
@param = options[:param] || :id
- @caption_key = options[:caption]
+ @caption = options[:caption]
@html_options = options[:html] || {}
end
- def caption
- # check if localized string exists on first render (after GLoc strings are loaded)
- @caption ||= (@caption_key || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
+ def caption(project=nil)
+ if @caption.is_a?(Proc)
+ c = @caption.call(project).to_s
+ c = @name.to_s.humanize if c.blank?
+ c
+ else
+ # check if localized string exists on first render (after GLoc strings are loaded)
+ @caption_key ||= (@caption || (l_has_string?("label_#{@name}".to_sym) ? "label_#{@name}".to_sym : @name.to_s.humanize))
+ end
end
end
end
diff --git a/groups/lib/redmine/platform.rb b/groups/lib/redmine/platform.rb
new file mode 100644
index 000000000..f41b92f2e
--- /dev/null
+++ b/groups/lib/redmine/platform.rb
@@ -0,0 +1,26 @@
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ module Platform
+ class << self
+ def mswin?
+ (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i)
+ end
+ end
+ end
+end
diff --git a/groups/lib/redmine/plugin.rb b/groups/lib/redmine/plugin.rb
index 36632c13e..cf6c194a2 100644
--- a/groups/lib/redmine/plugin.rb
+++ b/groups/lib/redmine/plugin.rb
@@ -116,6 +116,32 @@ module Redmine #:nodoc:
self.instance_eval(&block)
@project_module = nil
end
+
+ # Registers an activity provider.
+ #
+ # Options:
+ # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
+ # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
+ #
+ # A model can provide several activity event types.
+ #
+ # Examples:
+ # register :news
+ # register :scrums, :class_name => 'Meeting'
+ # register :issues, :class_name => ['Issue', 'Journal']
+ #
+ # Retrieving events:
+ # Associated model(s) must implement the find_events class method.
+ # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
+ #
+ # The following call should return all the scrum events visible by current user that occured in the 5 last days:
+ # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
+ # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
+ #
+ # Note that :view_scrums permission is required to view these events in the activity view.
+ def activity_provider(*args)
+ Redmine::Activity.register(*args)
+ end
# Returns +true+ if the plugin can be configured.
def configurable?
diff --git a/groups/lib/redmine/scm/adapters/abstract_adapter.rb b/groups/lib/redmine/scm/adapters/abstract_adapter.rb
index 2c254d48d..9f400880d 100644
--- a/groups/lib/redmine/scm/adapters/abstract_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/abstract_adapter.rb
@@ -24,6 +24,29 @@ module Redmine
end
class AbstractAdapter #:nodoc:
+ class << self
+ # Returns the version of the scm client
+ # Eg: [1, 5, 0] or [] if unknown
+ def client_version
+ []
+ end
+
+ # Returns the version string of the scm client
+ # Eg: '1.5.0' or 'Unknown version' if unknown
+ def client_version_string
+ v = client_version || 'Unknown version'
+ v.is_a?(Array) ? v.join('.') : v.to_s
+ end
+
+ # Returns true if the current client version is above
+ # or equals the given one
+ # If option is :unknown is set to true, it will return
+ # true if the client version is unknown
+ def client_version_above?(v, options={})
+ ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
+ end
+ end
+
def initialize(url, root_url=nil, login=nil, password=nil)
@url = url
@login = login if login && !login.empty?
@@ -77,12 +100,16 @@ module Redmine
def entries(path=nil, identifier=nil)
return nil
end
+
+ def properties(path, identifier=nil)
+ return nil
+ end
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
return nil
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
return nil
end
@@ -94,15 +121,30 @@ module Redmine
path ||= ''
(path[0,1]!="/") ? "/#{path}" : path
end
+
+ def with_trailling_slash(path)
+ path ||= ''
+ (path[-1,1] == "/") ? path : "#{path}/"
+ end
+
+ def without_leading_slash(path)
+ path ||= ''
+ path.gsub(%r{^/+}, '')
+ end
+
+ def without_trailling_slash(path)
+ path ||= ''
+ (path[-1,1] == "/") ? path[0..-2] : path
+ end
def shell_quote(str)
- if RUBY_PLATFORM =~ /mswin/
+ if Redmine::Platform.mswin?
'"' + str.gsub(/"/, '\\"') + '"'
else
"'" + str.gsub(/'/, "'\"'\"'") + "'"
end
end
-
+
private
def retrieve_root_url
info = self.info
@@ -116,10 +158,18 @@ module Redmine
end
def logger
- RAILS_DEFAULT_LOGGER
+ self.class.logger
end
-
+
def shellout(cmd, &block)
+ self.class.shellout(cmd, &block)
+ end
+
+ def self.logger
+ RAILS_DEFAULT_LOGGER
+ end
+
+ def self.shellout(cmd, &block)
logger.debug "Shelling out: #{cmd}" if logger && logger.debug?
begin
IO.popen(cmd, "r+") do |io|
@@ -127,11 +177,22 @@ module Redmine
block.call(io) if block_given?
end
rescue Errno::ENOENT => e
+ msg = strip_credential(e.message)
# The command failed, log it and re-raise
- logger.error("SCM command failed: #{cmd}\n with: #{e.message}")
- raise CommandFailed.new(e.message)
+ logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
+ raise CommandFailed.new(msg)
end
end
+
+ # Hides username/password in a given command
+ def self.strip_credential(cmd)
+ q = (Redmine::Platform.mswin? ? '"' : "'")
+ cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
+ end
+
+ def strip_credential(cmd)
+ self.class.strip_credential(cmd)
+ end
end
class Entries < Array
@@ -208,167 +269,7 @@ module Redmine
end
end
-
- # A line of Diff
- class Diff
- attr_accessor :nb_line_left
- attr_accessor :line_left
- attr_accessor :nb_line_right
- attr_accessor :line_right
- attr_accessor :type_diff_right
- attr_accessor :type_diff_left
- def initialize ()
- self.nb_line_left = ''
- self.nb_line_right = ''
- self.line_left = ''
- self.line_right = ''
- self.type_diff_right = ''
- self.type_diff_left = ''
- end
-
- def inspect
- puts '### Start Line Diff ###'
- puts self.nb_line_left
- puts self.line_left
- puts self.nb_line_right
- puts self.line_right
- end
- end
-
- class DiffTableList < Array
- def initialize (diff, type="inline")
- diff_table = DiffTable.new type
- diff.each do |line|
- if line =~ /^(---|\+\+\+) (.*)$/
- self << diff_table if diff_table.length > 1
- diff_table = DiffTable.new type
- end
- a = diff_table.add_line line
- end
- self << diff_table unless diff_table.empty?
- self
- end
- end
-
- # Class for create a Diff
- class DiffTable < Hash
- attr_reader :file_name, :line_num_l, :line_num_r
-
- # Initialize with a Diff file and the type of Diff View
- # The type view must be inline or sbs (side_by_side)
- def initialize(type="inline")
- @parsing = false
- @nb_line = 1
- @start = false
- @before = 'same'
- @second = true
- @type = type
- end
-
- # Function for add a line of this Diff
- def add_line(line)
- unless @parsing
- if line =~ /^(---|\+\+\+) (.*)$/
- @file_name = $2
- return false
- elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
- @line_num_l = $5.to_i
- @line_num_r = $2.to_i
- @parsing = true
- end
- else
- if line =~ /^[^\+\-\s@\\]/
- self.delete(self.keys.sort.last)
- @parsing = false
- return false
- elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
- @line_num_l = $5.to_i
- @line_num_r = $2.to_i
- else
- @nb_line += 1 if parse_line(line, @type)
- end
- end
- return true
- end
-
- def inspect
- puts '### DIFF TABLE ###'
- puts "file : #{file_name}"
- self.each do |d|
- d.inspect
- end
- end
-
- private
- # Test if is a Side By Side type
- def sbs?(type, func)
- if @start and type == "sbs"
- if @before == func and @second
- tmp_nb_line = @nb_line
- self[tmp_nb_line] = Diff.new
- else
- @second = false
- tmp_nb_line = @start
- @start += 1
- @nb_line -= 1
- end
- else
- tmp_nb_line = @nb_line
- @start = @nb_line
- self[tmp_nb_line] = Diff.new
- @second = true
- end
- unless self[tmp_nb_line]
- @nb_line += 1
- self[tmp_nb_line] = Diff.new
- else
- self[tmp_nb_line]
- end
- end
-
- # Escape the HTML for the diff
- def escapeHTML(line)
- CGI.escapeHTML(line)
- end
-
- def parse_line(line, type="inline")
- if line[0, 1] == "+"
- diff = sbs? type, 'add'
- @before = 'add'
- diff.line_left = escapeHTML line[1..-1]
- diff.nb_line_left = @line_num_l
- diff.type_diff_left = 'diff_in'
- @line_num_l += 1
- true
- elsif line[0, 1] == "-"
- diff = sbs? type, 'remove'
- @before = 'remove'
- diff.line_right = escapeHTML line[1..-1]
- diff.nb_line_right = @line_num_r
- diff.type_diff_right = 'diff_out'
- @line_num_r += 1
- true
- elsif line[0, 1] =~ /\s/
- @before = 'same'
- @start = false
- diff = Diff.new
- diff.line_right = escapeHTML line[1..-1]
- diff.nb_line_right = @line_num_r
- diff.line_left = escapeHTML line[1..-1]
- diff.nb_line_left = @line_num_l
- self[@nb_line] = diff
- @line_num_l += 1
- @line_num_r += 1
- true
- elsif line[0, 1] = "\\"
- true
- else
- false
- end
- end
- end
-
class Annotate
attr_reader :lines, :revisions
diff --git a/groups/lib/redmine/scm/adapters/bazaar_adapter.rb b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb
index 2225a627c..ff69e3e6b 100644
--- a/groups/lib/redmine/scm/adapters/bazaar_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/bazaar_adapter.rb
@@ -132,7 +132,7 @@ module Redmine
revisions
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
path ||= ''
if identifier_to
identifier_to = identifier_to.to_i
@@ -147,7 +147,7 @@ module Redmine
end
end
#return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def cat(path, identifier=nil)
diff --git a/groups/lib/redmine/scm/adapters/cvs_adapter.rb b/groups/lib/redmine/scm/adapters/cvs_adapter.rb
index 37920b599..089a6b153 100644
--- a/groups/lib/redmine/scm/adapters/cvs_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/cvs_adapter.rb
@@ -65,7 +65,7 @@ module Redmine
entries = Entries.new
cmd = "#{CVS_BIN} -d #{root_url} rls -ed"
cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
- cmd << " #{path_with_project}"
+ cmd << " #{shell_quote path_with_project}"
shellout(cmd) do |io|
io.each_line(){|line|
fields=line.chop.split('/',-1)
@@ -110,7 +110,7 @@ module Redmine
path_with_project="#{url}#{with_leading_slash(path)}"
cmd = "#{CVS_BIN} -d #{root_url} rlog"
cmd << " -d\">#{time_to_cvstime(identifier_from)}\"" if identifier_from
- cmd << " #{path_with_project}"
+ cmd << " #{shell_quote path_with_project}"
shellout(cmd) do |io|
state="entry_start"
@@ -227,10 +227,10 @@ module Redmine
end
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
path_with_project="#{url}#{with_leading_slash(path)}"
- cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{path_with_project}"
+ cmd = "#{CVS_BIN} -d #{root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
diff = []
shellout(cmd) do |io|
io.each_line do |line|
@@ -238,14 +238,16 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def cat(path, identifier=nil)
identifier = (identifier) ? identifier : "HEAD"
logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
path_with_project="#{url}#{with_leading_slash(path)}"
- cmd = "#{CVS_BIN} -d #{root_url} co -r#{identifier} -p #{path_with_project}"
+ cmd = "#{CVS_BIN} -d #{root_url} co"
+ cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
+ cmd << " -p #{shell_quote path_with_project}"
cat = nil
shellout(cmd) do |io|
cat = io.read
@@ -258,7 +260,7 @@ module Redmine
identifier = (identifier) ? identifier : "HEAD"
logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
path_with_project="#{url}#{with_leading_slash(path)}"
- cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}"
+ cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
blame = Annotate.new
shellout(cmd) do |io|
io.each_line do |line|
diff --git a/groups/lib/redmine/scm/adapters/darcs_adapter.rb b/groups/lib/redmine/scm/adapters/darcs_adapter.rb
index a1d1867b1..4a5183f79 100644
--- a/groups/lib/redmine/scm/adapters/darcs_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/darcs_adapter.rb
@@ -25,16 +25,36 @@ module Redmine
# Darcs executable name
DARCS_BIN = "darcs"
+ class << self
+ def client_version
+ @@client_version ||= (darcs_binary_version || [])
+ end
+
+ def darcs_binary_version
+ cmd = "#{DARCS_BIN} --version"
+ version = nil
+ shellout(cmd) do |io|
+ # Read darcs version in first returned line
+ if m = io.gets.match(%r{((\d+\.)+\d+)})
+ version = m[0].scan(%r{\d+}).collect(&:to_i)
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ version
+ end
+ end
+
def initialize(url, root_url=nil, login=nil, password=nil)
@url = url
@root_url = url
end
def supports_cat?
- false
+ # cat supported in darcs 2.0.0 and higher
+ self.class.client_version_above?([2, 0, 0])
end
-
- # Get info about the svn repository
+
+ # Get info about the darcs repository
def info
rev = revisions(nil,nil,nil,{:limit => 1})
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
@@ -94,7 +114,7 @@ module Redmine
revisions
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
path = '*' if path.blank?
cmd = "#{DARCS_BIN} diff --repodir #{@url}"
if identifier_to.nil?
@@ -111,9 +131,22 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
+ def cat(path, identifier=nil)
+ cmd = "#{DARCS_BIN} show content --repodir #{@url}"
+ cmd << " --match \"hash #{identifier}\"" if identifier
+ cmd << " #{shell_quote path}"
+ cat = nil
+ shellout(cmd) do |io|
+ io.binmode
+ cat = io.read
+ end
+ return nil if $? && $?.exitstatus != 0
+ cat
+ end
+
private
def entry_from_xml(element, path_prefix)
diff --git a/groups/lib/redmine/scm/adapters/filesystem_adapter.rb b/groups/lib/redmine/scm/adapters/filesystem_adapter.rb
new file mode 100644
index 000000000..99296a090
--- /dev/null
+++ b/groups/lib/redmine/scm/adapters/filesystem_adapter.rb
@@ -0,0 +1,93 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# FileSystem adapter
+# File written by Paul Rivier, at Demotera.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+require 'find'
+
+module Redmine
+ module Scm
+ module Adapters
+ class FilesystemAdapter < AbstractAdapter
+
+
+ def initialize(url, root_url=nil, login=nil, password=nil)
+ @url = with_trailling_slash(url)
+ end
+
+ def format_path_ends(path, leading=true, trailling=true)
+ path = leading ? with_leading_slash(path) :
+ without_leading_slash(path)
+ trailling ? with_trailling_slash(path) :
+ without_trailling_slash(path)
+ end
+
+ def info
+ info = Info.new({:root_url => target(),
+ :lastrev => nil
+ })
+ info
+ rescue CommandFailed
+ return nil
+ end
+
+ def entries(path="", identifier=nil)
+ entries = Entries.new
+ Dir.new(target(path)).each do |e|
+ relative_path = format_path_ends((format_path_ends(path,
+ false,
+ true) + e),
+ false,false)
+ target = target(relative_path)
+ entries <<
+ Entry.new({ :name => File.basename(e),
+ # below : list unreadable files, but dont link them.
+ :path => File.readable?(target) ? relative_path : "",
+ :kind => (File.directory?(target) ? 'dir' : 'file'),
+ :size => (File.directory?(target) ? nil : [File.size(target)].pack('l').unpack('L').first),
+ :lastrev =>
+ Revision.new({:time => (File.mtime(target)).localtime,
+ })
+ }) if File.exist?(target) and # paranoid test
+ %w{file directory}.include?(File.ftype(target)) and # avoid special types
+ not File.basename(e).match(/^\.+$/) # avoid . and ..
+ end
+ entries.sort_by_name
+ end
+
+ def cat(path, identifier=nil)
+ File.new(target(path), "rb").read
+ end
+
+ private
+
+ # AbstractAdapter::target is implicitly made to quote paths.
+ # Here we do not shell-out, so we do not want quotes.
+ def target(path=nil)
+ #Prevent the use of ..
+ if path and !path.match(/(^|\/)\.\.(\/|$)/)
+ return "#{self.url}#{without_leading_slash(path)}"
+ end
+ return self.url
+ end
+
+ end
+ end
+ end
+end
diff --git a/groups/lib/redmine/scm/adapters/git_adapter.rb b/groups/lib/redmine/scm/adapters/git_adapter.rb
index 77604f283..30d624001 100644
--- a/groups/lib/redmine/scm/adapters/git_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/git_adapter.rb
@@ -27,9 +27,13 @@ module Redmine
# Get the revision of a particuliar file
def get_rev (rev,path)
- cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
- cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if
- rev=='latest' or rev.nil?
+
+ if rev != 'latest' && !rev.nil?
+ cmd="#{GIT_BIN} --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}"
+ else
+ branch = shellout("#{GIT_BIN} --git-dir #{target('')} branch") { |io| io.grep(/\*/)[0].strip.match(/\* (.*)/)[1] }
+ cmd="#{GIT_BIN} --git-dir #{target('')} log -1 #{branch} -- #{shell_quote path}"
+ end
rev=[]
i=0
shellout(cmd) do |io|
@@ -135,10 +139,10 @@ module Redmine
def revisions(path, identifier_from, identifier_to, options={})
revisions = Revisions.new
cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
+ cmd << " --reverse" if options[:reverse]
cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
cmd << " #{shell_quote identifier_to} " if identifier_to
- #cmd << " HEAD " if !identifier_to
shellout(cmd) do |io|
files=[]
changeset = {}
@@ -151,13 +155,18 @@ module Redmine
value = $1
if (parsing_descr == 1 || parsing_descr == 2)
parsing_descr = 0
- revisions << Revision.new({:identifier => changeset[:commit],
- :scmid => changeset[:commit],
- :author => changeset[:author],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => files
- })
+ revision = Revision.new({:identifier => changeset[:commit],
+ :scmid => changeset[:commit],
+ :author => changeset[:author],
+ :time => Time.parse(changeset[:date]),
+ :message => changeset[:description],
+ :paths => files
+ })
+ if block_given?
+ yield revision
+ else
+ revisions << revision
+ end
changeset = {}
files = []
revno = revno + 1
@@ -186,21 +195,27 @@ module Redmine
end
end
- revisions << Revision.new({:identifier => changeset[:commit],
+ if changeset[:commit]
+ revision = Revision.new({:identifier => changeset[:commit],
:scmid => changeset[:commit],
:author => changeset[:author],
:time => Time.parse(changeset[:date]),
:message => changeset[:description],
:paths => files
- }) if changeset[:commit]
-
+ })
+ if block_given?
+ yield revision
+ else
+ revisions << revision
+ end
+ end
end
return nil if $? && $?.exitstatus != 0
revisions
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
path ||= ''
if !identifier_to
identifier_to = nil
@@ -216,7 +231,7 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def annotate(path, identifier=nil)
diff --git a/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl b/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
new file mode 100644
index 000000000..b3029e6ff
--- /dev/null
+++ b/groups/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
@@ -0,0 +1,12 @@
+changeset = 'This template must be used with --debug option\n'
+changeset_quiet = 'This template must be used with --debug option\n'
+changeset_verbose = 'This template must be used with --debug option\n'
+changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file = '<path action="M">{file|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>" \ No newline at end of file
diff --git a/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl b/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
new file mode 100644
index 000000000..3eef85016
--- /dev/null
+++ b/groups/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
@@ -0,0 +1,12 @@
+changeset = 'This template must be used with --debug option\n'
+changeset_quiet = 'This template must be used with --debug option\n'
+changeset_verbose = 'This template must be used with --debug option\n'
+changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
+
+file_mod = '<path action="M">{file_mod|escape}</path>\n'
+file_add = '<path action="A">{file_add|escape}</path>\n'
+file_del = '<path action="D">{file_del|escape}</path>\n'
+file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n'
+tag = '<tag>{tag|escape}</tag>\n'
+header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
+# footer="</log>"
diff --git a/groups/lib/redmine/scm/adapters/mercurial_adapter.rb b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb
index 6f42dda06..4eed776d8 100644
--- a/groups/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -21,9 +21,45 @@ module Redmine
module Scm
module Adapters
class MercurialAdapter < AbstractAdapter
-
+
# Mercurial executable name
HG_BIN = "hg"
+ TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
+ TEMPLATE_NAME = "hg-template"
+ TEMPLATE_EXTENSION = "tmpl"
+
+ class << self
+ def client_version
+ @@client_version ||= (hgversion || [])
+ end
+
+ def hgversion
+ # The hg version is expressed either as a
+ # release number (eg 0.9.5 or 1.0) or as a revision
+ # id composed of 12 hexa characters.
+ theversion = hgversion_from_command_line
+ if theversion.match(/^\d+(\.\d+)+/)
+ theversion.split(".").collect(&:to_i)
+ end
+ end
+
+ def hgversion_from_command_line
+ %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
+ end
+
+ def template_path
+ @@template_path ||= template_path_for(client_version)
+ end
+
+ def template_path_for(version)
+ if ((version <=> [0,9,5]) > 0) || version.empty?
+ ver = "1.0"
+ else
+ ver = "0.9.5"
+ end
+ "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
+ end
+ end
def info
cmd = "#{HG_BIN} -R #{target('')} root"
@@ -33,8 +69,8 @@ module Redmine
end
return nil if $? && $?.exitstatus != 0
info = Info.new({:root_url => root_url.chomp,
- :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
- })
+ :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
+ })
info
rescue CommandFailed
return nil
@@ -43,68 +79,78 @@ module Redmine
def entries(path=nil, identifier=nil)
path ||= ''
entries = Entries.new
- cmd = "#{HG_BIN} -R #{target('')} --cwd #{target(path)} locate"
- cmd << " -r #{identifier.to_i}" if identifier
- cmd << " " + shell_quote('glob:**')
+ cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
+ cmd << " " + shell_quote("path:#{path}") unless path.empty?
shellout(cmd) do |io|
io.each_line do |line|
- e = line.chomp.split(%r{[\/\\]})
- entries << Entry.new({:name => e.first,
- :path => (path.empty? ? e.first : "#{path}/#{e.first}"),
- :kind => (e.size > 1 ? 'dir' : 'file'),
- :lastrev => Revision.new
- }) unless entries.detect{|entry| entry.name == e.first}
+ # HG uses antislashs as separator on Windows
+ line = line.gsub(/\\/, "/")
+ if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
+ e ||= line
+ e = e.chomp.split(%r{[\/\\]})
+ entries << Entry.new({:name => e.first,
+ :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
+ :kind => (e.size > 1 ? 'dir' : 'file'),
+ :lastrev => Revision.new
+ }) unless entries.detect{|entry| entry.name == e.first}
+ end
end
end
return nil if $? && $?.exitstatus != 0
entries.sort_by_name
end
-
- def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
+
+ # Fetch the revisions by using a template file that
+ # makes Mercurial produce a xml output.
+ def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
revisions = Revisions.new
- cmd = "#{HG_BIN} -v --encoding utf8 -R #{target('')} log"
+ cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.class.template_path}"
if identifier_from && identifier_to
cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
elsif identifier_from
cmd << " -r #{identifier_from.to_i}:"
end
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
+ cmd << " #{path}" if path
shellout(cmd) do |io|
- changeset = {}
- parsing_descr = false
- line_feeds = 0
-
- io.each_line do |line|
- if line =~ /^(\w+):\s*(.*)$/
- key = $1
- value = $2
- if parsing_descr && line_feeds > 1
- parsing_descr = false
- revisions << build_revision_from_changeset(changeset)
- changeset = {}
- end
- if !parsing_descr
- changeset.store key.to_sym, value
- if $1 == "description"
- parsing_descr = true
- line_feeds = 0
- next
+ begin
+ # HG doesn't close the XML Document...
+ doc = REXML::Document.new(io.read << "</log>")
+ doc.elements.each("log/logentry") do |logentry|
+ paths = []
+ copies = logentry.get_elements('paths/path-copied')
+ logentry.elements.each("paths/path") do |path|
+ # Detect if the added file is a copy
+ if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
+ from_path = c.attributes['copyfrom-path']
+ from_rev = logentry.attributes['revision']
end
+ paths << {:action => path.attributes['action'],
+ :path => "/#{path.text}",
+ :from_path => from_path ? "/#{from_path}" : nil,
+ :from_revision => from_rev ? from_rev : nil
+ }
end
+ paths.sort! { |x,y| x[:path] <=> y[:path] }
+
+ revisions << Revision.new({:identifier => logentry.attributes['revision'],
+ :scmid => logentry.attributes['node'],
+ :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
+ :time => Time.parse(logentry.elements['date'].text).localtime,
+ :message => logentry.elements['msg'].text,
+ :paths => paths
+ })
end
- if parsing_descr
- changeset[:description] << line
- line_feeds += 1 if line.chomp.empty?
- end
+ rescue
+ logger.debug($!)
end
- # Add the last changeset if there is one left
- revisions << build_revision_from_changeset(changeset) if changeset[:date]
end
return nil if $? && $?.exitstatus != 0
revisions
end
- def diff(path, identifier_from, identifier_to=nil, type="inline")
+ def diff(path, identifier_from, identifier_to=nil)
path ||= ''
if identifier_to
identifier_to = identifier_to.to_i
@@ -120,12 +166,12 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def cat(path, identifier=nil)
cmd = "#{HG_BIN} -R #{target('')} cat"
- cmd << " -r #{identifier.to_i}" if identifier
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
cmd << " #{target(path)}"
cat = nil
shellout(cmd) do |io|
@@ -140,6 +186,7 @@ module Redmine
path ||= ''
cmd = "#{HG_BIN} -R #{target('')}"
cmd << " annotate -n -u"
+ cmd << " -r " + (identifier ? identifier.to_s : "tip")
cmd << " -r #{identifier.to_i}" if identifier
cmd << " #{target(path)}"
blame = Annotate.new
@@ -152,47 +199,6 @@ module Redmine
return nil if $? && $?.exitstatus != 0
blame
end
-
- private
-
- # Builds a revision objet from the changeset returned by hg command
- def build_revision_from_changeset(changeset)
- rev_id = changeset[:changeset].to_s.split(':').first.to_i
-
- # Changes
- paths = (rev_id == 0) ?
- # Can't get changes for revision 0 with hg status
- changeset[:files].to_s.split.collect{|path| {:action => 'A', :path => "/#{path}"}} :
- status(rev_id)
-
- Revision.new({:identifier => rev_id,
- :scmid => changeset[:changeset].to_s.split(':').last,
- :author => changeset[:user],
- :time => Time.parse(changeset[:date]),
- :message => changeset[:description],
- :paths => paths
- })
- end
-
- # Returns the file changes for a given revision
- def status(rev_id)
- cmd = "#{HG_BIN} -R #{target('')} status --rev #{rev_id.to_i - 1}:#{rev_id.to_i}"
- result = []
- shellout(cmd) do |io|
- io.each_line do |line|
- action, file = line.chomp.split
- next unless action && file
- file.gsub!("\\", "/")
- case action
- when 'R'
- result << { :action => 'D' , :path => "/#{file}" }
- else
- result << { :action => action, :path => "/#{file}" }
- end
- end
- end
- result
- end
end
end
end
diff --git a/groups/lib/redmine/scm/adapters/subversion_adapter.rb b/groups/lib/redmine/scm/adapters/subversion_adapter.rb
index 40c7eb3f1..2b7f0192e 100644
--- a/groups/lib/redmine/scm/adapters/subversion_adapter.rb
+++ b/groups/lib/redmine/scm/adapters/subversion_adapter.rb
@@ -26,6 +26,25 @@ module Redmine
# SVN executable name
SVN_BIN = "svn"
+ class << self
+ def client_version
+ @@client_version ||= (svn_binary_version || [])
+ end
+
+ def svn_binary_version
+ cmd = "#{SVN_BIN} --version"
+ version = nil
+ shellout(cmd) do |io|
+ # Read svn version in first returned line
+ if m = io.gets.match(%r{((\d+\.)+\d+)})
+ version = m[0].scan(%r{\d+}).collect(&:to_i)
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ version
+ end
+ end
+
# Get info about the svn repository
def info
cmd = "#{SVN_BIN} info --xml #{target('')}"
@@ -64,6 +83,9 @@ module Redmine
begin
doc = REXML::Document.new(output)
doc.elements.each("lists/list/entry") do |entry|
+ # Skip directory if there is no commit date (usually that
+ # means that we don't have read access to it)
+ next if entry.attributes['kind'] == 'dir' && entry.elements['commit'].elements['date'].nil?
entries << Entry.new({:name => entry.elements['name'].text,
:path => ((path.empty? ? "" : "#{path}/") + entry.elements['name'].text),
:kind => entry.attributes['kind'],
@@ -84,7 +106,29 @@ module Redmine
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
entries.sort_by_name
end
-
+
+ def properties(path, identifier=nil)
+ # proplist xml output supported in svn 1.5.0 and higher
+ return nil unless self.class.client_version_above?([1, 5, 0])
+
+ identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
+ cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
+ cmd << credentials_string
+ properties = {}
+ shellout(cmd) do |io|
+ output = io.read
+ begin
+ doc = REXML::Document.new(output)
+ doc.elements.each("properties/target/property") do |property|
+ properties[ property.attributes['name'] ] = property.text
+ end
+ rescue
+ end
+ end
+ return nil if $? && $?.exitstatus != 0
+ properties
+ end
+
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
path ||= ''
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
@@ -139,7 +183,7 @@ module Redmine
end
end
return nil if $? && $?.exitstatus != 0
- DiffTableList.new diff, type
+ diff
end
def cat(path, identifier=nil)
diff --git a/groups/lib/redmine/unified_diff.rb b/groups/lib/redmine/unified_diff.rb
new file mode 100644
index 000000000..aa8994454
--- /dev/null
+++ b/groups/lib/redmine/unified_diff.rb
@@ -0,0 +1,178 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+module Redmine
+ # Class used to parse unified diffs
+ class UnifiedDiff < Array
+ def initialize(diff, type="inline")
+ diff_table = DiffTable.new type
+ diff.each do |line|
+ if line =~ /^(---|\+\+\+) (.*)$/
+ self << diff_table if diff_table.length > 1
+ diff_table = DiffTable.new type
+ end
+ a = diff_table.add_line line
+ end
+ self << diff_table unless diff_table.empty?
+ self
+ end
+ end
+
+ # Class that represents a file diff
+ class DiffTable < Hash
+ attr_reader :file_name, :line_num_l, :line_num_r
+
+ # Initialize with a Diff file and the type of Diff View
+ # The type view must be inline or sbs (side_by_side)
+ def initialize(type="inline")
+ @parsing = false
+ @nb_line = 1
+ @start = false
+ @before = 'same'
+ @second = true
+ @type = type
+ end
+
+ # Function for add a line of this Diff
+ def add_line(line)
+ unless @parsing
+ if line =~ /^(---|\+\+\+) (.*)$/
+ @file_name = $2
+ return false
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $5.to_i
+ @line_num_r = $2.to_i
+ @parsing = true
+ end
+ else
+ if line =~ /^[^\+\-\s@\\]/
+ @parsing = false
+ return false
+ elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
+ @line_num_l = $5.to_i
+ @line_num_r = $2.to_i
+ else
+ @nb_line += 1 if parse_line(line, @type)
+ end
+ end
+ return true
+ end
+
+ def inspect
+ puts '### DIFF TABLE ###'
+ puts "file : #{file_name}"
+ self.each do |d|
+ d.inspect
+ end
+ end
+
+ private
+ # Test if is a Side By Side type
+ def sbs?(type, func)
+ if @start and type == "sbs"
+ if @before == func and @second
+ tmp_nb_line = @nb_line
+ self[tmp_nb_line] = Diff.new
+ else
+ @second = false
+ tmp_nb_line = @start
+ @start += 1
+ @nb_line -= 1
+ end
+ else
+ tmp_nb_line = @nb_line
+ @start = @nb_line
+ self[tmp_nb_line] = Diff.new
+ @second = true
+ end
+ unless self[tmp_nb_line]
+ @nb_line += 1
+ self[tmp_nb_line] = Diff.new
+ else
+ self[tmp_nb_line]
+ end
+ end
+
+ # Escape the HTML for the diff
+ def escapeHTML(line)
+ CGI.escapeHTML(line)
+ end
+
+ def parse_line(line, type="inline")
+ if line[0, 1] == "+"
+ diff = sbs? type, 'add'
+ @before = 'add'
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ diff.type_diff_left = 'diff_in'
+ @line_num_l += 1
+ true
+ elsif line[0, 1] == "-"
+ diff = sbs? type, 'remove'
+ @before = 'remove'
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.type_diff_right = 'diff_out'
+ @line_num_r += 1
+ true
+ elsif line[0, 1] =~ /\s/
+ @before = 'same'
+ @start = false
+ diff = Diff.new
+ diff.line_right = escapeHTML line[1..-1]
+ diff.nb_line_right = @line_num_r
+ diff.line_left = escapeHTML line[1..-1]
+ diff.nb_line_left = @line_num_l
+ self[@nb_line] = diff
+ @line_num_l += 1
+ @line_num_r += 1
+ true
+ elsif line[0, 1] = "\\"
+ true
+ else
+ false
+ end
+ end
+ end
+
+ # A line of diff
+ class Diff
+ attr_accessor :nb_line_left
+ attr_accessor :line_left
+ attr_accessor :nb_line_right
+ attr_accessor :line_right
+ attr_accessor :type_diff_right
+ attr_accessor :type_diff_left
+
+ def initialize()
+ self.nb_line_left = ''
+ self.nb_line_right = ''
+ self.line_left = ''
+ self.line_right = ''
+ self.type_diff_right = ''
+ self.type_diff_left = ''
+ end
+
+ def inspect
+ puts '### Start Line Diff ###'
+ puts self.nb_line_left
+ puts self.line_left
+ puts self.nb_line_right
+ puts self.line_right
+ end
+ end
+end
diff --git a/groups/lib/redmine/wiki_formatting.rb b/groups/lib/redmine/wiki_formatting.rb
index 79da2a38a..8c18d547f 100644
--- a/groups/lib/redmine/wiki_formatting.rb
+++ b/groups/lib/redmine/wiki_formatting.rb
@@ -26,7 +26,7 @@ module Redmine
class TextileFormatter < RedCloth
# auto_link rule after textile rules so that it doesn't break !image_url! tags
- RULES = [:textile, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
+ RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
def initialize(*args)
super
@@ -45,7 +45,7 @@ module Redmine
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
def hard_break( text )
- text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
+ text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
end
# Patch to add code highlighting support to RedCloth
@@ -56,7 +56,7 @@ module Redmine
content = @pre_list[$1.to_i]
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
content = "<code class=\"#{$1} CodeRay\">" +
- CodeRay.scan($2, $1).html(:escape => false, :line_numbers => :inline)
+ CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
end
content
end
@@ -65,10 +65,22 @@ module Redmine
# Patch to add 'table of content' support to RedCloth
def textile_p_withtoc(tag, atts, cite, content)
- if tag =~ /^h(\d)$/
- @toc << [$1.to_i, content]
+ # removes wiki links from the item
+ toc_item = content.gsub(/(\[\[|\]\])/, '')
+ # removes styles
+ # eg. %{color:red}Triggers% => Triggers
+ toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
+
+ # replaces non word caracters by dashes
+ anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
+
+ unless anchor.blank?
+ if tag =~ /^h(\d)$/
+ @toc << [$1.to_i, anchor, toc_item]
+ end
+ atts << " id=\"#{anchor}\""
+ content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
end
- content = "<a name=\"#{@toc.length}\" class=\"wiki-page\"></a>" + content
textile_p(tag, atts, cite, content)
end
@@ -81,13 +93,12 @@ module Redmine
div_class = 'toc'
div_class << ' right' if $1 == '>'
div_class << ' left' if $1 == '<'
- out = "<div class=\"#{div_class}\">"
- @toc.each_with_index do |heading, index|
- # remove wiki links from the item
- toc_item = heading.last.gsub(/(\[\[|\]\])/, '')
- out << "<a href=\"##{index+1}\" class=\"heading#{heading.first}\">#{toc_item}</a>"
+ out = "<ul class=\"#{div_class}\">"
+ @toc.each do |heading|
+ level, anchor, toc_item = heading
+ out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
end
- out << '</div>'
+ out << '</ul>'
out
end
end
@@ -126,6 +137,7 @@ module Redmine
)
(
(?:https?://)| # protocol spec, or
+ (?:ftp://)|
(?:www\.) # www.*
)
(
@@ -149,12 +161,16 @@ module Redmine
end
end
end
-
+
# Turns all email addresses into clickable links (code from Rails).
def inline_auto_mailto(text)
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
- text = $1
- %{<a href="mailto:#{$1}" class="email">#{text}</a>}
+ mail = $1
+ if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
+ mail
+ else
+ %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
+ end
end
end
end
diff --git a/groups/lib/redmine/wiki_formatting/macros.rb b/groups/lib/redmine/wiki_formatting/macros.rb
index 0848aee4e..adfc590e4 100644
--- a/groups/lib/redmine/wiki_formatting/macros.rb
+++ b/groups/lib/redmine/wiki_formatting/macros.rb
@@ -77,6 +77,12 @@ module Redmine
content_tag('dl', out)
end
+ desc "Displays a list of child pages."
+ macro :child_pages do |obj, args|
+ raise 'This macro applies to wiki pages only.' unless obj.is_a?(WikiContent)
+ render_page_hierarchy(obj.page.descendants.group_by(&:parent_id), obj.page.id)
+ end
+
desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
macro :include do |obj, args|
project = @project
diff --git a/groups/lib/tabular_form_builder.rb b/groups/lib/tabular_form_builder.rb
index 5b331fe3f..88e35a6d2 100644
--- a/groups/lib/tabular_form_builder.rb
+++ b/groups/lib/tabular_form_builder.rb
@@ -22,7 +22,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder
def initialize(object_name, object, template, options, proc)
set_language_if_valid options.delete(:lang)
- @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
+ super
end
(field_helpers - %w(radio_button hidden_field) + %w(date_select)).each do |selector|
diff --git a/groups/lib/tasks/email.rake b/groups/lib/tasks/email.rake
new file mode 100644
index 000000000..a37b3e197
--- /dev/null
+++ b/groups/lib/tasks/email.rake
@@ -0,0 +1,105 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+namespace :redmine do
+ namespace :email do
+
+ desc <<-END_DESC
+Read an email from standard input.
+
+Issue attributes control options:
+ project=PROJECT identifier of the target project
+ tracker=TRACKER name of the target tracker
+ category=CATEGORY name of the target category
+ priority=PRIORITY name of the target priority
+ allow_override=ATTRS allow email content to override attributes
+ specified by previous options
+ ATTRS is a comma separated list of attributes
+
+Examples:
+ # No project specified. Emails MUST contain the 'Project' keyword:
+ rake redmine:email:read RAILS_ENV="production" < raw_email
+
+ # Fixed project and default tracker specified, but emails can override
+ # both tracker and priority attributes:
+ rake redmine:email:read RAILS_ENV="production" \\
+ project=foo \\
+ tracker=bug \\
+ allow_override=tracker,priority < raw_email
+END_DESC
+
+ task :read => :environment do
+ options = { :issue => {} }
+ %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
+
+ MailHandler.receive(STDIN.read, options)
+ end
+
+ desc <<-END_DESC
+Read emails from an IMAP server.
+
+Available IMAP options:
+ host=HOST IMAP server host (default: 127.0.0.1)
+ port=PORT IMAP server port (default: 143)
+ ssl=SSL Use SSL? (default: false)
+ username=USERNAME IMAP account
+ password=PASSWORD IMAP password
+ folder=FOLDER IMAP folder to read (default: INBOX)
+
+Issue attributes control options:
+ project=PROJECT identifier of the target project
+ tracker=TRACKER name of the target tracker
+ category=CATEGORY name of the target category
+ priority=PRIORITY name of the target priority
+ allow_override=ATTRS allow email content to override attributes
+ specified by previous options
+ ATTRS is a comma separated list of attributes
+
+Examples:
+ # No project specified. Emails MUST contain the 'Project' keyword:
+
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\
+ host=imap.foo.bar username=redmine@example.net password=xxx
+
+
+ # Fixed project and default tracker specified, but emails can override
+ # both tracker and priority attributes:
+
+ rake redmine:email:receive_iamp RAILS_ENV="production" \\
+ host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
+ project=foo \\
+ tracker=bug \\
+ allow_override=tracker,priority
+END_DESC
+
+ task :receive_imap => :environment do
+ imap_options = {:host => ENV['host'],
+ :port => ENV['port'],
+ :ssl => ENV['ssl'],
+ :username => ENV['username'],
+ :password => ENV['password'],
+ :folder => ENV['folder']}
+
+ options = { :issue => {} }
+ %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
+ options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
+
+ Redmine::IMAP.check(imap_options, options)
+ end
+ end
+end
diff --git a/groups/lib/tasks/migrate_from_trac.rake b/groups/lib/tasks/migrate_from_trac.rake
index 7fe1f09ac..880964ff8 100644
--- a/groups/lib/tasks/migrate_from_trac.rake
+++ b/groups/lib/tasks/migrate_from_trac.rake
@@ -92,12 +92,17 @@ namespace :redmine do
set_table_name :milestone
def due
- if read_attribute(:due) > 0
+ if read_attribute(:due) && read_attribute(:due) > 0
Time.at(read_attribute(:due)).to_date
else
nil
end
end
+
+ def description
+ # Attribute is named descr in Trac v0.8.x
+ has_attribute?(:descr) ? read_attribute(:descr) : read_attribute(:description)
+ end
end
class TracTicketCustom < ActiveRecord::Base
@@ -126,6 +131,10 @@ namespace :redmine do
File.open("#{trac_fullpath}", 'rb').read
end
+ def description
+ read_attribute(:description).to_s.slice(0,255)
+ end
+
private
def trac_fullpath
attachment_type = read_attribute(:type)
@@ -140,7 +149,10 @@ namespace :redmine do
# ticket changes: only migrate status changes and comments
has_many :changes, :class_name => "TracTicketChange", :foreign_key => :ticket
- has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'ticket'"
+ has_many :attachments, :class_name => "TracAttachment",
+ :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
+ " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'ticket'" +
+ ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
has_many :customs, :class_name => "TracTicketCustom", :foreign_key => :ticket
def ticket_type
@@ -177,7 +189,10 @@ namespace :redmine do
set_table_name :wiki
set_primary_key :name
- has_many :attachments, :class_name => "TracAttachment", :foreign_key => :id, :conditions => "#{TracMigrate::TracAttachment.table_name}.type = 'wiki'"
+ has_many :attachments, :class_name => "TracAttachment",
+ :finder_sql => "SELECT DISTINCT attachment.* FROM #{TracMigrate::TracAttachment.table_name}" +
+ " WHERE #{TracMigrate::TracAttachment.table_name}.type = 'wiki'" +
+ ' AND #{TracMigrate::TracAttachment.table_name}.id = \'#{id}\''
def self.columns
# Hides readonly Trac field to prevent clash with AR readonly? method (Rails 2.0)
@@ -191,6 +206,10 @@ namespace :redmine do
set_table_name :permission
end
+ class TracSessionAttribute < ActiveRecord::Base
+ set_table_name :session_attribute
+ end
+
def self.find_or_create_user(username, project_member = false)
return User.anonymous if username.blank?
@@ -198,10 +217,23 @@ namespace :redmine do
if !u
# Create a new user if not found
mail = username[0,limit_for(User, 'mail')]
+ if mail_attr = TracSessionAttribute.find_by_sid_and_name(username, 'email')
+ mail = mail_attr.value
+ end
mail = "#{mail}@foo.bar" unless mail.include?("@")
- u = User.new :firstname => username[0,limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
- :lastname => '-',
- :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-')
+
+ name = username
+ if name_attr = TracSessionAttribute.find_by_sid_and_name(username, 'name')
+ name = name_attr.value
+ end
+ name =~ (/(.*)(\s+\w+)?/)
+ fn = $1.strip
+ ln = ($2 || '-').strip
+
+ u = User.new :mail => mail.gsub(/[^-@a-z0-9\.]/i, '-'),
+ :firstname => fn[0, limit_for(User, 'firstname')].gsub(/[^\w\s\'\-]/i, '-'),
+ :lastname => ln[0, limit_for(User, 'lastname')].gsub(/[^\w\s\'\-]/i, '-')
+
u.login = username[0,limit_for(User, 'login')].gsub(/[^a-z0-9_\-@\.]/i, '-')
u.password = 'trac'
u.admin = true if TracPermission.find_by_username_and_action(username, 'admin')
@@ -233,7 +265,8 @@ namespace :redmine do
text = text.gsub(/\[\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
text = text.gsub(/\[wiki:\"(.+)\".*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
- text = text.gsub(/\[wiki:([^\s\]]+).*\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
+ text = text.gsub(/\[wiki:([^\s\]]+)\]/) {|s| "[[#{$1.delete(',./?;|:')}]]"}
+ text = text.gsub(/\[wiki:([^\s\]]+)\s(.*)\]/) {|s| "[[#{$1.delete(',./?;|:')}|#{$2.delete(',./?;|:')}]]"}
# Links to pages UsingJustWikiCaps
text = text.gsub(/([^!]|^)(^| )([A-Z][a-z]+[A-Z][a-zA-Z]+)/, '\\1\\2[[\3]]')
@@ -408,6 +441,7 @@ namespace :redmine do
a.file = attachment
a.author = find_or_create_user(attachment.author)
a.container = i
+ a.description = attachment.description
migrated_ticket_attachments += 1 if a.save
end
@@ -456,6 +490,7 @@ namespace :redmine do
a = Attachment.new :created_on => attachment.time
a.file = attachment
a.author = find_or_create_user(attachment.author)
+ a.description = attachment.description
a.container = p
migrated_wiki_attachments += 1 if a.save
end
diff --git a/groups/lib/tasks/reminder.rake b/groups/lib/tasks/reminder.rake
new file mode 100644
index 000000000..73844fb79
--- /dev/null
+++ b/groups/lib/tasks/reminder.rake
@@ -0,0 +1,39 @@
+# redMine - project management software
+# Copyright (C) 2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+desc <<-END_DESC
+Send reminders about issues due in the next days.
+
+Available options:
+ * days => number of days to remind about (defaults to 7)
+ * tracker => id of tracker (defaults to all trackers)
+ * project => id or identifier of project (defaults to all projects)
+
+Example:
+ rake redmine:send_reminders days=7 RAILS_ENV="production"
+END_DESC
+
+namespace :redmine do
+ task :send_reminders => :environment do
+ options = {}
+ options[:days] = ENV['days'].to_i if ENV['days']
+ options[:project] = ENV['project'] if ENV['project']
+ options[:tracker] = ENV['tracker'].to_i if ENV['tracker']
+
+ Mailer.reminders(options)
+ end
+end
diff --git a/groups/lib/tasks/testing.rake b/groups/lib/tasks/testing.rake
new file mode 100644
index 000000000..42f756f68
--- /dev/null
+++ b/groups/lib/tasks/testing.rake
@@ -0,0 +1,46 @@
+### From http://svn.geekdaily.org/public/rails/plugins/generally_useful/tasks/coverage_via_rcov.rake
+
+### Inspired by http://blog.labratz.net/articles/2006/12/2/a-rake-task-for-rcov
+begin
+ require 'rcov/rcovtask'
+
+ rcov_options = "--rails --aggregate test/coverage.data --exclude '/gems/'"
+
+ namespace :test do
+ desc "Aggregate code coverage for all tests"
+ Rcov::RcovTask.new('coverage') do |t|
+ t.libs << 'test'
+ t.test_files = FileList['test/{unit,integration,functional}/*_test.rb']
+ t.verbose = true
+ t.rcov_opts << rcov_options
+ end
+
+ namespace :coverage do
+ desc "Delete coverage test data"
+ task :clean do
+ rm_f "test/coverage.data"
+ rm_rf "test/coverage"
+ end
+
+ desc "Aggregate code coverage for all tests with HTML output"
+ Rcov::RcovTask.new('html') do |t|
+ t.libs << 'test'
+ t.test_files = FileList['test/{unit,integration,functional}/*_test.rb']
+ t.output_dir = "test/coverage"
+ t.verbose = true
+ t.rcov_opts << rcov_options
+ end
+
+ desc "Open the HTML coverage report"
+ task :show_results do
+ system "open test/coverage/index.html"
+ end
+
+ task :full => "test:coverage:clean"
+ task :full => "test:coverage:html"
+ task :full => "test:coverage:show_results"
+ end
+ end
+rescue LoadError
+ # rcov not available
+end
diff --git a/groups/public/help/wiki_syntax.html b/groups/public/help/wiki_syntax.html
index 6a0e10022..846fe1bf7 100644
--- a/groups/public/help/wiki_syntax.html
+++ b/groups/public/help/wiki_syntax.html
@@ -22,13 +22,13 @@ table td h3 { font-size: 1.2em; text-align: left; }
<table width="100%">
<tr><th colspan="3">Font Styles</th></tr>
-<tr><th><img src="../../images/jstoolbar/bt_strong.png" style="border: 1px solid #bbb;" alt="Strong" /></th><td width="50%">*Strong*</td><td width="50%"><strong>Strong</strong></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_em.png" style="border: 1px solid #bbb;" alt="Italic" /></th><td>_Italic_</td><td><em>Italic</em></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_ins.png" style="border: 1px solid #bbb;" alt="Underline" /></th><td>+Underline+</td><td><ins>Underline</ins></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_del.png" style="border: 1px solid #bbb;" alt="Deleted" /></th><td>-Deleted-</td><td><del>Deleted</del></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_strong.png" style="border: 1px solid #bbb;" alt="Strong" /></th><td width="50%">*Strong*</td><td width="50%"><strong>Strong</strong></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_em.png" style="border: 1px solid #bbb;" alt="Italic" /></th><td>_Italic_</td><td><em>Italic</em></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_ins.png" style="border: 1px solid #bbb;" alt="Underline" /></th><td>+Underline+</td><td><ins>Underline</ins></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_del.png" style="border: 1px solid #bbb;" alt="Deleted" /></th><td>-Deleted-</td><td><del>Deleted</del></td></tr>
<tr><th></th><td>??Quote??</td><td><cite>Quote</cite></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_code.png" style="border: 1px solid #bbb;" alt="Inline Code" /></th><td>@Inline Code@</td><td><code>Inline Code</code></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_pre.png" style="border: 1px solid #bbb;" alt="Preformatted text" /></th><td>&lt;pre><br />&nbsp;lines<br />&nbsp;of code<br />&lt;/pre></td><td>
+<tr><th><img src="../images/jstoolbar/bt_code.png" style="border: 1px solid #bbb;" alt="Inline Code" /></th><td>@Inline Code@</td><td><code>Inline Code</code></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_pre.png" style="border: 1px solid #bbb;" alt="Preformatted text" /></th><td>&lt;pre><br />&nbsp;lines<br />&nbsp;of code<br />&lt;/pre></td><td>
<pre>
lines
of code
@@ -36,27 +36,27 @@ table td h3 { font-size: 1.2em; text-align: left; }
</td></tr>
<tr><th colspan="3">Lists</th></tr>
-<tr><th><img src="../../images/jstoolbar/bt_ul.png" style="border: 1px solid #bbb;" alt="Unordered list" /></th><td>* Item 1<br />* Item 2</td><td><ul><li>Item 1</li><li>Item 2</li></ul></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_ol.png" style="border: 1px solid #bbb;" alt="Ordered list" /></th><td># Item 1<br /># Item 2</td><td><ol><li>Item 1</li><li>Item 2</li></ol></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_ul.png" style="border: 1px solid #bbb;" alt="Unordered list" /></th><td>* Item 1<br />* Item 2</td><td><ul><li>Item 1</li><li>Item 2</li></ul></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_ol.png" style="border: 1px solid #bbb;" alt="Ordered list" /></th><td># Item 1<br /># Item 2</td><td><ol><li>Item 1</li><li>Item 2</li></ol></td></tr>
<tr><th colspan="3">Headings</th></tr>
-<tr><th><img src="../../images/jstoolbar/bt_h1.png" style="border: 1px solid #bbb;" alt="Heading 1" /></th><td>h1. Title 1</td><td><h1>Title 1</h1></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_h2.png" style="border: 1px solid #bbb;" alt="Heading 2" /></th><td>h2. Title 2</td><td><h2>Title 2</h2></td></tr>
-<tr><th><img src="../../images/jstoolbar/bt_h3.png" style="border: 1px solid #bbb;" alt="Heading 3" /></th><td>h3. Title 3</td><td><h3>Title 3</h3></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_h1.png" style="border: 1px solid #bbb;" alt="Heading 1" /></th><td>h1. Title 1</td><td><h1>Title 1</h1></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_h2.png" style="border: 1px solid #bbb;" alt="Heading 2" /></th><td>h2. Title 2</td><td><h2>Title 2</h2></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_h3.png" style="border: 1px solid #bbb;" alt="Heading 3" /></th><td>h3. Title 3</td><td><h3>Title 3</h3></td></tr>
<tr><th colspan="3">Links</th></tr>
<tr><th></th><td>http://foo.bar</td><td><a href="#">http://foo.bar</a></td></tr>
<tr><th></th><td>"Foo":http://foo.bar</td><td><a href="#">Foo</a></td></tr>
<tr><th colspan="3">Redmine links</th></tr>
-<tr><th><img src="../../images/jstoolbar/bt_link.png" style="border: 1px solid #bbb;" alt="Link to a Wiki page" /></th><td>[[Wiki page]]</td><td><a href="#">Wiki page</a></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_link.png" style="border: 1px solid #bbb;" alt="Link to a Wiki page" /></th><td>[[Wiki page]]</td><td><a href="#">Wiki page</a></td></tr>
<tr><th></th><td>Issue #12</td><td>Issue <a href="#">#12</a></td></tr>
<tr><th></th><td>Revision r43</td><td>Revision <a href="#">r43</a></td></tr>
<tr><th></th><td>commit:"f30e13e43"</td><td><a href="#">f30e13e4</a></td></tr>
<tr><th></th><td>source:some/file</td><td><a href="#">source:some/file</a></td></tr>
<tr><th colspan="3">Inline images</th></tr>
-<tr><th><img src="../../images/jstoolbar/bt_img.png" style="border: 1px solid #bbb;" alt="Image" /></th><td>!<em>image_url</em>!</td><td></td></tr>
+<tr><th><img src="../images/jstoolbar/bt_img.png" style="border: 1px solid #bbb;" alt="Image" /></th><td>!<em>image_url</em>!</td><td></td></tr>
<tr><th></th><td>!<em>attached_image</em>!</td><td></td></tr>
</table>
diff --git a/groups/public/images/bullet_toggle_minus.png b/groups/public/images/bullet_toggle_minus.png
new file mode 100644
index 000000000..5ce75938f
--- /dev/null
+++ b/groups/public/images/bullet_toggle_minus.png
Binary files differ
diff --git a/groups/public/images/bullet_toggle_plus.png b/groups/public/images/bullet_toggle_plus.png
new file mode 100644
index 000000000..b3603d30a
--- /dev/null
+++ b/groups/public/images/bullet_toggle_plus.png
Binary files differ
diff --git a/groups/public/images/comment.png b/groups/public/images/comment.png
new file mode 100644
index 000000000..7bc9233ea
--- /dev/null
+++ b/groups/public/images/comment.png
Binary files differ
diff --git a/groups/public/images/expand.png b/groups/public/images/expand.png
deleted file mode 100644
index 3e3aaa441..000000000
--- a/groups/public/images/expand.png
+++ /dev/null
Binary files differ
diff --git a/groups/public/images/jstoolbar/bt_bq.png b/groups/public/images/jstoolbar/bt_bq.png
new file mode 100644
index 000000000..c3af4e07f
--- /dev/null
+++ b/groups/public/images/jstoolbar/bt_bq.png
Binary files differ
diff --git a/groups/public/images/jstoolbar/bt_bq_remove.png b/groups/public/images/jstoolbar/bt_bq_remove.png
new file mode 100644
index 000000000..05d5ff7c7
--- /dev/null
+++ b/groups/public/images/jstoolbar/bt_bq_remove.png
Binary files differ
diff --git a/groups/public/images/locked.png b/groups/public/images/locked.png
index c2789e35c..82d629961 100644
--- a/groups/public/images/locked.png
+++ b/groups/public/images/locked.png
Binary files differ
diff --git a/groups/public/images/projects.png b/groups/public/images/projects.png
index 244c896f0..073c7219d 100644
--- a/groups/public/images/projects.png
+++ b/groups/public/images/projects.png
Binary files differ
diff --git a/groups/public/images/ticket_note.png b/groups/public/images/ticket_note.png
new file mode 100644
index 000000000..c69db223f
--- /dev/null
+++ b/groups/public/images/ticket_note.png
Binary files differ
diff --git a/groups/public/images/unlock.png b/groups/public/images/unlock.png
index e0d414978..f15fead72 100644
--- a/groups/public/images/unlock.png
+++ b/groups/public/images/unlock.png
Binary files differ
diff --git a/groups/public/javascripts/application.js b/groups/public/javascripts/application.js
index 4e8849842..3becbeb21 100644
--- a/groups/public/javascripts/application.js
+++ b/groups/public/javascripts/application.js
@@ -2,14 +2,27 @@
Copyright (C) 2006-2008 Jean-Philippe Lang */
function checkAll (id, checked) {
- var el = document.getElementById(id);
- for (var i = 0; i < el.elements.length; i++) {
- if (el.elements[i].disabled==false) {
- el.elements[i].checked = checked;
+ var els = Element.descendants(id);
+ for (var i = 0; i < els.length; i++) {
+ if (els[i].disabled==false) {
+ els[i].checked = checked;
}
}
}
+function toggleCheckboxesBySelector(selector) {
+ boxes = $$(selector);
+ var all_checked = true;
+ for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
+ for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; }
+}
+
+function showAndScrollTo(id, focus) {
+ Element.show(id);
+ if (focus!=null) { Form.Element.focus(focus); }
+ Element.scrollTo(id);
+}
+
var fileFieldCount = 1;
function addFileField() {
@@ -56,7 +69,7 @@ function setPredecessorFieldsVisibility() {
function promptToRemote(text, param, url) {
value = prompt(text + ':');
if (value) {
- new Ajax.Request(url + '?' + param + '=' + value, {asynchronous:true, evalScripts:true});
+ new Ajax.Request(url + '?' + param + '=' + encodeURIComponent(value), {asynchronous:true, evalScripts:true});
return false;
}
}
@@ -107,6 +120,15 @@ function scmEntryLoaded(id) {
Element.removeClassName(id, 'loading');
}
+function randomKey(size) {
+ var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
+ var key = '';
+ for (i = 0; i < size; i++) {
+ key += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return key;
+}
+
/* shows and hides ajax indicator */
Ajax.Responders.register({
onCreate: function(){
diff --git a/groups/public/javascripts/calendar/lang/calendar-he.js b/groups/public/javascripts/calendar/lang/calendar-he.js
index bd92e0073..9d4c87db0 100644
--- a/groups/public/javascripts/calendar/lang/calendar-he.js
+++ b/groups/public/javascripts/calendar/lang/calendar-he.js
@@ -113,7 +113,7 @@ Calendar._TT["DAY_FIRST"] = "הצג %s קוד×";
// This may be locale-dependent. It specifies the week-end days, as an array
// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
// means Monday, etc.
-Calendar._TT["WEEKEND"] = "6,7";
+Calendar._TT["WEEKEND"] = "5,6";
Calendar._TT["CLOSE"] = "סגור";
Calendar._TT["TODAY"] = "היו×";
diff --git a/groups/public/javascripts/calendar/lang/calendar-hu.js b/groups/public/javascripts/calendar/lang/calendar-hu.js
new file mode 100644
index 000000000..0e219c123
--- /dev/null
+++ b/groups/public/javascripts/calendar/lang/calendar-hu.js
@@ -0,0 +1,127 @@
+// ** I18N
+
+// Calendar HU language
+// Author: Takács Gábor
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("Vasárnap",
+ "Hétfő",
+ "Kedd",
+ "Szerda",
+ "Csütörtök",
+ "Péntek",
+ "Szombat",
+ "Vasárnap");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("Vas",
+ "Hét",
+ "Ked",
+ "Sze",
+ "Csü",
+ "Pén",
+ "Szo",
+ "Vas");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("Január",
+ "Február",
+ "Március",
+ "Ãprilis",
+ "Május",
+ "Június",
+ "Július",
+ "Augusztus",
+ "Szeptember",
+ "Október",
+ "November",
+ "December");
+
+// short month names
+Calendar._SMN = new Array
+("Jan",
+ "Feb",
+ "Már",
+ "Ãpr",
+ "Máj",
+ "Jún",
+ "Júl",
+ "Aug",
+ "Szep",
+ "Okt",
+ "Nov",
+ "Dec");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "A naptár leírása";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "Előző év (nyomvatart = menü)";
+Calendar._TT["PREV_MONTH"] = "Előző hónap (nyomvatart = menü)";
+Calendar._TT["GO_TODAY"] = "Irány a Ma";
+Calendar._TT["NEXT_MONTH"] = "Következő hónap (nyomvatart = menü)";
+Calendar._TT["NEXT_YEAR"] = "Következő év (nyomvatart = menü)";
+Calendar._TT["SEL_DATE"] = "Válasszon dátumot";
+Calendar._TT["DRAG_TO_MOVE"] = "Fogd és vidd";
+Calendar._TT["PART_TODAY"] = " (ma)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "%s megjelenítése elsőként";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "Bezár";
+Calendar._TT["TODAY"] = "Ma";
+Calendar._TT["TIME_PART"] = "(Shift-)Click vagy húzd az érték változtatásához";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y.%m.%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%B %e, %A";
+
+Calendar._TT["WK"] = "hét";
+Calendar._TT["TIME"] = "Idő:";
diff --git a/groups/public/javascripts/calendar/lang/calendar-pt-br.js b/groups/public/javascripts/calendar/lang/calendar-pt-br.js
index 5d4d014ce..bf7734ab3 100644
--- a/groups/public/javascripts/calendar/lang/calendar-pt-br.js
+++ b/groups/public/javascripts/calendar/lang/calendar-pt-br.js
@@ -2,7 +2,8 @@
// Calendar pt_BR language
// Author: Adalberto Machado, <betosm@terra.com.br>
-// Encoding: any
+// Review: Alexandre da Silva, <simpsomboy@gmail.com>
+// Encoding: UTF-8
// Distributed under the same terms as the calendar itself.
// For translators: please use UTF-8 if possible. We strongly believe that
@@ -13,7 +14,7 @@
Calendar._DN = new Array
("Domingo",
"Segunda",
- "Terca",
+ "Terça",
"Quarta",
"Quinta",
"Sexta",
@@ -45,13 +46,13 @@ Calendar._SDN = new Array
// First day of the week. "0" means display Sunday first, "1" means display
// Monday first, etc.
-Calendar._FD = 1;
+Calendar._FD = 0;
// full month names
Calendar._MN = new Array
("Janeiro",
"Fevereiro",
- "Marco",
+ "Março",
"Abril",
"Maio",
"Junho",
@@ -79,29 +80,30 @@ Calendar._SMN = new Array
// tooltips
Calendar._TT = {};
-Calendar._TT["INFO"] = "Sobre o calendario";
+Calendar._TT["INFO"] = "Sobre o calendário";
Calendar._TT["ABOUT"] =
"DHTML Date/Time Selector\n" +
"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
-"Ultima versao visite: http://www.dynarch.com/projects/calendar/\n" +
-"Distribuido sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." +
+"Última versão visite: http://www.dynarch.com/projects/calendar/\n" +
+"Distribuído sobre GNU LGPL. Veja http://gnu.org/licenses/lgpl.html para detalhes." +
"\n\n" +
-"Selecao de data:\n" +
-"- Use os botoes \xab, \xbb para selecionar o ano\n" +
-"- Use os botoes " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " para selecionar o mes\n" +
-"- Segure o botao do mouse em qualquer um desses botoes para selecao rapida.";
+"Seleção de data:\n" +
+"- Use os botões \xab, \xbb para selecionar o ano\n" +
+"- Use os botões " + String.fromCharCode(0x2039) + ", " +
+String.fromCharCode(0x203a) + " para selecionar o mês\n" +
+"- Segure o botão do mouse em qualquer um desses botões para seleção rápida.";
Calendar._TT["ABOUT_TIME"] = "\n\n" +
-"Selecao de hora:\n" +
+"Seleção de hora:\n" +
"- Clique em qualquer parte da hora para incrementar\n" +
"- ou Shift-click para decrementar\n" +
-"- ou clique e segure para selecao rapida.";
+"- ou clique e segure para seleção rápida.";
Calendar._TT["PREV_YEAR"] = "Ant. ano (segure para menu)";
-Calendar._TT["PREV_MONTH"] = "Ant. mes (segure para menu)";
+Calendar._TT["PREV_MONTH"] = "Ant. mês (segure para menu)";
Calendar._TT["GO_TODAY"] = "Hoje";
-Calendar._TT["NEXT_MONTH"] = "Prox. mes (segure para menu)";
-Calendar._TT["NEXT_YEAR"] = "Prox. ano (segure para menu)";
+Calendar._TT["NEXT_MONTH"] = "Próx. mes (segure para menu)";
+Calendar._TT["NEXT_YEAR"] = "Próx. ano (segure para menu)";
Calendar._TT["SEL_DATE"] = "Selecione a data";
Calendar._TT["DRAG_TO_MOVE"] = "Arraste para mover";
Calendar._TT["PART_TODAY"] = " (hoje)";
diff --git a/groups/public/javascripts/calendar/lang/calendar-th.js b/groups/public/javascripts/calendar/lang/calendar-th.js
new file mode 100644
index 000000000..dc4809e52
--- /dev/null
+++ b/groups/public/javascripts/calendar/lang/calendar-th.js
@@ -0,0 +1,127 @@
+// ** I18N
+
+// Calendar EN language
+// Author: Gampol Thitinilnithi, <gampolt@gmail.com>
+// Encoding: UTF-8
+// Distributed under the same terms as the calendar itself.
+
+// For translators: please use UTF-8 if possible. We strongly believe that
+// Unicode is the answer to a real internationalized world. Also please
+// include your contact information in the header, as can be seen above.
+
+// full day names
+Calendar._DN = new Array
+("อาทิตย์",
+ "จันทร์",
+ "อังคาร",
+ "พุธ",
+ "พฤหัสบดี",
+ "ศุà¸à¸£à¹Œ",
+ "เสาร์",
+ "อาทิตย์");
+
+// Please note that the following array of short day names (and the same goes
+// for short month names, _SMN) isn't absolutely necessary. We give it here
+// for exemplification on how one can customize the short day names, but if
+// they are simply the first N letters of the full name you can simply say:
+//
+// Calendar._SDN_len = N; // short day name length
+// Calendar._SMN_len = N; // short month name length
+//
+// If N = 3 then this is not needed either since we assume a value of 3 if not
+// present, to be compatible with translation files that were written before
+// this feature.
+
+// short day names
+Calendar._SDN = new Array
+("อา.",
+ "จ.",
+ "อ.",
+ "พ.",
+ "พฤ.",
+ "ศ.",
+ "ส.",
+ "อา.");
+
+// First day of the week. "0" means display Sunday first, "1" means display
+// Monday first, etc.
+Calendar._FD = 1;
+
+// full month names
+Calendar._MN = new Array
+("มà¸à¸£à¸²à¸„ม",
+ "à¸à¸¸à¸¡à¸ à¸²à¸žà¸±à¸™à¸˜à¹Œ",
+ "มีนาคม",
+ "เมษายน",
+ "พฤษภาคม",
+ "มิถุนายน",
+ "à¸à¸£à¸à¸Žà¸²à¸„ม",
+ "สิงหาคม",
+ "à¸à¸±à¸™à¸¢à¸²à¸¢à¸™",
+ "ตุลาคม",
+ "พฤศจิà¸à¸²à¸¢à¸™",
+ "ธันวาคม");
+
+// short month names
+Calendar._SMN = new Array
+("ม.ค.",
+ "à¸.พ.",
+ "มี.ค.",
+ "เม.ย.",
+ "พ.ค.",
+ "มิ.ย.",
+ "à¸.ค.",
+ "ส.ค.",
+ "à¸.ย.",
+ "ต.ค.",
+ "พ.ย.",
+ "ธ.ค.");
+
+// tooltips
+Calendar._TT = {};
+Calendar._TT["INFO"] = "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸šà¸›à¸à¸´à¸—ิน";
+
+Calendar._TT["ABOUT"] =
+"DHTML Date/Time Selector\n" +
+"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
+"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
+"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"\n\n" +
+"Date selection:\n" +
+"- Use the \xab, \xbb buttons to select year\n" +
+"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
+"- Hold mouse button on any of the above buttons for faster selection.";
+Calendar._TT["ABOUT_TIME"] = "\n\n" +
+"Time selection:\n" +
+"- Click on any of the time parts to increase it\n" +
+"- or Shift-click to decrease it\n" +
+"- or click and drag for faster selection.";
+
+Calendar._TT["PREV_YEAR"] = "ปีที่à¹à¸¥à¹‰à¸§ (ถ้าà¸à¸”ค้างจะมีเมนู)";
+Calendar._TT["PREV_MONTH"] = "เดือนที่à¹à¸¥à¹‰à¸§ (ถ้าà¸à¸”ค้างจะมีเมนู)";
+Calendar._TT["GO_TODAY"] = "ไปที่วันนี้";
+Calendar._TT["NEXT_MONTH"] = "เดือนหน้า (ถ้าà¸à¸”ค้างจะมีเมนู)";
+Calendar._TT["NEXT_YEAR"] = "ปีหน้า (ถ้าà¸à¸”ค้างจะมีเมนู)";
+Calendar._TT["SEL_DATE"] = "เลือà¸à¸§à¸±à¸™";
+Calendar._TT["DRAG_TO_MOVE"] = "à¸à¸”à¹à¸¥à¹‰à¸§à¸¥à¸²à¸à¹€à¸žà¸·à¹ˆà¸­à¸¢à¹‰à¸²à¸¢";
+Calendar._TT["PART_TODAY"] = " (วันนี้)";
+
+// the following is to inform that "%s" is to be the first day of week
+// %s will be replaced with the day name.
+Calendar._TT["DAY_FIRST"] = "à¹à¸ªà¸”ง %s เป็นวันà¹à¸£à¸";
+
+// This may be locale-dependent. It specifies the week-end days, as an array
+// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
+// means Monday, etc.
+Calendar._TT["WEEKEND"] = "0,6";
+
+Calendar._TT["CLOSE"] = "ปิด";
+Calendar._TT["TODAY"] = "วันนี้";
+Calendar._TT["TIME_PART"] = "(Shift-)à¸à¸”หรือà¸à¸”à¹à¸¥à¹‰à¸§à¸¥à¸²à¸à¹€à¸žà¸·à¹ˆà¸­à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¸„่า";
+
+// date formats
+Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
+Calendar._TT["TT_DATE_FORMAT"] = "%a %e %b";
+
+Calendar._TT["WK"] = "wk";
+Calendar._TT["TIME"] = "เวลา:";
diff --git a/groups/public/javascripts/calendar/lang/calendar-zh-tw.js b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js
index c48d25b0e..1e759db10 100644
--- a/groups/public/javascripts/calendar/lang/calendar-zh-tw.js
+++ b/groups/public/javascripts/calendar/lang/calendar-zh-tw.js
@@ -84,13 +84,13 @@ Calendar._TT["INFO"] = "關於 calendar";
Calendar._TT["ABOUT"] =
"DHTML 日期/時間 鏿“‡å™¨\n" +
"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
-"最For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
-"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"最新版本å–å¾—ä½å€: http://www.dynarch.com/projects/calendar/\n" +
+"使用 GNU LGPL 發行. åƒè€ƒ http://gnu.org/licenses/lgpl.html 以å–得更多關於 LGPL 之細節。" +
"\n\n" +
-"Date selection:\n" +
-"- Use the \xab, \xbb buttons to select year\n" +
-"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
-"- Hold mouse button on any of the above buttons for faster selection.";
+"æ—¥æœŸé¸æ“‡æ–¹å¼:\n" +
+"- 使用滑鼠點擊 \xab 〠\xbb æŒ‰éˆ•é¸æ“‡å¹´ä»½\n" +
+"- 使用滑鼠點擊 " + String.fromCharCode(0x2039) + " 〠" + String.fromCharCode(0x203a) + " æŒ‰éˆ•é¸æ“‡æœˆä»½\n" +
+"- 使用滑鼠點擊上述按鈕並按ä½ä¸æ”¾ï¼Œå¯é–‹å•Ÿå¿«é€Ÿé¸å–®ã€‚";
Calendar._TT["ABOUT_TIME"] = "\n\n" +
"æ™‚é–“é¸æ“‡æ–¹å¼ï¼š\n" +
"- ã€Œå–®æ“Šã€æ™‚分秒為éžå¢ž\n" +
diff --git a/groups/public/javascripts/calendar/lang/calendar-zh.js b/groups/public/javascripts/calendar/lang/calendar-zh.js
index ddb092bfa..121653fba 100644
--- a/groups/public/javascripts/calendar/lang/calendar-zh.js
+++ b/groups/public/javascripts/calendar/lang/calendar-zh.js
@@ -82,33 +82,33 @@ Calendar._TT = {};
Calendar._TT["INFO"] = "关于日历";
Calendar._TT["ABOUT"] =
-"DHTML Date/Time Selector\n" +
+"DHTML 日期/时间 选择器\n" +
"(c) dynarch.com 2002-2005 / Author: Mihai Bazon\n" + // don't translate this this ;-)
-"For latest version visit: http://www.dynarch.com/projects/calendar/\n" +
-"Distributed under GNU LGPL. See http://gnu.org/licenses/lgpl.html for details." +
+"最新版本请访问: http://www.dynarch.com/projects/calendar/\n" +
+"éµå¾ª GNU LGPL å‘布。详情请查阅 http://gnu.org/licenses/lgpl.html " +
"\n\n" +
-"Date selection:\n" +
-"- Use the \xab, \xbb buttons to select year\n" +
-"- Use the " + String.fromCharCode(0x2039) + ", " + String.fromCharCode(0x203a) + " buttons to select month\n" +
-"- Hold mouse button on any of the above buttons for faster selection.";
+"日期选择:\n" +
+"- 使用 \xab,\xbb 按钮选择年\n" +
+"- 使用 " + String.fromCharCode(0x2039) + "," + String.fromCharCode(0x203a) + " 按钮选择月\n" +
+"- 在上述按钮上按ä½ä¸æ”¾å¯ä»¥å¿«é€Ÿé€‰æ‹©";
Calendar._TT["ABOUT_TIME"] = "\n\n" +
-"Time selection:\n" +
-"- Click on any of the time parts to increase it\n" +
-"- or Shift-click to decrease it\n" +
-"- or click and drag for faster selection.";
+"时间选择:\n" +
+"- 点击时间的任æ„部分æ¥å¢žåŠ \n" +
+"- Shift加点击æ¥å‡å°‘\n" +
+"- ç‚¹å‡»åŽæ‹–动进行快速选择";
-Calendar._TT["PREV_YEAR"] = "上年 (hold for menu)";
-Calendar._TT["PREV_MONTH"] = "上月 (hold for menu)";
+Calendar._TT["PREV_YEAR"] = "上年(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)";
+Calendar._TT["PREV_MONTH"] = "上月(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)";
Calendar._TT["GO_TODAY"] = "回到今天";
-Calendar._TT["NEXT_MONTH"] = "下月 (hold for menu)";
-Calendar._TT["NEXT_YEAR"] = "下年 (hold for menu)";
+Calendar._TT["NEXT_MONTH"] = "下月(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)";
+Calendar._TT["NEXT_YEAR"] = "下年(按ä½ä¸æ”¾æ˜¾ç¤ºèœå•)";
Calendar._TT["SEL_DATE"] = "选择日期";
Calendar._TT["DRAG_TO_MOVE"] = "拖动";
Calendar._TT["PART_TODAY"] = " (今日)";
// the following is to inform that "%s" is to be the first day of week
// %s will be replaced with the day name.
-Calendar._TT["DAY_FIRST"] = "Display %s first";
+Calendar._TT["DAY_FIRST"] = "一周开始于 %s";
// This may be locale-dependent. It specifies the week-end days, as an array
// of comma-separated numbers. The numbers are from 0 to 6: 0 means Sunday, 1
@@ -117,11 +117,11 @@ Calendar._TT["WEEKEND"] = "0,6";
Calendar._TT["CLOSE"] = "关闭";
Calendar._TT["TODAY"] = "今天";
-Calendar._TT["TIME_PART"] = "(Shift-)Click or drag to change value";
+Calendar._TT["TIME_PART"] = "Shift加点击或者拖动æ¥å˜æ›´";
// date formats
Calendar._TT["DEF_DATE_FORMAT"] = "%Y-%m-%d";
-Calendar._TT["TT_DATE_FORMAT"] = "%a, %b %e";
+Calendar._TT["TT_DATE_FORMAT"] = "星期%a %b%e日";
-Calendar._TT["WK"] = "wk";
-Calendar._TT["TIME"] = "Time:";
+Calendar._TT["WK"] = "周";
+Calendar._TT["TIME"] = "时间:";
diff --git a/groups/public/javascripts/context_menu.js b/groups/public/javascripts/context_menu.js
index 3e2d571fa..20f0fc5a7 100644
--- a/groups/public/javascripts/context_menu.js
+++ b/groups/public/javascripts/context_menu.js
@@ -28,11 +28,11 @@ ContextMenu.prototype = {
RightClick: function(e) {
this.hideMenu();
// do not show the context menu on links
- if (Event.findElement(e, 'a') != document) { return; }
+ if (Event.element(e).tagName == 'A') { return; }
// right-click simulated by Alt+Click with Opera
if (window.opera && !e.altKey) { return; }
var tr = Event.findElement(e, 'tr');
- if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; }
+ if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; }
Event.stop(e);
if (!this.isSelected(tr)) {
this.unselectAll();
@@ -44,14 +44,14 @@ ContextMenu.prototype = {
Click: function(e) {
this.hideMenu();
- if (Event.findElement(e, 'a') != document) { return; }
+ if (Event.element(e).tagName == 'A') { return; }
if (window.opera && e.altKey) { return; }
if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
var tr = Event.findElement(e, 'tr');
if (tr!=document && tr.hasClassName('hascontextmenu')) {
// a row was clicked, check if the click was on checkbox
var box = Event.findElement(e, 'input');
- if (box!=document) {
+ if (box!=document && box!=undefined) {
// a checkbox may be clicked
if (box.checked) {
tr.addClassName('context-menu-selection');
@@ -90,6 +90,9 @@ ContextMenu.prototype = {
}
}
}
+ else{
+ this.RightClick(e);
+ }
},
showMenu: function(e) {
diff --git a/groups/public/javascripts/controls.js b/groups/public/javascripts/controls.js
index 8c273f874..5aaf0bb2b 100644
--- a/groups/public/javascripts/controls.js
+++ b/groups/public/javascripts/controls.js
@@ -1,6 +1,6 @@
-// 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)
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
@@ -37,22 +37,23 @@
if(typeof Effect == 'undefined')
throw("controls.js requires including script.aculo.us' effects.js library");
-var Autocompleter = {}
-Autocompleter.Base = function() {};
-Autocompleter.Base.prototype = {
+var Autocompleter = { }
+Autocompleter.Base = Class.create({
baseInitialize: function(element, update, options) {
- this.element = $(element);
+ element = $(element)
+ this.element = element;
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
+ this.oldElementValue = this.element.value;
if(this.setOptions)
this.setOptions(options);
else
- this.options = options || {};
+ this.options = options || { };
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
@@ -74,6 +75,9 @@ Autocompleter.Base.prototype = {
if(typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
this.observer = null;
@@ -81,15 +85,14 @@ Autocompleter.Base.prototype = {
Element.hide(this.update);
- Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
- Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', 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) &&
+ (Prototype.Browser.IE) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
@@ -139,17 +142,17 @@ Autocompleter.Base.prototype = {
case Event.KEY_UP:
this.markPrevious();
this.render();
- if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
- if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+ 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;
+ (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
@@ -195,7 +198,6 @@ Autocompleter.Base.prototype = {
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
-
if(this.hasFocus) {
this.show();
this.active = true;
@@ -238,21 +240,22 @@ Autocompleter.Base.prototype = {
}
var value = '';
if (this.options.select) {
- var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+ var nodes = $(selectedElement).select('.' + this.options.select) || [];
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+/);
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
- this.element.value = newValue + value;
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
} else {
this.element.value = value;
}
+ this.oldElementValue = this.element.value;
this.element.focus();
if (this.options.afterUpdateElement)
@@ -296,39 +299,48 @@ Autocompleter.Base.prototype = {
onObserverEvent: function() {
this.changed = false;
+ this.tokenBounds = null;
if(this.getToken().length>=this.options.minChars) {
- this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
+ this.oldElementValue = this.element.value;
},
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<this.options.tokens.length; i++) {
- var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
- if (thisTokenPos > lastTokenPos)
- lastTokenPos = thisTokenPos;
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
}
- return lastTokenPos;
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
}
-}
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
-Ajax.Autocompleter = Class.create();
-Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
@@ -338,7 +350,9 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
},
getUpdatedChoices: function() {
- entry = encodeURIComponent(this.options.paramName) + '=' +
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
@@ -346,14 +360,13 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
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
@@ -391,8 +404,7 @@ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.pro
// 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(), {
+Autocompleter.Local = Class.create(Autocompleter.Base, {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
@@ -448,13 +460,12 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
- }, options || {});
+ }, options || { });
}
});
-// AJAX in-place editor
-//
-// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
// 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
@@ -465,353 +476,472 @@ Field.scrollFreeActivate = function(field) {
}, 1);
}
-Ajax.InPlaceEditor = Class.create();
-Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
-Ajax.InPlaceEditor.prototype = {
+Ajax.InPlaceEditor = Class.create({
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;
- }
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
}
-
- if (this.options.externalControl) {
+ if (this.options.externalControl)
this.options.externalControl = $(this.options.externalControl);
- }
-
- this.originalBackground = Element.getStyle(this.element, 'background-color');
- if (!this.originalBackground) {
- this.originalBackground = "transparent";
- }
-
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || '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);
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
}
},
- 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(/<br/i) || string.match(/<p>/i);
- },
- convertHTMLLineBreaks: function(string) {
- return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/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 text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
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;
+ if (0 < size) fld.size = size;
} 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;
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
}
-
- if(this.options.loadTextURL) {
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
this.loadExternalText();
- }
- this.form.appendChild(this.editField);
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
},
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;
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('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]);
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
}
- return false;
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
},
- onLoading: function() {
- this.saving = true;
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
showSaving: function() {
- this.oldInnerHTML = this.element.innerHTML;
+ 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);
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
},
- removeForm: function() {
- if(this.form) {
- if (this.form.parentNode) Element.remove(this.form);
- this.form = null;
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
}
},
- 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)
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
},
- 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
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw 'Server returned an invalid collection representation.';
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
});
+ new Ajax.Request(this.options.loadCollectionURL, options);
},
- 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);
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
}
- this.editing = false;
- this.saving = false;
- this.oldInnerHTML = null;
- this.onLeaveEditMode();
+ tempOption.update((text || '').stripScripts().stripTags());
},
- onComplete: function(transport) {
- this.leaveEditMode();
- this.options.onComplete.bind(this)(transport, this.element);
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
},
- 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);
- }
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
}
-};
+});
-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;
- }
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
- 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);
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
}
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
}
});
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
// 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 = {
+Form.Element.DelayedObserver = Class.create({
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
@@ -830,4 +960,4 @@ Form.Element.DelayedObserver.prototype = {
this.timer = null;
this.callback(this.element, $F(this.element));
}
-};
+});
diff --git a/groups/public/javascripts/dragdrop.js b/groups/public/javascripts/dragdrop.js
index c71ddb827..bf5cfea66 100644
--- a/groups/public/javascripts/dragdrop.js
+++ b/groups/public/javascripts/dragdrop.js
@@ -1,10 +1,10 @@
-// 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)
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2007 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')
+if(Object.isUndefined(Effect))
throw("dragdrop.js requires including script.aculo.us' effects.js library");
var Droppables = {
@@ -20,14 +20,13 @@ var Droppables = {
greedy: true,
hoverclass: null,
tree: false
- }, arguments[1] || {});
+ }, arguments[1] || { });
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
- if((typeof containment == 'object') &&
- (containment.constructor == Array)) {
+ if(Object.isArray(containment)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
@@ -87,21 +86,23 @@ var Droppables = {
show: function(point, element) {
if(!this.drops.length) return;
- var affected = [];
+ var drop, 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) {
+ if(affected.length>0)
drop = Droppables.findDeepestChild(affected);
+
+ if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
+ if (drop) {
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);
+ if (drop != this.last_active) Droppables.activate(drop);
}
},
@@ -110,8 +111,10 @@ var Droppables = {
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);
+ if (this.last_active.onDrop) {
+ this.last_active.onDrop(element, this.last_active.element, event);
+ return true;
+ }
},
reset: function() {
@@ -219,10 +222,7 @@ var Draggables = {
/*--------------------------------------------------------------------------*/
-var Draggable = Class.create();
-Draggable._dragging = {};
-
-Draggable.prototype = {
+var Draggable = Class.create({
initialize: function(element) {
var defaults = {
handle: false,
@@ -233,7 +233,7 @@ Draggable.prototype = {
});
},
endeffect: function(element) {
- var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+ var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
queue: {scope:'_draggable', position:'end'},
afterFinish: function(){
@@ -243,6 +243,7 @@ Draggable.prototype = {
},
zindex: 1000,
revert: false,
+ quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
@@ -250,7 +251,7 @@ Draggable.prototype = {
delay: 0
};
- if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+ if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
Object.extend(defaults, {
starteffect: function(element) {
element._opacity = Element.getOpacity(element);
@@ -259,11 +260,11 @@ Draggable.prototype = {
}
});
- var options = Object.extend(defaults, arguments[1] || {});
+ var options = Object.extend(defaults, arguments[1] || { });
this.element = $(element);
- if(options.handle && (typeof options.handle == 'string'))
+ if(options.handle && Object.isString(options.handle))
this.handle = this.element.down('.'+options.handle, 0);
if(!this.handle) this.handle = $(options.handle);
@@ -276,7 +277,6 @@ Draggable.prototype = {
Element.makePositioned(this.element); // fix IE
- this.delta = this.currentDelta();
this.options = options;
this.dragging = false;
@@ -298,17 +298,17 @@ Draggable.prototype = {
},
initDrag: function(event) {
- if(typeof Draggable._dragging[this.element] != 'undefined' &&
+ if(!Object.isUndefined(Draggable._dragging[this.element]) &&
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;
+ if((tag_name = src.tagName.toUpperCase()) && (
+ tag_name=='INPUT' ||
+ tag_name=='SELECT' ||
+ tag_name=='OPTION' ||
+ tag_name=='BUTTON' ||
+ tag_name=='TEXTAREA')) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
@@ -321,6 +321,8 @@ Draggable.prototype = {
startDrag: function(event) {
this.dragging = true;
+ if(!this.delta)
+ this.delta = this.currentDelta();
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
@@ -329,7 +331,9 @@ Draggable.prototype = {
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
- Position.absolutize(this.element);
+ this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
+ if (!this.element._originallyAbsolute)
+ Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
@@ -351,8 +355,12 @@ Draggable.prototype = {
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
- Position.prepare();
- Droppables.show(pointer, this.element);
+
+ if(!this.options.quiet){
+ Position.prepare();
+ Droppables.show(pointer, this.element);
+ }
+
Draggables.notify('onDrag', this, event);
this.draw(pointer);
@@ -380,30 +388,44 @@ Draggable.prototype = {
}
// fix AppleWebKit rendering
- if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+ if(Prototype.Browser.WebKit) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
+
+ if(this.options.quiet){
+ Position.prepare();
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ Droppables.show(pointer, this.element);
+ }
if(this.options.ghosting) {
- Position.relativize(this.element);
+ if (!this.element._originallyAbsolute)
+ Position.relativize(this.element);
+ delete this.element._originallyAbsolute;
Element.remove(this._clone);
this._clone = null;
}
- if(success) Droppables.fire(event, this.element);
+ var dropped = false;
+ if(success) {
+ dropped = Droppables.fire(event, this.element);
+ if (!dropped) dropped = false;
+ }
+ if(dropped && this.options.onDropped) this.options.onDropped(this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
- if(revert && typeof revert == 'function') revert = revert(this.element);
+ if(revert && Object.isFunction(revert)) 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]);
+ if (dropped == 0 || revert != 'failure')
+ this.options.reverteffect(this.element,
+ d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
@@ -451,15 +473,15 @@ Draggable.prototype = {
}.bind(this));
if(this.options.snap) {
- if(typeof this.options.snap == 'function') {
+ if(Object.isFunction(this.options.snap)) {
p = this.options.snap(p[0],p[1],this);
} else {
- if(this.options.snap instanceof Array) {
+ if(Object.isArray(this.options.snap)) {
p = p.map( function(v, i) {
- return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+ return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
- return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+ return (v/this.options.snap).round()*this.options.snap }.bind(this))
}
}}
@@ -543,12 +565,13 @@ Draggable.prototype = {
}
return { top: T, left: L, width: W, height: H };
}
-}
+});
+
+Draggable._dragging = { };
/*--------------------------------------------------------------------------*/
-var SortableObserver = Class.create();
-SortableObserver.prototype = {
+var SortableObserver = Class.create({
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
@@ -564,15 +587,15 @@ SortableObserver.prototype = {
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
-}
+});
var Sortable = {
SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
- sortables: {},
+ sortables: { },
_findRootElement: function(element) {
- while (element.tagName != "BODY") {
+ while (element.tagName.toUpperCase() != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
@@ -612,13 +635,20 @@ var Sortable = {
delay: 0,
hoverclass: null,
ghosting: false,
+ quiet: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: this.SERIALIZE_RULE,
+
+ // these take arrays of elements or ids and can be
+ // used for better initialization performance
+ elements: false,
+ handles: false,
+
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
- }, arguments[1] || {});
+ }, arguments[1] || { });
// clear any old sortable with same element
this.destroy(element);
@@ -626,6 +656,7 @@ var Sortable = {
// build options for the draggables
var options_for_draggable = {
revert: true,
+ quiet: options.quiet,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
@@ -679,10 +710,9 @@ var Sortable = {
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.elements || this.findElements(element, options) || []).each( function(e,i) {
+ var handle = options.handles ? $(options.handles[i]) :
+ (options.handle ? $(e).select('.' + options.handle)[0] : e);
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
@@ -842,7 +872,7 @@ var Sortable = {
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
- }, arguments[1] || {});
+ }, arguments[1] || { });
var root = {
id: null,
@@ -866,7 +896,7 @@ var Sortable = {
sequence: function(element) {
element = $(element);
- var options = Object.extend(this.options(element), arguments[1] || {});
+ 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] : '';
@@ -875,9 +905,9 @@ var Sortable = {
setSequence: function(element, new_sequence) {
element = $(element);
- var options = Object.extend(this.options(element), arguments[2] || {});
+ var options = Object.extend(this.options(element), arguments[2] || { });
- var nodeMap = {};
+ 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];
@@ -895,7 +925,7 @@ var Sortable = {
serialize: function(element) {
element = $(element);
- var options = Object.extend(Sortable.options(element), arguments[1] || {});
+ var options = Object.extend(Sortable.options(element), arguments[1] || { });
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
@@ -919,7 +949,7 @@ Element.isParent = function(child, element) {
return Element.isParent(child.parentNode, element);
}
-Element.findChildren = function(element, only, recursive, tagName) {
+Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
diff --git a/groups/public/javascripts/effects.js b/groups/public/javascripts/effects.js
index 3b02eda2b..f030b5dbe 100644
--- a/groups/public/javascripts/effects.js
+++ b/groups/public/javascripts/effects.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
@@ -11,17 +11,17 @@
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
- if(this.slice(0,4) == 'rgb(') {
+ 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();
+ 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));
-}
+ return (color.length==7 ? color : (arguments[0] || this));
+};
/*--------------------------------------------------------------------------*/
@@ -30,7 +30,7 @@ Element.collectTextNodes = function(element) {
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) {
@@ -38,47 +38,18 @@ Element.collectTextNodesIgnoreClass = function(element, className) {
((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);
+ if (Prototype.Browser.WebKit) 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){
+Element.getInlineOpacity = function(element){
return $(element).style.opacity || '';
-}
+};
Element.forceRerendering = function(element) {
try {
@@ -91,31 +62,63 @@ Element.forceRerendering = function(element) {
/*--------------------------------------------------------------------------*/
-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'
},
+ 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) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+ return pos > 1 ? 1 : pos;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+ },
+ pulse: function(pos, pulses) {
+ pulses = pulses || 5;
+ return (
+ ((pos % (1/pulses)) * pulses).round() == 0 ?
+ ((pos * pulses * 2) - (pos * pulses * 2).floor()) :
+ 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
+ );
+ },
+ spring: function(pos) {
+ return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+ },
+ DefaultOptions: {
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ },
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';
+ if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
- if(child.nodeType==3) {
+ if (child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
- Builder.node('span',{style: tagifyStyle},
+ new Element('span', {style: tagifyStyle}).update(
character == ' ' ? String.fromCharCode(160) : character),
child);
});
@@ -125,8 +128,8 @@ var Effect = {
},
multiple: function(element, effect) {
var elements;
- if(((typeof element == 'object') ||
- (typeof element == 'function')) &&
+ if (((typeof element == 'object') ||
+ Object.isFunction(element)) &&
(element.length))
elements = element;
else
@@ -135,7 +138,7 @@ var Effect = {
var options = Object.extend({
speed: 0.1,
delay: 0.0
- }, arguments[2] || {});
+ }, arguments[2] || { });
var masterDelay = options.delay;
$A(elements).each( function(element, index) {
@@ -152,53 +155,20 @@ var Effect = {
effect = (effect || 'appear').toLowerCase();
var options = Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
- }, arguments[2] || {});
+ }, 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;
- }
-};
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
/* ------------- core effects ------------- */
-Effect.ScopedQueue = Class.create();
-Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+Effect.ScopedQueue = Class.create(Enumerable, {
initialize: function() {
this.effects = [];
- this.interval = null;
+ this.interval = null;
},
_each: function(iterator) {
this.effects._each(iterator);
@@ -206,7 +176,7 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
add: function(effect) {
var timestamp = new Date().getTime();
- var position = (typeof effect.options.queue == 'string') ?
+ var position = Object.isString(effect.options.queue) ?
effect.options.queue : effect.options.queue.position;
switch(position) {
@@ -229,115 +199,111 @@ Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
effect.startOn += timestamp;
effect.finishOn += timestamp;
- if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
+ 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);
+ if (!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
- if(this.effects.length == 0) {
+ if (this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
- this.effects.invoke('loop', timePos);
+ for(var i=0, len=this.effects.length;i<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
}
});
Effect.Queues = {
instances: $H(),
get: function(queueName) {
- if(typeof queueName != 'string') return queueName;
+ if (!Object.isString(queueName)) return queueName;
- if(!this.instances[queueName])
- this.instances[queueName] = new Effect.ScopedQueue();
-
- return this.instances[queueName];
+ return this.instances.get(queueName) ||
+ this.instances.set(queueName, new Effect.ScopedQueue());
}
-}
+};
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 = {
+Effect.Base = Class.create({
position: null,
start: function(options) {
- this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
+ function codeForEvent(options,eventName){
+ return (
+ (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
+ (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
+ );
+ }
+ if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+ 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.finishOn = this.startOn+(this.options.duration*1000);
+ this.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ eval('this.render = function(pos){ '+
+ 'if (this.state=="idle"){this.state="running";'+
+ codeForEvent(this.options,'beforeSetup')+
+ (this.setup ? 'this.setup();':'')+
+ codeForEvent(this.options,'afterSetup')+
+ '};if (this.state=="running"){'+
+ 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
+ 'this.position=pos;'+
+ codeForEvent(this.options,'beforeUpdate')+
+ (this.update ? 'this.update(pos);':'')+
+ codeForEvent(this.options,'afterUpdate')+
+ '}}');
+
this.event('beforeStart');
- if(!this.options.sync)
- Effect.Queues.get(typeof this.options.queue == 'string' ?
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
'global' : this.options.queue.scope).add(this);
},
loop: function(timePos) {
- if(timePos >= this.startOn) {
- if(timePos >= this.finishOn) {
+ if (timePos >= this.startOn) {
+ if (timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
- if(this.finish) this.finish();
+ 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) {
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = (pos * this.totalFrames).round();
+ 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' ?
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
'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);
+ if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+ if (this.options[eventName]) this.options[eventName](this);
},
inspect: function() {
- return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
+ var data = $H();
+ for(property in this)
+ if (!Object.isFunction(this[property])) data.set(property, this[property]);
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
}
-}
+});
-Effect.Parallel = Class.create();
-Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+Effect.Parallel = Class.create(Effect.Base, {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
@@ -350,35 +316,45 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
- if(effect.finish) effect.finish(position);
+ if (effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
-Effect.Event = Class.create();
-Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+Effect.Tween = Class.create(Effect.Base, {
+ initialize: function(object, from, to) {
+ object = Object.isString(object) ? $(object) : object;
+ var args = $A(arguments), method = args.last(),
+ options = args.length == 5 ? args[3] : null;
+ this.method = Object.isFunction(method) ? method.bind(object) :
+ Object.isFunction(object[method]) ? object[method].bind(object) :
+ function(value) { object[method] = value };
+ this.start(Object.extend({ from: from, to: to }, options || { }));
+ },
+ update: function(position) {
+ this.method(position);
+ }
+});
+
+Effect.Event = Class.create(Effect.Base, {
initialize: function() {
- var options = Object.extend({
- duration: 0
- }, arguments[0] || {});
- this.start(options);
+ this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
},
update: Prototype.emptyFunction
});
-Effect.Opacity = Class.create();
-Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+Effect.Opacity = Class.create(Effect.Base, {
initialize: function(element) {
this.element = $(element);
- if(!this.element) throw(Effect._elementDoesNotExistError);
+ 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))
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
this.element.setStyle({zoom: 1});
var options = Object.extend({
from: this.element.getOpacity() || 0.0,
to: 1.0
- }, arguments[1] || {});
+ }, arguments[1] || { });
this.start(options);
},
update: function(position) {
@@ -386,36 +362,30 @@ Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
}
});
-Effect.Move = Class.create();
-Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+Effect.Move = Class.create(Effect.Base, {
initialize: function(element) {
this.element = $(element);
- if(!this.element) throw(Effect._elementDoesNotExistError);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({
x: 0,
y: 0,
mode: 'relative'
- }, arguments[1] || {});
+ }, 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
+ if (this.options.mode == 'absolute') {
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'
+ left: (this.options.x * position + this.originalLeft).round() + 'px',
+ top: (this.options.y * position + this.originalTop).round() + 'px'
});
}
});
@@ -423,30 +393,29 @@ Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
return new Effect.Move(element,
- Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+ Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};
-Effect.Scale = Class.create();
-Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+Effect.Scale = Class.create(Effect.Base, {
initialize: function(element, percent) {
this.element = $(element);
- if(!this.element) throw(Effect._elementDoesNotExistError);
+ 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
+ scaleMode: 'box', // 'box' or 'contents' or { } with provided values
scaleFrom: 100.0,
scaleTo: percent
- }, arguments[2] || {});
+ }, arguments[2] || { });
this.start(options);
},
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = this.element.getStyle('position');
- this.originalStyle = {};
+ this.originalStyle = { };
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
@@ -456,7 +425,7 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%','pt'].each( function(fontSizeType) {
- if(fontSize.indexOf(fontSizeType)>0) {
+ if (fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType;
}
@@ -465,60 +434,61 @@ Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
- if(this.options.scaleMode=='box')
+ if (this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
- if(/^content/.test(this.options.scaleMode))
+ if (/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
- if(!this.dims)
+ 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)
+ 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);
+ 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 d = { };
+ if (this.options.scaleX) d.width = width.round() + 'px';
+ if (this.options.scaleY) d.height = height.round() + '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';
+ 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';
+ 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), {
+Effect.Highlight = Class.create(Effect.Base, {
initialize: function(element) {
this.element = $(element);
- if(!this.element) throw(Effect._elementDoesNotExistError);
- var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
+ 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; }
+ 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.oldStyle = { };
+ if (!this.options.keepBackgroundImage) {
+ 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)
+ 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));
@@ -526,7 +496,7 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype),
},
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)) });
+ return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
},
finish: function() {
this.element.setStyle(Object.extend(this.oldStyle, {
@@ -535,30 +505,21 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype),
}
});
-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));
- }
-});
+Effect.ScrollTo = function(element) {
+ var options = arguments[1] || { },
+ scrollOffsets = document.viewport.getScrollOffsets(),
+ elementOffsets = $(element).cumulativeOffset(),
+ max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();
+
+ if (options.offset) elementOffsets[1] += options.offset;
+
+ return new Effect.Tween(null,
+ scrollOffsets.top,
+ elementOffsets[1] > max ? max : elementOffsets[1],
+ options,
+ function(p){ scrollTo(scrollOffsets.left, p.round()) }
+ );
+};
/* ------------- combination effects ------------- */
@@ -566,14 +527,15 @@ 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] || {});
+ 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);
@@ -586,9 +548,9 @@ Effect.Appear = function(element) {
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from).show();
- }}, arguments[1] || {});
+ }}, arguments[1] || { });
return new Effect.Opacity(element,options);
-}
+};
Effect.Puff = function(element) {
element = $(element);
@@ -610,9 +572,9 @@ Effect.Puff = function(element) {
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide().setStyle(oldStyle); }
- }, arguments[1] || {})
+ }, arguments[1] || { })
);
-}
+};
Effect.BlindUp = function(element) {
element = $(element);
@@ -624,9 +586,9 @@ Effect.BlindUp = function(element) {
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping();
}
- }, arguments[1] || {})
+ }, arguments[1] || { })
);
-}
+};
Effect.BlindDown = function(element) {
element = $(element);
@@ -643,8 +605,8 @@ Effect.BlindDown = function(element) {
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
- }, arguments[1] || {}));
-}
+ }, arguments[1] || { }));
+};
Effect.SwitchOff = function(element) {
element = $(element);
@@ -665,8 +627,8 @@ Effect.SwitchOff = function(element) {
}
})
}
- }, arguments[1] || {}));
-}
+ }, arguments[1] || { }));
+};
Effect.DropOut = function(element) {
element = $(element);
@@ -685,29 +647,35 @@ Effect.DropOut = function(element) {
afterFinishInternal: function(effect) {
effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
}
- }, arguments[1] || {}));
-}
+ }, arguments[1] || { }));
+};
Effect.Shake = function(element) {
element = $(element);
+ var options = Object.extend({
+ distance: 20,
+ duration: 0.5
+ }, arguments[1] || {});
+ var distance = parseFloat(options.distance);
+ var split = parseFloat(options.duration) / 10.0;
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) {
+ return new Effect.Move(element,
+ { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
- { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
- { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
- { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
- { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
- { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+ { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
effect.element.undoPositioned().setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
-}
+};
Effect.SlideDown = function(element) {
element = $(element).cleanWhitespace();
@@ -723,7 +691,7 @@ Effect.SlideDown = function(element) {
afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.down().makePositioned();
- if(window.opera) effect.element.setStyle({top: ''});
+ if (window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping().setStyle({height: '0px'}).show();
},
afterUpdateInternal: function(effect) {
@@ -733,23 +701,25 @@ Effect.SlideDown = function(element) {
afterFinishInternal: function(effect) {
effect.element.undoClipping().undoPositioned();
effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
- }, arguments[1] || {})
+ }, arguments[1] || { })
);
-}
+};
Effect.SlideUp = function(element) {
element = $(element).cleanWhitespace();
var oldInnerBottom = element.down().getStyle('bottom');
+ var elementDimensions = element.getDimensions();
return new Effect.Scale(element, window.opera ? 0 : 1,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
+ scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
- beforeStartInternal: function(effect) {
+ afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.down().makePositioned();
- if(window.opera) effect.element.setStyle({top: ''});
+ if (window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping().show();
},
afterUpdateInternal: function(effect) {
@@ -757,12 +727,12 @@ Effect.SlideUp = function(element) {
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
- effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
- effect.element.down().undoPositioned();
+ effect.element.hide().undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
}
- }, arguments[1] || {})
+ }, arguments[1] || { })
);
-}
+};
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
@@ -775,7 +745,7 @@ Effect.Squish = function(element) {
effect.element.hide().undoClipping();
}
});
-}
+};
Effect.Grow = function(element) {
element = $(element);
@@ -784,7 +754,7 @@ Effect.Grow = function(element) {
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.full
- }, arguments[1] || {});
+ }, arguments[1] || { });
var oldStyle = {
top: element.style.top,
left: element.style.left,
@@ -849,7 +819,7 @@ Effect.Grow = function(element) {
)
}
});
-}
+};
Effect.Shrink = function(element) {
element = $(element);
@@ -858,7 +828,7 @@ Effect.Shrink = function(element) {
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.none
- }, arguments[1] || {});
+ }, arguments[1] || { });
var oldStyle = {
top: element.style.top,
left: element.style.left,
@@ -903,11 +873,11 @@ Effect.Shrink = function(element) {
effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
}, options)
);
-}
+};
Effect.Pulsate = function(element) {
element = $(element);
- var options = arguments[1] || {};
+ 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)) };
@@ -916,7 +886,7 @@ Effect.Pulsate = function(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);
@@ -936,37 +906,71 @@ Effect.Fold = function(element) {
afterFinishInternal: function(effect) {
effect.element.hide().undoClipping().setStyle(oldStyle);
} });
- }}, arguments[1] || {}));
+ }}, arguments[1] || { }));
};
-Effect.Morph = Class.create();
-Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+Effect.Morph = Class.create(Effect.Base, {
initialize: function(element) {
this.element = $(element);
- if(!this.element) throw(Effect._elementDoesNotExistError);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
var options = Object.extend({
- style: ''
- }, arguments[1] || {});
+ style: { }
+ }, arguments[1] || { });
+
+ if (!Object.isString(options.style)) this.style = $H(options.style);
+ else {
+ if (options.style.include(':'))
+ this.style = options.style.parseStyle();
+ else {
+ this.element.addClassName(options.style);
+ this.style = $H(this.element.getStyles());
+ this.element.removeClassName(options.style);
+ var css = this.element.getStyles();
+ this.style = this.style.reject(function(style) {
+ return style.value == css[style.key];
+ });
+ options.afterFinishInternal = function(effect) {
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ effect.element.style[transform.style] = '';
+ });
+ }
+ }
+ }
this.start(options);
},
+
setup: function(){
function parseColor(color){
- if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+ 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
- });
+ this.transforms = this.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if (property == 'opacity') {
+ value = parseFloat(value);
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if (Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
}.bind(this)).reject(function(transform){
return (
(transform.originalValue == transform.targetValue) ||
@@ -978,32 +982,35 @@ Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
});
},
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);
+ var style = { }, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ (transform.originalValue +
+ (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+ (transform.unit === null ? '' : transform.unit);
+ this.element.setStyle(style, true);
}
});
-Effect.Transform = Class.create();
-Object.extend(Effect.Transform.prototype, {
+Effect.Transform = Class.create({
initialize: function(tracks){
this.tracks = [];
- this.options = arguments[1] || {};
+ this.options = arguments[1] || { };
this.addTracks(tracks);
},
addTracks: function(tracks){
tracks.each(function(track){
- var data = $H(track).values().first();
+ track = $H(track);
+ var data = track.values().first();
this.tracks.push($H({
- ids: $H(track).keys().first(),
+ ids: track.keys().first(),
effect: Effect.Morph,
options: { style: data }
}));
@@ -1013,76 +1020,101 @@ Object.extend(Effect.Transform.prototype, {
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)) });
+ var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+ var elements = [$(ids) || $$(ids)].flatten();
+ return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, 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_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
- var element = Element.extend(document.createElement('div'));
- element.innerHTML = '<div style="' + this + '"></div>';
- var style = element.down().style, styleRules = $H();
+ var style, styleRules = $H();
+ if (Prototype.Browser.WebKit)
+ style = new Element('div',{style:this}).style;
+ else {
+ String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
+ style = String.__parseStyleElement.childNodes[0].style;
+ }
Element.CSS_PROPERTIES.each(function(property){
- if(style[property]) styleRules[property] = style[property];
+ if (style[property]) styleRules.set(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;
+ if (Prototype.Browser.IE && this.include('opacity'))
+ styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+ return styleRules;
};
-Element.morph = function(element, style) {
- new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
- return element;
+if (document.defaultView && document.defaultView.getComputedStyle) {
+ Element.getStyles = function(element) {
+ var css = document.defaultView.getComputedStyle($(element), null);
+ return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+ styles[property] = css[property];
+ return styles;
+ });
+ };
+} else {
+ Element.getStyles = function(element) {
+ element = $(element);
+ var css = element.currentStyle, styles;
+ styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
+ hash.set(property, css[property]);
+ return hash;
+ });
+ if (!styles.opacity) styles.set('opacity', element.getOpacity());
+ return styles;
+ };
};
-['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
- 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
- function(f) { Element.Methods[f] = Element[f]; }
+Effect.Methods = {
+ morph: function(element, style) {
+ element = $(element);
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+ return element;
+ },
+ visualEffect: function(element, effect, options) {
+ element = $(element)
+ var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[klass](element, options);
+ return element;
+ },
+ highlight: function(element, options) {
+ element = $(element);
+ new Effect.Highlight(element, options);
+ return element;
+ }
+};
+
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+ 'pulsate shake puff squish switchOff dropOut').each(
+ function(effect) {
+ Effect.Methods[effect] = function(element, options){
+ element = $(element);
+ Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+ return element;
+ }
+ }
);
-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);
-};
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+ function(f) { Effect.Methods[f] = Element[f]; }
+);
-Element.addMethods(); \ No newline at end of file
+Element.addMethods(Effect.Methods);
diff --git a/groups/public/javascripts/jstoolbar/jstoolbar.js b/groups/public/javascripts/jstoolbar/jstoolbar.js
index be982d4b9..64c460217 100644
--- a/groups/public/javascripts/jstoolbar/jstoolbar.js
+++ b/groups/public/javascripts/jstoolbar/jstoolbar.js
@@ -498,6 +498,37 @@ jsToolBar.prototype.elements.ol = {
}
}
+// spacer
+jsToolBar.prototype.elements.space3 = {type: 'space'}
+
+// bq
+jsToolBar.prototype.elements.bq = {
+ type: 'button',
+ title: 'Quote',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
+ });
+ }
+ }
+}
+
+// unbq
+jsToolBar.prototype.elements.unbq = {
+ type: 'button',
+ title: 'Unquote',
+ fn: {
+ wiki: function() {
+ this.encloseLineSelection('','',function(str) {
+ str = str.replace(/\r/g,'');
+ return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
+ });
+ }
+ }
+}
+
// pre
jsToolBar.prototype.elements.pre = {
type: 'button',
@@ -508,7 +539,7 @@ jsToolBar.prototype.elements.pre = {
}
// spacer
-jsToolBar.prototype.elements.space3 = {type: 'space'}
+jsToolBar.prototype.elements.space4 = {type: 'space'}
// wiki page
jsToolBar.prototype.elements.link = {
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-bg.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js
index 8a59a8162..f2c0dbff5 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-cs.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Záhlaví 2';
jsToolBar.strings['Heading 3'] = 'Záhlaví 3';
jsToolBar.strings['Unordered list'] = 'Seznam';
jsToolBar.strings['Ordered list'] = 'Uspořádaný seznam';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Předformátovaný text';
jsToolBar.strings['Wiki link'] = 'Vložit odkaz na Wiki stránku';
jsToolBar.strings['Image'] = 'Vložit obrázek';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js
index 9996acaf3..6ccc8ead2 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-da.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Overskrift 2';
jsToolBar.strings['Heading 3'] = 'Overskrift 3';
jsToolBar.strings['Unordered list'] = 'Unummereret list';
jsToolBar.strings['Ordered list'] = 'Nummereret list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatteret tekst';
jsToolBar.strings['Wiki link'] = 'Link til en Wiki side';
jsToolBar.strings['Image'] = 'Billede';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js
index e2ba3fc1c..ce686860f 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-de.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Überschrift 2. Ordnung';
jsToolBar.strings['Heading 3'] = 'Überschrift 3. Ordnung';
jsToolBar.strings['Unordered list'] = 'Aufzählungsliste';
jsToolBar.strings['Ordered list'] = 'Nummerierte Liste';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Präformatierter Text';
jsToolBar.strings['Wiki link'] = 'Verweis (Link) zu einer Wiki-Seite';
jsToolBar.strings['Image'] = 'Grafik';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-en.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-es.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js
index 357d25951..c2229b281 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fi.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Otsikko 2';
jsToolBar.strings['Heading 3'] = 'Otsikko 3';
jsToolBar.strings['Unordered list'] = 'Järjestämätön lista';
jsToolBar.strings['Ordered list'] = 'Järjestetty lista';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Ennaltamuotoiltu teksti';
jsToolBar.strings['Wiki link'] = 'Linkki Wiki sivulle';
jsToolBar.strings['Image'] = 'Kuva';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js
index 3cbc67863..c52a783bc 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-fr.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Titre niveau 2';
jsToolBar.strings['Heading 3'] = 'Titre niveau 3';
jsToolBar.strings['Unordered list'] = 'Liste à puces';
jsToolBar.strings['Ordered list'] = 'Liste numérotée';
+jsToolBar.strings['Quote'] = 'Citer';
+jsToolBar.strings['Unquote'] = 'Supprimer citation';
jsToolBar.strings['Preformatted text'] = 'Texte préformaté';
jsToolBar.strings['Wiki link'] = 'Lien vers une page Wiki';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-he.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js
new file mode 100644
index 000000000..c31ba00c0
--- /dev/null
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-hu.js
@@ -0,0 +1,16 @@
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'Félkövér';
+jsToolBar.strings['Italic'] = 'Dőlt';
+jsToolBar.strings['Underline'] = 'Aláhúzott';
+jsToolBar.strings['Deleted'] = 'Törölt';
+jsToolBar.strings['Code'] = 'Kód sorok';
+jsToolBar.strings['Heading 1'] = 'Fejléc 1';
+jsToolBar.strings['Heading 2'] = 'Fejléc 2';
+jsToolBar.strings['Heading 3'] = 'Fejléc 3';
+jsToolBar.strings['Unordered list'] = 'Felsorolás';
+jsToolBar.strings['Ordered list'] = 'Számozott lista';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Előreformázott szöveg';
+jsToolBar.strings['Wiki link'] = 'Link egy Wiki oldalra';
+jsToolBar.strings['Image'] = 'Kép';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-it.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js
index fc4d987de..c9413dac2 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ja.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = '見出㗠2';
jsToolBar.strings['Heading 3'] = '見出㗠3';
jsToolBar.strings['Unordered list'] = 'é †ä¸åŒãƒªã‚¹ãƒˆ';
jsToolBar.strings['Ordered list'] = '番å·ã¤ãリスト';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = '整形済ã¿ãƒ†ã‚­ã‚¹ãƒˆ';
jsToolBar.strings['Wiki link'] = 'Wiki ページã¸ã®ãƒªãƒ³ã‚¯';
jsToolBar.strings['Image'] = 'ç”»åƒ';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ko.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js
index f0a7c5d90..8af364c8d 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-lt.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Nenumeruotas sąrašas';
jsToolBar.strings['Ordered list'] = 'Numeruotas sąrašas';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatuotas tekstas';
jsToolBar.strings['Wiki link'] = 'Nuoroda į Wiki puslapį';
jsToolBar.strings['Image'] = 'Paveikslas';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-nl.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js
index cf6e19ff9..799597343 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-no.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Overskrift 2';
jsToolBar.strings['Heading 3'] = 'Overskrift 3';
jsToolBar.strings['Unordered list'] = 'Punktliste';
jsToolBar.strings['Ordered list'] = 'Nummerert liste';
+jsToolBar.strings['Quote'] = 'Sitat';
+jsToolBar.strings['Unquote'] = 'Avslutt sitat';
jsToolBar.strings['Preformatted text'] = 'Preformatert tekst';
jsToolBar.strings['Wiki link'] = 'Lenke til Wiki-side';
jsToolBar.strings['Image'] = 'Bilde';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pl.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js
index cd36a4b55..5035524ab 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt-br.js
@@ -1,14 +1,18 @@
+// Translated by: Alexandre da Silva <simpsomboy@gmail.com>
+
jsToolBar.strings = {};
-jsToolBar.strings['Strong'] = 'Strong';
-jsToolBar.strings['Italic'] = 'Italic';
-jsToolBar.strings['Underline'] = 'Underline';
-jsToolBar.strings['Deleted'] = 'Deleted';
-jsToolBar.strings['Code'] = 'Inline Code';
-jsToolBar.strings['Heading 1'] = 'Heading 1';
-jsToolBar.strings['Heading 2'] = 'Heading 2';
-jsToolBar.strings['Heading 3'] = 'Heading 3';
-jsToolBar.strings['Unordered list'] = 'Unordered list';
-jsToolBar.strings['Ordered list'] = 'Ordered list';
-jsToolBar.strings['Preformatted text'] = 'Preformatted text';
-jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
-jsToolBar.strings['Image'] = 'Image';
+jsToolBar.strings['Strong'] = 'Negrito';
+jsToolBar.strings['Italic'] = 'Itálico';
+jsToolBar.strings['Underline'] = 'Sublinhado';
+jsToolBar.strings['Deleted'] = 'Excluído';
+jsToolBar.strings['Code'] = 'Código Inline';
+jsToolBar.strings['Heading 1'] = 'Cabeçalho 1';
+jsToolBar.strings['Heading 2'] = 'Cabeçalho 2';
+jsToolBar.strings['Heading 3'] = 'Cabeçalho 3';
+jsToolBar.strings['Unordered list'] = 'Lista não ordenada';
+jsToolBar.strings['Ordered list'] = 'Lista ordenada';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'Texto pré-formatado';
+jsToolBar.strings['Wiki link'] = 'Link para uma página Wiki';
+jsToolBar.strings['Image'] = 'Imagem';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-pt.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ro.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js
index 6370a3e2d..a6d8c4fad 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-ru.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Заголовок 2';
jsToolBar.strings['Heading 3'] = 'Заголовок 3';
jsToolBar.strings['Unordered list'] = 'Маркированный ÑпиÑок';
jsToolBar.strings['Ordered list'] = 'Ðумерованный ÑпиÑок';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Заранее форматированный текÑÑ‚';
jsToolBar.strings['Wiki link'] = 'СÑылка на Ñтраницу в Wiki';
jsToolBar.strings['Image'] = 'Ð’Ñтавка изображениÑ';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sr.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-sv.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js
new file mode 100644
index 000000000..d87164226
--- /dev/null
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-th.js
@@ -0,0 +1,16 @@
+jsToolBar.strings = {};
+jsToolBar.strings['Strong'] = 'หนา';
+jsToolBar.strings['Italic'] = 'เอียง';
+jsToolBar.strings['Underline'] = 'ขีดเส้นใต้';
+jsToolBar.strings['Deleted'] = 'ขีดฆ่า';
+jsToolBar.strings['Code'] = 'โค๊ดโปรà¹à¸à¸£à¸¡';
+jsToolBar.strings['Heading 1'] = 'หัวข้อ 1';
+jsToolBar.strings['Heading 2'] = 'หัวข้อ 2';
+jsToolBar.strings['Heading 3'] = 'หัวข้อ 3';
+jsToolBar.strings['Unordered list'] = 'รายà¸à¸²à¸£';
+jsToolBar.strings['Ordered list'] = 'ลำดับเลข';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
+jsToolBar.strings['Preformatted text'] = 'รูปà¹à¸šà¸šà¸‚้อความคงที่';
+jsToolBar.strings['Wiki link'] = 'เชื่อมโยงไปหน้า Wiki อื่น';
+jsToolBar.strings['Image'] = 'รูปภาพ';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js
index cd36a4b55..2d68498f9 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-uk.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = 'Heading 2';
jsToolBar.strings['Heading 3'] = 'Heading 3';
jsToolBar.strings['Unordered list'] = 'Unordered list';
jsToolBar.strings['Ordered list'] = 'Ordered list';
+jsToolBar.strings['Quote'] = 'Quote';
+jsToolBar.strings['Unquote'] = 'Remove Quote';
jsToolBar.strings['Preformatted text'] = 'Preformatted text';
jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
jsToolBar.strings['Image'] = 'Image';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js
index 1e46e2470..86599c5a2 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh-tw.js
@@ -9,6 +9,8 @@ jsToolBar.strings['Heading 2'] = '標題 2';
jsToolBar.strings['Heading 3'] = '標題 3';
jsToolBar.strings['Unordered list'] = '項目清單';
jsToolBar.strings['Ordered list'] = '編號清單';
-jsToolBar.strings['Preformatted text'] = 'æ ¼å¼åŒ–文字';
+jsToolBar.strings['Quote'] = '引文';
+jsToolBar.strings['Unquote'] = 'å–æ¶ˆå¼•æ–‡';
+jsToolBar.strings['Preformatted text'] = 'å·²æ ¼å¼æ–‡å­—';
jsToolBar.strings['Wiki link'] = '連çµè‡³ Wiki é é¢';
jsToolBar.strings['Image'] = '圖片';
diff --git a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js
index cd36a4b55..a9b6ba230 100644
--- a/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js
+++ b/groups/public/javascripts/jstoolbar/lang/jstoolbar-zh.js
@@ -1,14 +1,16 @@
jsToolBar.strings = {};
-jsToolBar.strings['Strong'] = 'Strong';
-jsToolBar.strings['Italic'] = 'Italic';
-jsToolBar.strings['Underline'] = 'Underline';
-jsToolBar.strings['Deleted'] = 'Deleted';
-jsToolBar.strings['Code'] = 'Inline Code';
-jsToolBar.strings['Heading 1'] = 'Heading 1';
-jsToolBar.strings['Heading 2'] = 'Heading 2';
-jsToolBar.strings['Heading 3'] = 'Heading 3';
-jsToolBar.strings['Unordered list'] = 'Unordered list';
-jsToolBar.strings['Ordered list'] = 'Ordered list';
-jsToolBar.strings['Preformatted text'] = 'Preformatted text';
-jsToolBar.strings['Wiki link'] = 'Link to a Wiki page';
-jsToolBar.strings['Image'] = 'Image';
+jsToolBar.strings['Strong'] = '粗体';
+jsToolBar.strings['Italic'] = '斜体';
+jsToolBar.strings['Underline'] = '下划线';
+jsToolBar.strings['Deleted'] = '删除线';
+jsToolBar.strings['Code'] = '程åºä»£ç ';
+jsToolBar.strings['Heading 1'] = '标题 1';
+jsToolBar.strings['Heading 2'] = '标题 2';
+jsToolBar.strings['Heading 3'] = '标题 3';
+jsToolBar.strings['Unordered list'] = 'æ— åºåˆ—表';
+jsToolBar.strings['Ordered list'] = '排åºåˆ—表';
+jsToolBar.strings['Quote'] = '引用';
+jsToolBar.strings['Unquote'] = '删除引用';
+jsToolBar.strings['Preformatted text'] = 'æ ¼å¼åŒ–文本';
+jsToolBar.strings['Wiki link'] = '连接到 Wiki 页é¢';
+jsToolBar.strings['Image'] = '图片';
diff --git a/groups/public/javascripts/prototype.js b/groups/public/javascripts/prototype.js
index 2735d10dc..546f9fe44 100644
--- a/groups/public/javascripts/prototype.js
+++ b/groups/public/javascripts/prototype.js
@@ -1,43 +1,114 @@
-/* Prototype JavaScript framework, version 1.5.0
+/* Prototype JavaScript framework, version 1.6.0.1
* (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/
+ * For details, see the Prototype web site: http://www.prototypejs.org/
*
-/*--------------------------------------------------------------------------*/
+ *--------------------------------------------------------------------------*/
var Prototype = {
- Version: '1.5.0',
+ Version: '1.6.0.1',
+
+ Browser: {
+ IE: !!(window.attachEvent && !window.opera),
+ Opera: !!window.opera,
+ WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+ Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+ MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
+ },
+
BrowserFeatures: {
- XPath: !!document.evaluate
+ XPath: !!document.evaluate,
+ ElementExtensions: !!window.HTMLElement,
+ SpecificElementExtensions:
+ document.createElement('div').__proto__ &&
+ document.createElement('div').__proto__ !==
+ document.createElement('form').__proto__
},
- ScriptFragment: '(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)',
- emptyFunction: function() {},
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
K: function(x) { return x }
-}
+};
+
+if (Prototype.Browser.MobileSafari)
+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+/* Based on Alex Arnell's inheritance implementation. */
var Class = {
create: function() {
- return function() {
+ var parent = null, properties = $A(arguments);
+ if (Object.isFunction(properties[0]))
+ parent = properties.shift();
+
+ function klass() {
this.initialize.apply(this, arguments);
}
+
+ Object.extend(klass, Class.Methods);
+ klass.superclass = parent;
+ klass.subclasses = [];
+
+ if (parent) {
+ var subclass = function() { };
+ subclass.prototype = parent.prototype;
+ klass.prototype = new subclass;
+ parent.subclasses.push(klass);
+ }
+
+ for (var i = 0; i < properties.length; i++)
+ klass.addMethods(properties[i]);
+
+ if (!klass.prototype.initialize)
+ klass.prototype.initialize = Prototype.emptyFunction;
+
+ klass.prototype.constructor = klass;
+
+ return klass;
+ }
+};
+
+Class.Methods = {
+ addMethods: function(source) {
+ var ancestor = this.superclass && this.superclass.prototype;
+ var properties = Object.keys(source);
+
+ if (!Object.keys({ toString: true }).length)
+ properties.push("toString", "valueOf");
+
+ for (var i = 0, length = properties.length; i < length; i++) {
+ var property = properties[i], value = source[property];
+ if (ancestor && Object.isFunction(value) &&
+ value.argumentNames().first() == "$super") {
+ var method = value, value = Object.extend((function(m) {
+ return function() { return ancestor[m].apply(this, arguments) };
+ })(property).wrap(method), {
+ valueOf: function() { return method },
+ toString: function() { return method.toString() }
+ });
+ }
+ this.prototype[property] = value;
+ }
+
+ return this;
}
-}
+};
-var Abstract = new Object();
+var Abstract = { };
Object.extend = function(destination, source) {
- for (var property in 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.isUndefined(object)) return 'undefined';
if (object === null) return 'null';
return object.inspect ? object.inspect() : object.toString();
} catch (e) {
@@ -46,6 +117,37 @@ Object.extend(Object, {
}
},
+ toJSON: function(object) {
+ var type = typeof object;
+ switch (type) {
+ case 'undefined':
+ case 'function':
+ case 'unknown': return;
+ case 'boolean': return object.toString();
+ }
+
+ if (object === null) return 'null';
+ if (object.toJSON) return object.toJSON();
+ if (Object.isElement(object)) return;
+
+ var results = [];
+ for (var property in object) {
+ var value = Object.toJSON(object[property]);
+ if (!Object.isUndefined(value))
+ results.push(property.toJSON() + ': ' + value);
+ }
+
+ return '{' + results.join(', ') + '}';
+ },
+
+ toQueryString: function(object) {
+ return $H(object).toQueryString();
+ },
+
+ toHTML: function(object) {
+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
+ },
+
keys: function(object) {
var keys = [];
for (var property in object)
@@ -61,41 +163,101 @@ Object.extend(Object, {
},
clone: function(object) {
- return Object.extend({}, object);
+ return Object.extend({ }, object);
+ },
+
+ isElement: function(object) {
+ return object && object.nodeType == 1;
+ },
+
+ isArray: function(object) {
+ return object && object.constructor === Array;
+ },
+
+ isHash: function(object) {
+ return object instanceof Hash;
+ },
+
+ isFunction: function(object) {
+ return typeof object == "function";
+ },
+
+ isString: function(object) {
+ return typeof object == "string";
+ },
+
+ isNumber: function(object) {
+ return typeof object == "number";
+ },
+
+ isUndefined: function(object) {
+ return typeof object == "undefined";
}
});
-Function.prototype.bind = function() {
- var __method = this, args = $A(arguments), object = args.shift();
- return function() {
- return __method.apply(object, args.concat($A(arguments)));
- }
-}
+Object.extend(Function.prototype, {
+ argumentNames: function() {
+ var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");
+ return names.length == 1 && !names[0] ? [] : names;
+ },
-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)));
- }
-}
+ bind: function() {
+ if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function() {
+ return __method.apply(object, args.concat($A(arguments)));
+ }
+ },
-Object.extend(Number.prototype, {
- toColorPart: function() {
- var digits = this.toString(16);
- if (this < 16) return '0' + digits;
- return digits;
+ bindAsEventListener: function() {
+ var __method = this, args = $A(arguments), object = args.shift();
+ return function(event) {
+ return __method.apply(object, [event || window.event].concat(args));
+ }
},
- succ: function() {
- return this + 1;
+ curry: function() {
+ if (!arguments.length) return this;
+ var __method = this, args = $A(arguments);
+ return function() {
+ return __method.apply(this, args.concat($A(arguments)));
+ }
},
- times: function(iterator) {
- $R(0, this, true).each(iterator);
- return this;
+ delay: function() {
+ var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
+ return window.setTimeout(function() {
+ return __method.apply(__method, args);
+ }, timeout);
+ },
+
+ wrap: function(wrapper) {
+ var __method = this;
+ return function() {
+ return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));
+ }
+ },
+
+ methodize: function() {
+ if (this._methodized) return this._methodized;
+ var __method = this;
+ return this._methodized = function() {
+ return __method.apply(null, [this].concat($A(arguments)));
+ };
}
});
+Function.prototype.defer = Function.prototype.delay.curry(0.01);
+
+Date.prototype.toJSON = function() {
+ return '"' + this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z"';
+};
+
var Try = {
these: function() {
var returnValue;
@@ -105,17 +267,22 @@ var Try = {
try {
returnValue = lambda();
break;
- } catch (e) {}
+ } catch (e) { }
}
return returnValue;
}
-}
+};
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
/*--------------------------------------------------------------------------*/
-var PeriodicalExecuter = Class.create();
-PeriodicalExecuter.prototype = {
+var PeriodicalExecuter = Class.create({
initialize: function(callback, frequency) {
this.callback = callback;
this.frequency = frequency;
@@ -128,6 +295,10 @@ PeriodicalExecuter.prototype = {
this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
},
+ execute: function() {
+ this.callback(this);
+ },
+
stop: function() {
if (!this.timer) return;
clearInterval(this.timer);
@@ -138,16 +309,26 @@ PeriodicalExecuter.prototype = {
if (!this.currentlyExecuting) {
try {
this.currentlyExecuting = true;
- this.callback(this);
+ this.execute();
} finally {
this.currentlyExecuting = false;
}
}
}
-}
-String.interpret = function(value){
- return value == null ? '' : String(value);
-}
+});
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
Object.extend(String.prototype, {
gsub: function(pattern, replacement) {
@@ -168,7 +349,7 @@ Object.extend(String.prototype, {
sub: function(pattern, replacement, count) {
replacement = this.gsub.prepareReplacement(replacement);
- count = count === undefined ? 1 : count;
+ count = Object.isUndefined(count) ? 1 : count;
return this.gsub(pattern, function(match) {
if (--count < 0) return match[0];
@@ -178,14 +359,14 @@ Object.extend(String.prototype, {
scan: function(pattern, iterator) {
this.gsub(pattern, iterator);
- return this;
+ return String(this);
},
truncate: function(length, truncation) {
length = length || 30;
- truncation = truncation === undefined ? '...' : truncation;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
return this.length > length ?
- this.slice(0, length - truncation.length) + truncation : this;
+ this.slice(0, length - truncation.length) + truncation : String(this);
},
strip: function() {
@@ -213,35 +394,34 @@ Object.extend(String.prototype, {
},
escapeHTML: function() {
- var div = document.createElement('div');
- var text = document.createTextNode(this);
- div.appendChild(text);
- return div.innerHTML;
+ var self = arguments.callee;
+ self.text.data = this;
+ return self.div.innerHTML;
},
unescapeHTML: function() {
- var div = document.createElement('div');
+ var div = new Element('div');
div.innerHTML = this.stripTags();
return div.childNodes[0] ? (div.childNodes.length > 1 ?
- $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+ $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 {};
+ if (!match) return { };
- return match[1].split(separator || '&').inject({}, function(hash, pair) {
+ 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;
+ var key = decodeURIComponent(pair.shift());
+ var value = pair.length > 1 ? pair.join('=') : pair[0];
+ if (value != undefined) value = decodeURIComponent(value);
- if (hash[name] !== undefined) {
- if (hash[name].constructor != Array)
- hash[name] = [hash[name]];
- if (value) hash[name].push(value);
+ if (key in hash) {
+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+ hash[key].push(value);
}
- else hash[name] = value;
+ else hash[key] = value;
}
return hash;
});
@@ -256,6 +436,10 @@ Object.extend(String.prototype, {
String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
},
+ times: function(count) {
+ return count < 1 ? '' : new Array(count + 1).join(this);
+ },
+
camelize: function() {
var parts = this.split('-'), len = parts.length;
if (len == 1) return parts[0];
@@ -270,7 +454,7 @@ Object.extend(String.prototype, {
return camelized;
},
- capitalize: function(){
+ capitalize: function() {
return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
},
@@ -283,52 +467,131 @@ Object.extend(String.prototype, {
},
inspect: function(useDoubleQuotes) {
- var escapedString = this.replace(/\\/g, '\\\\');
- if (useDoubleQuotes)
- return '"' + escapedString.replace(/"/g, '\\"') + '"';
- else
- return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+ var character = String.specialChar[match[0]];
+ return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ },
+
+ toJSON: function() {
+ return this.inspect(true);
+ },
+
+ unfilterJSON: function(filter) {
+ return this.sub(filter || Prototype.JSONFilter, '#{1}');
+ },
+
+ isJSON: function() {
+ var str = this;
+ if (str.blank()) return false;
+ str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+ return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+ },
+
+ evalJSON: function(sanitize) {
+ var json = this.unfilterJSON();
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ },
+
+ include: function(pattern) {
+ return this.indexOf(pattern) > -1;
+ },
+
+ startsWith: function(pattern) {
+ return this.indexOf(pattern) === 0;
+ },
+
+ endsWith: function(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.lastIndexOf(pattern) === d;
+ },
+
+ empty: function() {
+ return this == '';
+ },
+
+ blank: function() {
+ return /^\s*$/.test(this);
+ },
+
+ interpolate: function(object, pattern) {
+ return new Template(this, pattern).evaluate(object);
+ }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+ escapeHTML: function() {
+ return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+ },
+ unescapeHTML: function() {
+ return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
}
});
String.prototype.gsub.prepareReplacement = function(replacement) {
- if (typeof replacement == 'function') return replacement;
+ if (Object.isFunction(replacement)) 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 = {
+Object.extend(String.prototype.escapeHTML, {
+ div: document.createElement('div'),
+ text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create({
initialize: function(template, pattern) {
this.template = template.toString();
- this.pattern = pattern || Template.Pattern;
+ this.pattern = pattern || Template.Pattern;
},
evaluate: function(object) {
+ if (Object.isFunction(object.toTemplateReplacements))
+ object = object.toTemplateReplacements();
+
return this.template.gsub(this.pattern, function(match) {
- var before = match[1];
+ if (object == null) return '';
+
+ var before = match[1] || '';
if (before == '\\') return match[2];
- return before + String.interpret(object[match[3]]);
- });
+
+ var ctx = object, expr = match[3];
+ var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + String.interpret(ctx);
+ }.bind(this));
}
-}
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
-var $break = new Object();
-var $continue = new Object();
+var $break = { };
var Enumerable = {
- each: function(iterator) {
+ each: function(iterator, context) {
var index = 0;
+ iterator = iterator.bind(context);
try {
this._each(function(value) {
- try {
- iterator(value, index++);
- } catch (e) {
- if (e != $continue) throw e;
- }
+ iterator(value, index++);
});
} catch (e) {
if (e != $break) throw e;
@@ -336,40 +599,45 @@ var Enumerable = {
return this;
},
- eachSlice: function(number, iterator) {
+ eachSlice: function(number, iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var index = -number, slices = [], array = this.toArray();
while ((index += number) < array.length)
slices.push(array.slice(index, index+number));
- return slices.map(iterator);
+ return slices.collect(iterator, context);
},
- all: function(iterator) {
+ all: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var result = true;
this.each(function(value, index) {
- result = result && !!(iterator || Prototype.K)(value, index);
+ result = result && !!iterator(value, index);
if (!result) throw $break;
});
return result;
},
- any: function(iterator) {
+ any: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var result = false;
this.each(function(value, index) {
- if (result = !!(iterator || Prototype.K)(value, index))
+ if (result = !!iterator(value, index))
throw $break;
});
return result;
},
- collect: function(iterator) {
+ collect: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var results = [];
this.each(function(value, index) {
- results.push((iterator || Prototype.K)(value, index));
+ results.push(iterator(value, index));
});
return results;
},
- detect: function(iterator) {
+ detect: function(iterator, context) {
+ iterator = iterator.bind(context);
var result;
this.each(function(value, index) {
if (iterator(value, index)) {
@@ -380,7 +648,8 @@ var Enumerable = {
return result;
},
- findAll: function(iterator) {
+ findAll: function(iterator, context) {
+ iterator = iterator.bind(context);
var results = [];
this.each(function(value, index) {
if (iterator(value, index))
@@ -389,17 +658,24 @@ var Enumerable = {
return results;
},
- grep: function(pattern, iterator) {
+ grep: function(filter, iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var results = [];
+
+ if (Object.isString(filter))
+ filter = new RegExp(filter);
+
this.each(function(value, index) {
- var stringValue = value.toString();
- if (stringValue.match(pattern))
- results.push((iterator || Prototype.K)(value, index));
- })
+ if (filter.match(value))
+ results.push(iterator(value, index));
+ });
return results;
},
include: function(object) {
+ if (Object.isFunction(this.indexOf))
+ if (this.indexOf(object) != -1) return true;
+
var found = false;
this.each(function(value) {
if (value == object) {
@@ -411,14 +687,15 @@ var Enumerable = {
},
inGroupsOf: function(number, fillWith) {
- fillWith = fillWith === undefined ? null : fillWith;
+ fillWith = Object.isUndefined(fillWith) ? null : fillWith;
return this.eachSlice(number, function(slice) {
while(slice.length < number) slice.push(fillWith);
return slice;
});
},
- inject: function(memo, iterator) {
+ inject: function(memo, iterator, context) {
+ iterator = iterator.bind(context);
this.each(function(value, index) {
memo = iterator(memo, value, index);
});
@@ -432,30 +709,33 @@ var Enumerable = {
});
},
- max: function(iterator) {
+ max: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var result;
this.each(function(value, index) {
- value = (iterator || Prototype.K)(value, index);
- if (result == undefined || value >= result)
+ value = iterator(value, index);
+ if (result == null || value >= result)
result = value;
});
return result;
},
- min: function(iterator) {
+ min: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var result;
this.each(function(value, index) {
- value = (iterator || Prototype.K)(value, index);
- if (result == undefined || value < result)
+ value = iterator(value, index);
+ if (result == null || value < result)
result = value;
});
return result;
},
- partition: function(iterator) {
+ partition: function(iterator, context) {
+ iterator = iterator ? iterator.bind(context) : Prototype.K;
var trues = [], falses = [];
this.each(function(value, index) {
- ((iterator || Prototype.K)(value, index) ?
+ (iterator(value, index) ?
trues : falses).push(value);
});
return [trues, falses];
@@ -463,13 +743,14 @@ var Enumerable = {
pluck: function(property) {
var results = [];
- this.each(function(value, index) {
+ this.each(function(value) {
results.push(value[property]);
});
return results;
},
- reject: function(iterator) {
+ reject: function(iterator, context) {
+ iterator = iterator.bind(context);
var results = [];
this.each(function(value, index) {
if (!iterator(value, index))
@@ -478,7 +759,8 @@ var Enumerable = {
return results;
},
- sortBy: function(iterator) {
+ sortBy: function(iterator, context) {
+ iterator = iterator.bind(context);
return this.map(function(value, index) {
return {value: value, criteria: iterator(value, index)};
}).sort(function(left, right) {
@@ -493,7 +775,7 @@ var Enumerable = {
zip: function() {
var iterator = Prototype.K, args = $A(arguments);
- if (typeof args.last() == 'function')
+ if (Object.isFunction(args.last()))
iterator = args.pop();
var collections = [this].concat(args).map($A);
@@ -509,31 +791,42 @@ var Enumerable = {
inspect: function() {
return '#<Enumerable:' + this.toArray().inspect() + '>';
}
-}
+};
Object.extend(Enumerable, {
map: Enumerable.collect,
find: Enumerable.detect,
select: Enumerable.findAll,
+ filter: Enumerable.findAll,
member: Enumerable.include,
- entries: Enumerable.toArray
+ entries: Enumerable.toArray,
+ every: Enumerable.all,
+ some: Enumerable.any
});
-var $A = Array.from = function(iterable) {
+function $A(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]);
+ if (iterable.toArray) return iterable.toArray();
+ var length = iterable.length, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+}
+
+if (Prototype.Browser.WebKit) {
+ function $A(iterable) {
+ if (!iterable) return [];
+ if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&
+ iterable.toArray) return iterable.toArray();
+ var length = iterable.length, results = new Array(length);
+ while (length--) results[length] = iterable[length];
return results;
}
}
+Array.from = $A;
+
Object.extend(Array.prototype, Enumerable);
-if (!Array.prototype._reverse)
- Array.prototype._reverse = Array.prototype.reverse;
+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
Object.extend(Array.prototype, {
_each: function(iterator) {
@@ -562,7 +855,7 @@ Object.extend(Array.prototype, {
flatten: function() {
return this.inject([], function(array, value) {
- return array.concat(value && value.constructor == Array ?
+ return array.concat(Object.isArray(value) ?
value.flatten() : [value]);
});
},
@@ -574,12 +867,6 @@ Object.extend(Array.prototype, {
});
},
- 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();
},
@@ -588,9 +875,17 @@ Object.extend(Array.prototype, {
return this.length > 1 ? this : this[0];
},
- uniq: function() {
- return this.inject([], function(array, value) {
- return array.include(value) ? array : array.concat([value]);
+ uniq: function(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ },
+
+ intersect: function(array) {
+ return this.uniq().findAll(function(item) {
+ return array.detect(function(value) { return item === value });
});
},
@@ -604,125 +899,187 @@ Object.extend(Array.prototype, {
inspect: function() {
return '[' + this.map(Object.inspect).join(', ') + ']';
+ },
+
+ toJSON: function() {
+ var results = [];
+ this.each(function(object) {
+ var value = Object.toJSON(object);
+ if (!Object.isUndefined(value)) results.push(value);
+ });
+ return '[' + results.join(', ') + ']';
}
});
+// use native browser JS 1.6 implementation if available
+if (Object.isFunction(Array.prototype.forEach))
+ Array.prototype._each = Array.prototype.forEach;
+
+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
+ i || (i = 0);
+ var length = this.length;
+ if (i < 0) i = length + i;
+ for (; i < length; i++)
+ if (this[i] === item) return i;
+ return -1;
+};
+
+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+ var n = this.slice(0, i).reverse().indexOf(item);
+ return (n < 0) ? n : i - n - 1;
+};
+
Array.prototype.toArray = Array.prototype.clone;
-function $w(string){
+function $w(string) {
+ if (!Object.isString(string)) return [];
string = string.strip();
return string ? string.split(/\s+/) : [];
}
-if(window.opera){
- Array.prototype.concat = function(){
+if (Prototype.Browser.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++)
+ 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 (Object.isArray(arguments[i])) {
+ 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');
+Object.extend(Number.prototype, {
+ toColorPart: function() {
+ return this.toPaddedString(2, 16);
},
- merge: function(hash) {
- return $H(hash).inject(this, function(mergedHash, pair) {
- mergedHash[pair.key] = pair.value;
- return mergedHash;
- });
+ succ: function() {
+ return this + 1;
},
- 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;
+ times: function(iterator) {
+ $R(0, this, true).each(iterator);
+ return this;
},
- toQueryString: function() {
- return Hash.toQueryString(this);
+ toPaddedString: function(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
},
- inspect: function() {
- return '#<Hash:{' + this.map(function(pair) {
- return pair.map(Object.inspect).join(': ');
- }).join(', ') + '}>';
+ toJSON: function() {
+ return isFinite(this) ? this.toString() : 'null';
}
});
+$w('abs round ceil floor').each(function(method){
+ Number.prototype[method] = Math[method].methodize();
+});
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, {
+
+var Hash = Class.create(Enumerable, (function() {
+
+ function toQueryPair(key, value) {
+ if (Object.isUndefined(value)) return key;
+ return key + '=' + encodeURIComponent(String.interpret(value));
+ }
+
+ return {
+ initialize: function(object) {
+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+ },
+
+ _each: function(iterator) {
+ for (var key in this._object) {
+ var value = this._object[key], pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ },
+
+ set: function(key, value) {
+ return this._object[key] = value;
+ },
+
+ get: function(key) {
+ return this._object[key];
+ },
+
+ unset: function(key) {
+ var value = this._object[key];
+ delete this._object[key];
+ return value;
+ },
+
+ toObject: function() {
+ return Object.clone(this._object);
+ },
+
+ keys: function() {
+ return this.pluck('key');
+ },
+
+ values: function() {
+ return this.pluck('value');
+ },
+
+ index: function(value) {
+ var match = this.detect(function(pair) {
+ return pair.value === value;
+ });
+ return match && match.key;
+ },
+
+ merge: function(object) {
+ return this.clone().update(object);
+ },
+
+ update: function(object) {
+ return new Hash(object).inject(this, function(result, pair) {
+ result.set(pair.key, pair.value);
+ return result;
+ });
+ },
+
+ toQueryString: function() {
+ return this.map(function(pair) {
+ var key = encodeURIComponent(pair.key), values = pair.value;
+
+ if (values && typeof values == 'object') {
+ if (Object.isArray(values))
+ return values.map(toQueryPair.curry(key)).join('&');
+ }
+ return toQueryPair(key, values);
+ }).join('&');
+ },
+
+ inspect: function() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ },
+
+ toJSON: function() {
+ return Object.toJSON(this.toObject());
+ },
+
+ clone: function() {
+ return new Hash(this);
+ }
+ }
+})());
+
+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
+Hash.from = $H;
+var ObjectRange = Class.create(Enumerable, {
initialize: function(start, end, exclusive) {
this.start = start;
this.end = end;
@@ -748,7 +1105,7 @@ Object.extend(ObjectRange.prototype, {
var $R = function(start, end, exclusive) {
return new ObjectRange(start, end, exclusive);
-}
+};
var Ajax = {
getTransport: function() {
@@ -760,7 +1117,7 @@ var Ajax = {
},
activeRequestCount: 0
-}
+};
Ajax.Responders = {
responders: [],
@@ -780,10 +1137,10 @@ Ajax.Responders = {
dispatch: function(callback, request, transport, json) {
this.each(function(responder) {
- if (typeof responder[callback] == 'function') {
+ if (Object.isFunction(responder[callback])) {
try {
responder[callback].apply(responder, [request, transport, json]);
- } catch (e) {}
+ } catch (e) { }
}
});
}
@@ -792,49 +1149,45 @@ Ajax.Responders = {
Object.extend(Ajax.Responders, Enumerable);
Ajax.Responders.register({
- onCreate: function() {
- Ajax.activeRequestCount++;
- },
- onComplete: function() {
- Ajax.activeRequestCount--;
- }
+ onCreate: function() { Ajax.activeRequestCount++ },
+ onComplete: function() { Ajax.activeRequestCount-- }
});
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
- setOptions: function(options) {
+Ajax.Base = Class.create({
+ initialize: function(options) {
this.options = {
method: 'post',
asynchronous: true,
contentType: 'application/x-www-form-urlencoded',
encoding: 'UTF-8',
- parameters: ''
- }
- Object.extend(this.options, options || {});
+ parameters: '',
+ evalJSON: true,
+ evalJS: true
+ };
+ Object.extend(this.options, options || { });
this.options.method = this.options.method.toLowerCase();
- if (typeof this.options.parameters == 'string')
+
+ if (Object.isString(this.options.parameters))
this.options.parameters = this.options.parameters.toQueryParams();
+ else if (Object.isHash(this.options.parameters))
+ this.options.parameters = this.options.parameters.toObject();
}
-}
-
-Ajax.Request = Class.create();
-Ajax.Request.Events =
- ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+});
-Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+Ajax.Request = Class.create(Ajax.Base, {
_complete: false,
- initialize: function(url, options) {
+ initialize: function($super, url, options) {
+ $super(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;
+ var params = Object.clone(this.options.parameters);
if (!['get', 'post'].include(this.method)) {
// simulate other verbs over post
@@ -842,28 +1195,31 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
this.method = 'post';
}
- params = Hash.toQueryString(params);
- if (params && /Konqueror|Safari|KHTML/.test(navigator.userAgent)) params += '&_='
+ this.parameters = params;
- // when GET, append parameters to URL
- if (this.method == 'get' && params)
- this.url += (this.url.indexOf('?') > -1 ? '&' : '?') + params;
+ if (params = Object.toQueryString(params)) {
+ // when GET, append parameters to URL
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
try {
- Ajax.Responders.dispatch('onCreate', this, this.transport);
+ var response = new Ajax.Response(this);
+ if (this.options.onCreate) this.options.onCreate(response);
+ Ajax.Responders.dispatch('onCreate', this, response);
this.transport.open(this.method.toUpperCase(), this.url,
this.options.asynchronous);
- if (this.options.asynchronous)
- setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
this.transport.onreadystatechange = this.onStateChange.bind(this);
this.setRequestHeaders();
- var body = this.method == 'post' ? (this.options.postBody || params) : null;
-
- this.transport.send(body);
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
/* Force Firefox to handle ready state 4 for synchronous requests */
if (!this.options.asynchronous && this.transport.overrideMimeType)
@@ -905,7 +1261,7 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
if (typeof this.options.requestHeaders == 'object') {
var extras = this.options.requestHeaders;
- if (typeof extras.push == 'function')
+ if (Object.isFunction(extras.push))
for (var i = 0, length = extras.length; i < length; i += 2)
headers[extras[i]] = extras[i+1];
else
@@ -917,32 +1273,39 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
},
success: function() {
- return !this.transport.status
- || (this.transport.status >= 200 && this.transport.status < 300);
+ var status = this.getStatus();
+ return !status || (status >= 200 && status < 300);
+ },
+
+ getStatus: function() {
+ try {
+ return this.transport.status || 0;
+ } catch (e) { return 0 }
},
respondToReadyState: function(readyState) {
- var state = Ajax.Request.Events[readyState];
- var transport = this.transport, json = this.evalJSON();
+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
if (state == 'Complete') {
try {
this._complete = true;
- (this.options['on' + this.transport.status]
+ (this.options['on' + response.status]
|| this.options['on' + (this.success() ? 'Success' : 'Failure')]
- || Prototype.emptyFunction)(transport, json);
+ || Prototype.emptyFunction)(response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
- if ((this.getHeader('Content-type') || 'text/javascript').strip().
- match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
- this.evalResponse();
+ var contentType = response.getHeader('Content-type');
+ if (this.options.evalJS == 'force'
+ || (this.options.evalJS && contentType
+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+ this.evalResponse();
}
try {
- (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
- Ajax.Responders.dispatch('on' + state, this, transport, json);
+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
} catch (e) {
this.dispatchException(e);
}
@@ -959,16 +1322,9 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
} 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);
+ return eval((this.transport.responseText || '').unfilterJSON());
} catch (e) {
this.dispatchException(e);
}
@@ -980,57 +1336,126 @@ Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
}
});
-Ajax.Updater = Class.create();
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Response = Class.create({
+ initialize: function(request){
+ this.request = request;
+ var transport = this.transport = request.transport,
+ readyState = this.readyState = transport.readyState;
+
+ if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+ this.status = this.getStatus();
+ this.statusText = this.getStatusText();
+ this.responseText = String.interpret(transport.responseText);
+ this.headerJSON = this._getHeaderJSON();
+ }
+
+ if(readyState == 4) {
+ var xml = transport.responseXML;
+ this.responseXML = Object.isUndefined(xml) ? null : xml;
+ this.responseJSON = this._getResponseJSON();
+ }
+ },
+
+ status: 0,
+ statusText: '',
+
+ getStatus: Ajax.Request.prototype.getStatus,
+
+ getStatusText: function() {
+ try {
+ return this.transport.statusText || '';
+ } catch (e) { return '' }
+ },
+
+ getHeader: Ajax.Request.prototype.getHeader,
+
+ getAllHeaders: function() {
+ try {
+ return this.getAllResponseHeaders();
+ } catch (e) { return null }
+ },
+
+ getResponseHeader: function(name) {
+ return this.transport.getResponseHeader(name);
+ },
+
+ getAllResponseHeaders: function() {
+ return this.transport.getAllResponseHeaders();
+ },
-Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
- initialize: function(container, url, options) {
+ _getHeaderJSON: function() {
+ var json = this.getHeader('X-JSON');
+ if (!json) return null;
+ json = decodeURIComponent(escape(json));
+ try {
+ return json.evalJSON(this.request.options.sanitizeJSON);
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ },
+
+ _getResponseJSON: function() {
+ var options = this.request.options;
+ if (!options.evalJSON || (options.evalJSON != 'force' &&
+ !(this.getHeader('Content-type') || '').include('application/json')) ||
+ this.responseText.blank())
+ return null;
+ try {
+ return this.responseText.evalJSON(options.sanitizeJSON);
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+ initialize: function($super, 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);
+ options = Object.clone(options);
+ var onComplete = options.onComplete;
+ options.onComplete = (function(response, json) {
+ this.updateContent(response.responseText);
+ if (Object.isFunction(onComplete)) onComplete(response, json);
}).bind(this);
- this.request(url);
+ $super(url, options);
},
- updateContent: function() {
- var receiver = this.container[this.success() ? 'success' : 'failure'];
- var response = this.transport.responseText;
+ updateContent: function(responseText) {
+ var receiver = this.container[this.success() ? 'success' : 'failure'],
+ options = this.options;
- if (!this.options.evalScripts) response = response.stripScripts();
+ if (!options.evalScripts) responseText = responseText.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);
+ if (options.insertion) {
+ if (Object.isString(options.insertion)) {
+ var insertion = { }; insertion[options.insertion] = responseText;
+ receiver.insert(insertion);
+ }
+ else options.insertion(receiver, responseText);
+ }
+ else receiver.update(responseText);
}
}
});
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
- initialize: function(container, url, options) {
- this.setOptions(options);
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+ initialize: function($super, container, url, options) {
+ $super(options);
this.onComplete = this.options.onComplete;
this.frequency = (this.options.frequency || 2);
this.decay = (this.options.decay || 1);
- this.updater = {};
+ this.updater = { };
this.container = container;
this.url = url;
@@ -1048,15 +1473,14 @@ Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
(this.onComplete || Prototype.emptyFunction).apply(this, arguments);
},
- updateComplete: function(request) {
+ updateComplete: function(response) {
if (this.options.decay) {
- this.decay = (request.responseText == this.lastText ?
+ this.decay = (response.responseText == this.lastText ?
this.decay * this.options.decay : 1);
- this.lastText = request.responseText;
+ this.lastText = response.responseText;
}
- this.timer = setTimeout(this.onTimerEvent.bind(this),
- this.decay * this.frequency * 1000);
+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
},
onTimerEvent: function() {
@@ -1069,7 +1493,7 @@ function $(element) {
elements.push($(arguments[i]));
return elements;
}
- if (typeof element == 'string')
+ if (Object.isString(element))
element = document.getElementById(element);
return Element.extend(element);
}
@@ -1080,63 +1504,51 @@ if (Prototype.BrowserFeatures.XPath) {
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));
+ results.push(Element.extend(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);
+if (!window.Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+ // DOM level 2 ECMAScript Language Binding
+ Object.extend(Node, {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ });
+}
- for (var property in methods) {
- var value = methods[property];
- if (typeof value == 'function' && !(property in element))
- element[property] = cache.findOrStore(value);
+(function() {
+ var element = this.Element;
+ this.Element = function(tagName, attributes) {
+ attributes = attributes || { };
+ tagName = tagName.toLowerCase();
+ var cache = Element.cache;
+ if (Prototype.Browser.IE && attributes.name) {
+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
+ delete attributes.name;
+ return Element.writeAttribute(document.createElement(tagName), attributes);
}
- }
+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+ };
+ Object.extend(this.Element, element || { });
+}).call(window);
- 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.cache = { };
Element.Methods = {
visible: function(element) {
@@ -1165,28 +1577,74 @@ Element.Methods = {
return element;
},
- update: function(element, html) {
- html = typeof html == 'undefined' ? '' : html.toString();
- $(element).innerHTML = html.stripScripts();
- setTimeout(function() {html.evalScripts()}, 10);
+ update: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+ content = Object.toHTML(content);
+ element.innerHTML = content.stripScripts();
+ content.evalScripts.bind(content).defer();
return element;
},
- replace: function(element, html) {
+ replace: function(element, content) {
element = $(element);
- html = typeof html == 'undefined' ? '' : html.toString();
- if (element.outerHTML) {
- element.outerHTML = html.stripScripts();
- } else {
+ if (content && content.toElement) content = content.toElement();
+ else if (!Object.isElement(content)) {
+ content = Object.toHTML(content);
var range = element.ownerDocument.createRange();
- range.selectNodeContents(element);
- element.parentNode.replaceChild(
- range.createContextualFragment(html.stripScripts()), element);
+ range.selectNode(element);
+ content.evalScripts.bind(content).defer();
+ content = range.createContextualFragment(content.stripScripts());
+ }
+ element.parentNode.replaceChild(content, element);
+ return element;
+ },
+
+ insert: function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = {bottom:insertions};
+
+ var content, t, range;
+
+ for (position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ t = Element._insertionTranslations[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ t.insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+
+ range = element.ownerDocument.createRange();
+ t.initializeRange(element, range);
+ t.insert(element, range.createContextualFragment(content.stripScripts()));
+
+ content.evalScripts.bind(content).defer();
}
- setTimeout(function() {html.evalScripts()}, 10);
+
return element;
},
+ wrap: function(element, wrapper, attributes) {
+ element = $(element);
+ if (Object.isElement(wrapper))
+ $(wrapper).writeAttribute(attributes || { });
+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+ else wrapper = new Element('div', wrapper);
+ if (element.parentNode)
+ element.parentNode.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ return wrapper;
+ },
+
inspect: function(element) {
element = $(element);
var result = '<' + element.tagName.toLowerCase();
@@ -1212,7 +1670,13 @@ Element.Methods = {
},
descendants: function(element) {
- return $A($(element).getElementsByTagName('*'));
+ return $(element).getElementsBySelector("*");
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
},
immediateDescendants: function(element) {
@@ -1236,48 +1700,96 @@ Element.Methods = {
},
match: function(element, selector) {
- if (typeof selector == 'string')
+ if (Object.isString(selector))
selector = new Selector(selector);
return selector.match($(element));
},
up: function(element, expression, index) {
- return Selector.findElement($(element).ancestors(), expression, index);
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = element.ancestors();
+ return expression ? Selector.findElement(ancestors, expression, index) :
+ ancestors[index || 0];
},
down: function(element, expression, index) {
- return Selector.findElement($(element).descendants(), expression, index);
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ var descendants = element.descendants();
+ return expression ? Selector.findElement(descendants, expression, index) :
+ descendants[index || 0];
},
previous: function(element, expression, index) {
- return Selector.findElement($(element).previousSiblings(), expression, index);
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+ var previousSiblings = element.previousSiblings();
+ return expression ? Selector.findElement(previousSiblings, expression, index) :
+ previousSiblings[index || 0];
},
next: function(element, expression, index) {
- return Selector.findElement($(element).nextSiblings(), expression, index);
+ element = $(element);
+ if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+ var nextSiblings = element.nextSiblings();
+ return expression ? Selector.findElement(nextSiblings, expression, index) :
+ nextSiblings[index || 0];
},
- getElementsBySelector: function() {
+ select: function() {
var args = $A(arguments), element = $(args.shift());
return Selector.findChildElements(element, args);
},
- getElementsByClassName: function(element, className) {
- return document.getElementsByClassName(className, element);
+ adjacent: function() {
+ var args = $A(arguments), element = $(args.shift());
+ return Selector.findChildElements(element.parentNode, args).without(element);
+ },
+
+ identify: function(element) {
+ element = $(element);
+ var id = element.readAttribute('id'), self = arguments.callee;
+ if (id) return id;
+ do { id = 'anonymous_element_' + self.counter++ } while ($(id));
+ element.writeAttribute('id', id);
+ return id;
},
readAttribute: function(element, name) {
element = $(element);
- if (document.all && !window.opera) {
- var t = Element._attributeTranslations;
+ if (Prototype.Browser.IE) {
+ var t = Element._attributeTranslations.read;
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;
+ if (t.names[name]) name = t.names[name];
+ if (name.include(':')) {
+ return (!element.attributes || !element.attributes[name]) ? null :
+ element.attributes[name].value;
+ }
}
return element.getAttribute(name);
},
+ writeAttribute: function(element, name, value) {
+ element = $(element);
+ var attributes = { }, t = Element._attributeTranslations.write;
+
+ if (typeof name == 'object') attributes = name;
+ else attributes[name] = Object.isUndefined(value) ? true : value;
+
+ for (var attr in attributes) {
+ name = t.names[attr] || attr;
+ value = attributes[attr];
+ if (t.values[attr]) name = t.values[attr](element, value);
+ if (value === false || value === null)
+ element.removeAttribute(name);
+ else if (value === true)
+ element.setAttribute(name, name);
+ else element.setAttribute(name, value);
+ }
+ return element;
+ },
+
getHeight: function(element) {
return $(element).getDimensions().height;
},
@@ -1293,39 +1805,28 @@ Element.Methods = {
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;
+ return (elementClassName.length > 0 && (elementClassName == className ||
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
},
addClassName: function(element, className) {
if (!(element = $(element))) return;
- Element.classNames(element).add(className);
+ if (!element.hasClassName(className))
+ element.className += (element.className ? ' ' : '') + className;
return element;
},
removeClassName: function(element, className) {
if (!(element = $(element))) return;
- Element.classNames(element).remove(className);
+ element.className = element.className.replace(
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
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();
+ return element[element.hasClassName(className) ?
+ 'removeClassName' : 'addClassName'](className);
},
// removes whitespace-only text node children
@@ -1342,74 +1843,76 @@ Element.Methods = {
},
empty: function(element) {
- return $(element).innerHTML.match(/^\s*$/);
+ return $(element).innerHTML.blank();
},
descendantOf: function(element, ancestor) {
element = $(element), ancestor = $(ancestor);
+ var originalAncestor = ancestor;
+
+ if (element.compareDocumentPosition)
+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+ if (element.sourceIndex && !Prototype.Browser.Opera) {
+ var e = element.sourceIndex, a = ancestor.sourceIndex,
+ nextAncestor = ancestor.nextSibling;
+ if (!nextAncestor) {
+ do { ancestor = ancestor.parentNode; }
+ while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);
+ }
+ if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex);
+ }
+
while (element = element.parentNode)
- if (element == ancestor) return true;
+ if (element == originalAncestor) return true;
return false;
},
scrollTo: function(element) {
element = $(element);
- var pos = Position.cumulativeOffset(element);
+ var pos = element.cumulativeOffset();
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();
+ style = style == 'float' ? 'cssFloat' : 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];
- }
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
}
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
- if((value == 'auto') && ['width','height'].include(style) && (element.getStyle('display') != 'none'))
- value = element['offset'+style.capitalize()] + 'px';
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
- 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;
+ setStyle: function(element, styles) {
+ element = $(element);
+ var elementStyle = element.style, match;
+ if (Object.isString(styles)) {
+ element.style.cssText += ';' + styles;
+ return styles.include('opacity') ?
+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
}
- return value == 'auto' ? null : value;
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property]);
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+ property] = styles[property];
+
+ return element;
},
- setStyle: function(element, style) {
+ setOpacity: function(element, value) {
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;
- }
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
return element;
},
@@ -1468,8 +1971,8 @@ Element.Methods = {
makeClipping: function(element) {
element = $(element);
if (element._overflow) return element;
- element._overflow = element.style.overflow || 'auto';
- if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+ element._overflow = Element.getStyle(element, 'overflow') || 'auto';
+ if (element._overflow !== 'hidden')
element.style.overflow = 'hidden';
return element;
},
@@ -1480,393 +1983,1398 @@ Element.Methods = {
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"
-};
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return Element._returnOffset(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 Element._returnOffset(valueL, valueT);
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'absolute') return;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
-Element._attributeTranslations.values = {
- _getAttr: function(element, attribute) {
- return element.getAttribute(attribute, 2);
+ var offsets = element.positionedOffset();
+ 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';
+ return element;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (element.getStyle('position') == 'relative') return;
+ // Position.prepare(); // To be done manually by Scripty when it needs it.
+
+ 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;
+ return element;
+ },
+
+ cumulativeScrollOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
},
- _flag: function(element, attribute) {
- return $(element).hasAttribute(attribute) ? attribute : null;
+ getOffsetParent: 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);
},
- style: function(element) {
- return element.style.cssText.toLowerCase();
+ viewportOffset: 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 &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!Prototype.Browser.Opera || element.tagName == 'BODY') {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return Element._returnOffset(valueL, valueT);
},
- title: function(element) {
- var node = element.getAttributeNode('title');
- return node.specified ? node.nodeValue : null;
+ clonePosition: function(element, source) {
+ 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 = source.viewportOffset();
+
+ // find coordinate system to use
+ element = $(element);
+ 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(element, 'position') == 'absolute') {
+ parent = element.getOffsetParent();
+ delta = parent.viewportOffset();
+ }
+
+ // 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) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+ return element;
}
};
-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.identify.counter = 1;
+
+Object.extend(Element.Methods, {
+ getElementsBySelector: Element.Methods.select,
+ childElements: Element.Methods.immediateDescendants
});
-Element.Methods.Simulated = {
- hasAttribute: function(element, attribute) {
- var t = Element._attributeTranslations;
- attribute = t.names[attribute] || attribute;
- return $(element).getAttributeNode(attribute).specified;
+Element._attributeTranslations = {
+ write: {
+ names: {
+ className: 'class',
+ htmlFor: 'for'
+ },
+ values: { }
}
};
-// IE is missing .innerHTML support for TABLE-related elements
-if (document.all && !window.opera){
- Element.Methods.update = function(element, html) {
+
+if (!document.createRange || Prototype.Browser.Opera) {
+ Element.Methods.insert = function(element, insertions) {
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 = '<table><tbody>' + html.stripScripts() + '</tbody></table>';
- depth = 2;
- break;
- case 'TR':
- div.innerHTML = '<table><tbody><tr>' + html.stripScripts() + '</tr></tbody></table>';
- depth = 3;
- break;
- case 'TD':
- div.innerHTML = '<table><tbody><tr><td>' + html.stripScripts() + '</td></tr></tbody></table>';
- depth = 4;
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = { bottom: insertions };
+
+ var t = Element._insertionTranslations, content, position, pos, tagName;
+
+ for (position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ pos = t[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ pos.insert(element, content);
+ continue;
}
- $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();
+ content = Object.toHTML(content);
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ if (t.tags[tagName]) {
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ if (position == 'top' || position == 'after') fragments.reverse();
+ fragments.each(pos.insert.curry(element));
+ }
+ else element.insertAdjacentHTML(pos.adjacency, content.stripScripts());
+
+ content.evalScripts.bind(content).defer();
}
- setTimeout(function() {html.evalScripts()}, 10);
+
return element;
- }
-};
+ };
+}
-Object.extend(Element, Element.Methods);
+if (Prototype.Browser.Opera) {
+ Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+ function(proceed, element, style) {
+ switch (style) {
+ case 'left': case 'top': case 'right': case 'bottom':
+ if (proceed(element, 'position') === 'static') return null;
+ case 'height': case 'width':
+ // returns '0px' for hidden elements; we want it to return null
+ if (!Element.visible(element)) return null;
+
+ // returns the border-box dimensions rather than the content-box
+ // dimensions, so we subtract padding and borders from the value
+ var dim = parseInt(proceed(element, style), 10);
+
+ if (dim !== element['offset' + style.capitalize()])
+ return dim + 'px';
+
+ var properties;
+ if (style === 'height') {
+ properties = ['border-top-width', 'padding-top',
+ 'padding-bottom', 'border-bottom-width'];
+ }
+ else {
+ properties = ['border-left-width', 'padding-left',
+ 'padding-right', 'border-right-width'];
+ }
+ return properties.inject(dim, function(memo, property) {
+ var val = proceed(element, property);
+ return val === null ? memo : memo - parseInt(val, 10);
+ }) + 'px';
+ default: return proceed(element, style);
+ }
+ }
+ );
-var _nativeExtensions = false;
+ Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+ function(proceed, element, attribute) {
+ if (attribute === 'title') return element.title;
+ return proceed(element, attribute);
+ }
+ );
+}
-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__;
+else if (Prototype.Browser.IE) {
+ $w('positionedOffset getOffsetParent viewportOffset').each(function(method) {
+ Element.Methods[method] = Element.Methods[method].wrap(
+ function(proceed, element) {
+ element = $(element);
+ var position = element.getStyle('position');
+ if (position != 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
});
-Element.addMethods = function(methods) {
- Object.extend(Element.Methods, methods || {});
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
- 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 (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
}
- }
- 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);
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset' + style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ function stripAlpha(filter){
+ return filter.replace(/alpha\([^\)]*\)/gi,'');
+ }
+ element = $(element);
+ var currentStyle = element.currentStyle;
+ if ((currentStyle && !currentStyle.hasLayout) ||
+ (!currentStyle && element.style.zoom == 'normal'))
+ element.style.zoom = 1;
+
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ (filter = stripAlpha(filter)) ?
+ style.filter = filter : style.removeAttribute('filter');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = stripAlpha(filter) +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ Element._attributeTranslations = {
+ read: {
+ names: {
+ 'class': 'className',
+ 'for': 'htmlFor'
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _getAttrNode: function(element, attribute) {
+ var node = element.getAttributeNode(attribute);
+ return node ? node.value : "";
+ },
+ _getEv: function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ return attribute ? attribute.toString().slice(23, -2) : null;
+ },
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ return element.title;
+ }
+ }
+ }
+ };
+
+ Element._attributeTranslations.write = {
+ names: Object.clone(Element._attributeTranslations.read.names),
+ values: {
+ checked: function(element, value) {
+ element.checked = !!value;
+ },
+
+ style: function(element, value) {
+ element.style.cssText = value ? value : '';
+ }
+ }
+ };
+
+ Element._attributeTranslations.has = {};
+
+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+ 'encType maxLength readOnly longDesc').each(function(attr) {
+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+ });
+
+ (function(v) {
+ Object.extend(v, {
+ href: v._getAttr,
+ src: v._getAttr,
+ type: v._getAttr,
+ action: v._getAttrNode,
+ disabled: v._flag,
+ checked: v._flag,
+ readonly: v._flag,
+ multiple: v._flag,
+ onload: v._getEv,
+ onunload: v._getEv,
+ onclick: v._getEv,
+ ondblclick: v._getEv,
+ onmousedown: v._getEv,
+ onmouseup: v._getEv,
+ onmouseover: v._getEv,
+ onmousemove: v._getEv,
+ onmouseout: v._getEv,
+ onfocus: v._getEv,
+ onblur: v._getEv,
+ onkeypress: v._getEv,
+ onkeydown: v._getEv,
+ onkeyup: v._getEv,
+ onsubmit: v._getEv,
+ onreset: v._getEv,
+ onselect: v._getEv,
+ onchange: v._getEv
});
- _nativeExtensions = true;
- }
+ })(Element._attributeTranslations.read.values);
}
-var Toggle = new Object();
-Toggle.display = Element.toggle;
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
-/*--------------------------------------------------------------------------*/
+else if (Prototype.Browser.WebKit) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+
+ if (value == 1)
+ if(element.tagName == 'IMG' && element.width) {
+ element.width++; element.width--;
+ } else try {
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch (e) { }
+
+ return element;
+ };
+
+ // Safari returns margins on body which is incorrect if the child is absolutely
+ // positioned. For performance reasons, redefine Element#cumulativeOffset for
+ // KHTML/WebKit only.
+ Element.Methods.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);
-Abstract.Insertion = function(adjacency) {
- this.adjacency = adjacency;
+ return Element._returnOffset(valueL, valueT);
+ };
}
-Abstract.Insertion.prototype = {
- initialize: function(element, content) {
- this.element = $(element);
- this.content = content.stripScripts();
+if (Prototype.Browser.IE || Prototype.Browser.Opera) {
+ // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
+ Element.Methods.update = function(element, content) {
+ element = $(element);
- 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()._reverse());
- } else {
- throw e;
- }
- }
- } else {
- this.range = this.element.ownerDocument.createRange();
- if (this.initializeRange) this.initializeRange();
- this.insertContent([this.range.createContextualFragment(this.content)]);
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) return element.update().insert(content);
+
+ content = Object.toHTML(content);
+ var tagName = element.tagName.toUpperCase();
+
+ if (tagName in Element._insertionTranslations.tags) {
+ $A(element.childNodes).each(function(node) { element.removeChild(node) });
+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+ .each(function(node) { element.appendChild(node) });
+ }
+ else element.innerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+if (document.createElement('div').outerHTML) {
+ Element.Methods.replace = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ element.parentNode.replaceChild(content, element);
+ return element;
}
- setTimeout(function() {content.evalScripts()}, 10);
+ content = Object.toHTML(content);
+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+ if (Element._insertionTranslations.tags[tagName]) {
+ var nextSibling = element.next();
+ var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ parent.removeChild(element);
+ if (nextSibling)
+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+ else
+ fragments.each(function(node) { parent.appendChild(node) });
+ }
+ else element.outerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+Element._returnOffset = function(l, t) {
+ var result = [l, t];
+ result.left = l;
+ result.top = t;
+ return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+ var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
+ div.innerHTML = t[0] + html + t[1];
+ t[2].times(function() { div = div.firstChild });
+ return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+ before: {
+ adjacency: 'beforeBegin',
+ insert: function(element, node) {
+ element.parentNode.insertBefore(node, element);
+ },
+ initializeRange: function(element, range) {
+ range.setStartBefore(element);
+ }
},
+ top: {
+ adjacency: 'afterBegin',
+ insert: function(element, node) {
+ element.insertBefore(node, element.firstChild);
+ },
+ initializeRange: function(element, range) {
+ range.selectNodeContents(element);
+ range.collapse(true);
+ }
+ },
+ bottom: {
+ adjacency: 'beforeEnd',
+ insert: function(element, node) {
+ element.appendChild(node);
+ }
+ },
+ after: {
+ adjacency: 'afterEnd',
+ insert: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
+ },
+ initializeRange: function(element, range) {
+ range.setStartAfter(element);
+ }
+ },
+ tags: {
+ TABLE: ['<table>', '</table>', 1],
+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+ SELECT: ['<select>', '</select>', 1]
+ }
+};
- contentFromAnonymousTable: function() {
- var div = document.createElement('div');
- div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
- return $A(div.childNodes[0].childNodes[0].childNodes);
+(function() {
+ this.bottom.initializeRange = this.top.initializeRange;
+ Object.extend(this.tags, {
+ THEAD: this.tags.TBODY,
+ TFOOT: this.tags.TBODY,
+ TH: this.tags.TD
+ });
+}).call(Element._insertionTranslations);
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ attribute = Element._attributeTranslations.has[attribute] || attribute;
+ var node = $(element).getAttributeNode(attribute);
+ return node && node.specified;
}
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+ window.HTMLElement = { };
+ window.HTMLElement.prototype = document.createElement('div').__proto__;
+ Prototype.BrowserFeatures.ElementExtensions = true;
}
-var Insertion = new Object();
+Element.extend = (function() {
+ if (Prototype.BrowserFeatures.SpecificElementExtensions)
+ return Prototype.K;
-Insertion.Before = Class.create();
-Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
- initializeRange: function() {
- this.range.setStartBefore(this.element);
- },
+ var Methods = { }, ByTag = Element.Methods.ByTag;
+
+ var extend = Object.extend(function(element) {
+ if (!element || element._extendedByPrototype ||
+ element.nodeType != 1 || element == window) return element;
+
+ var methods = Object.clone(Methods),
+ tagName = element.tagName, property, value;
+
+ // extend methods for specific tags
+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+ for (property in methods) {
+ value = methods[property];
+ if (Object.isFunction(value) && !(property in element))
+ element[property] = value.methodize();
+ }
+
+ element._extendedByPrototype = Prototype.emptyFunction;
+ return element;
- insertContent: function(fragments) {
- fragments.each((function(fragment) {
- this.element.parentNode.insertBefore(fragment, this.element);
- }).bind(this));
+ }, {
+ refresh: function() {
+ // extend methods for all tags (Safari doesn't need this)
+ if (!Prototype.BrowserFeatures.ElementExtensions) {
+ Object.extend(Methods, Element.Methods);
+ Object.extend(Methods, Element.Methods.Simulated);
+ }
+ }
+ });
+
+ extend.refresh();
+ return extend;
+})();
+
+Element.hasAttribute = function(element, attribute) {
+ if (element.hasAttribute) return element.hasAttribute(attribute);
+ return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
}
-});
-Insertion.Top = Class.create();
-Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
- initializeRange: function() {
- this.range.selectNodeContents(this.element);
- this.range.collapse(true);
- },
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
- insertContent: function(fragments) {
- fragments.reverse(false).each((function(fragment) {
- this.element.insertBefore(fragment, this.element.firstChild);
- }).bind(this));
+ if (!tagName) Object.extend(Element.Methods, methods || { });
+ else {
+ if (Object.isArray(tagName)) tagName.each(extend);
+ else extend(tagName);
}
-});
-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);
- },
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = { };
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
- insertContent: function(fragments) {
- fragments.each((function(fragment) {
- this.element.appendChild(fragment);
- }).bind(this));
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!Object.isFunction(value)) continue;
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = value.methodize();
+ }
}
-});
-Insertion.After = Class.create();
-Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
- initializeRange: function() {
- this.range.setStartAfter(this.element);
- },
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ window[klass] = { };
+ window[klass].prototype = document.createElement(tagName).__proto__;
+ return window[klass];
+ }
- insertContent: function(fragments) {
- fragments.each((function(fragment) {
- this.element.parentNode.insertBefore(fragment,
- this.element.nextSibling);
- }).bind(this));
+ if (F.ElementExtensions) {
+ copy(Element.Methods, HTMLElement.prototype);
+ copy(Element.Methods.Simulated, HTMLElement.prototype, true);
}
-});
-/*--------------------------------------------------------------------------*/
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (Object.isUndefined(klass)) continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
-Element.ClassNames = Class.create();
-Element.ClassNames.prototype = {
- initialize: function(element) {
- this.element = $(element);
- },
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
- _each: function(iterator) {
- this.element.className.split(/\s+/).select(function(name) {
- return name.length > 0;
- })._each(iterator);
- },
+ if (Element.extend.refresh) Element.extend.refresh();
+ Element.cache = { };
+};
- set: function(className) {
- this.element.className = className;
+document.viewport = {
+ getDimensions: function() {
+ var dimensions = { };
+ var B = Prototype.Browser;
+ $w('width height').each(function(d) {
+ var D = d.capitalize();
+ dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :
+ (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];
+ });
+ return dimensions;
},
- add: function(classNameToAdd) {
- if (this.include(classNameToAdd)) return;
- this.set($A(this).concat(classNameToAdd).join(' '));
+ getWidth: function() {
+ return this.getDimensions().width;
},
- remove: function(classNameToRemove) {
- if (!this.include(classNameToRemove)) return;
- this.set($A(this).without(classNameToRemove).join(' '));
+ getHeight: function() {
+ return this.getDimensions().height;
},
- toString: function() {
- return $A(this).join(' ');
+ getScrollOffsets: function() {
+ return Element._returnOffset(
+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
}
};
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license. Please see http://www.yui-ext.com/ for more information. */
-Object.extend(Element.ClassNames.prototype, Enumerable);
-var Selector = Class.create();
-Selector.prototype = {
+var Selector = Class.create({
initialize: function(expression) {
- this.params = {classNames: []};
- this.expression = expression.toString().strip();
- this.parseExpression();
+ this.expression = expression.strip();
this.compileMatcher();
},
- parseExpression: function() {
- function abort(message) { throw 'Parse error in selector: ' + message; }
+ shouldUseXPath: function() {
+ if (!Prototype.BrowserFeatures.XPath) return false;
- if (this.expression == '') abort('empty expression');
+ var e = this.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];
- }
+ // Safari 3 chokes on :*-of-type and :empty
+ if (Prototype.Browser.WebKit &&
+ (e.include("-of-type") || e.include(":empty")))
+ return false;
+
+ // XPath can't do namespaced attributes, nor can it read
+ // the "checked" property from DOM nodes
+ if ((/(\[[\w-]*?:|:checked)/).test(this.expression))
+ return false;
- if (expr == '*') return this.params.wildcard = true;
+ return true;
+ },
+
+ compileMatcher: function() {
+ if (this.shouldUseXPath())
+ return this.compileXPathMatcher();
- 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());
+ var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+ c = Selector.criteria, le, p, m;
+
+ if (Selector._cache[e]) {
+ this.matcher = Selector._cache[e];
+ return;
+ }
+
+ this.matcher = ["this.matcher = function(root) {",
+ "var r = root, h = Selector.handlers, c = false, n;"];
+
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
+ new Template(c[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
+ }
}
- expr = rest;
}
- if (expr.length > 0) abort(expr.inspect());
+ this.matcher.push("return h.unique(n);\n}");
+ eval(this.matcher.join('\n'));
+ Selector._cache[this.expression] = this.matcher;
},
- buildMatchExpression: function() {
- var params = this.params, conditions = [], clause;
+ compileXPathMatcher: function() {
+ var e = this.expression, ps = Selector.patterns,
+ x = Selector.xpath, le, m;
- 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() + ')';
- }
+ if (Selector._cache[e]) {
+ this.xpath = Selector._cache[e]; return;
+ }
- 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';
+ this.matcher = ['.//*'];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ if (m = e.match(ps[i])) {
+ this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
+ new Template(x[i]).evaluate(m));
+ e = e.replace(m[0], '');
+ break;
}
- });
+ }
}
- return conditions.join(' && ');
+ this.xpath = this.matcher.join('');
+ Selector._cache[this.expression] = this.xpath;
},
- compileMatcher: function() {
- this.match = new Function('element', 'if (!element.tagName) return false; \
- element = $(element); \
- return ' + this.buildMatchExpression());
+ findElements: function(root) {
+ root = root || document;
+ if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+ return this.matcher(root);
},
- findElements: function(scope) {
- var element;
+ match: function(element) {
+ this.tokens = [];
- if (element = $(this.params.id))
- if (this.match(element))
- if (!scope || Element.childOf(element, scope))
- return [element];
+ var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
+ var le, p, m;
- scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+ while (e && le !== e && (/\S/).test(e)) {
+ le = e;
+ for (var i in ps) {
+ p = ps[i];
+ if (m = e.match(p)) {
+ // use the Selector.assertions methods unless the selector
+ // is too complex.
+ if (as[i]) {
+ this.tokens.push([i, Object.clone(m)]);
+ e = e.replace(m[0], '');
+ } else {
+ // reluctantly do a document-wide search
+ // and look for a match in the array
+ return this.findElements(document).include(element);
+ }
+ }
+ }
+ }
- var results = [];
- for (var i = 0, length = scope.length; i < length; i++)
- if (this.match(element = scope[i]))
- results.push(Element.extend(element));
+ var match = true, name, matches;
+ for (var i = 0, token; token = this.tokens[i]; i++) {
+ name = token[0], matches = token[1];
+ if (!Selector.assertions[name](element, matches)) {
+ match = false; break;
+ }
+ }
- return results;
+ return match;
},
toString: function() {
return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector:" + this.expression.inspect() + ">";
}
-}
+});
Object.extend(Selector, {
+ _cache: { },
+
+ xpath: {
+ descendant: "//*",
+ child: "/*",
+ adjacent: "/following-sibling::*[1]",
+ laterSibling: '/following-sibling::*',
+ tagName: function(m) {
+ if (m[1] == '*') return '';
+ return "[local-name()='" + m[1].toLowerCase() +
+ "' or local-name()='" + m[1].toUpperCase() + "']";
+ },
+ className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+ id: "[@id='#{1}']",
+ attrPresence: function(m) {
+ m[1] = m[1].toLowerCase();
+ return new Template("[@#{1}]").evaluate(m);
+ },
+ attr: function(m) {
+ m[1] = m[1].toLowerCase();
+ m[3] = m[5] || m[6];
+ return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+ },
+ pseudo: function(m) {
+ var h = Selector.xpath.pseudos[m[1]];
+ if (!h) return '';
+ if (Object.isFunction(h)) return h(m);
+ return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+ },
+ operators: {
+ '=': "[@#{1}='#{3}']",
+ '!=': "[@#{1}!='#{3}']",
+ '^=': "[starts-with(@#{1}, '#{3}')]",
+ '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+ '*=': "[contains(@#{1}, '#{3}')]",
+ '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+ '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+ },
+ pseudos: {
+ 'first-child': '[not(preceding-sibling::*)]',
+ 'last-child': '[not(following-sibling::*)]',
+ 'only-child': '[not(preceding-sibling::* or following-sibling::*)]',
+ 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+ 'checked': "[@checked]",
+ 'disabled': "[@disabled]",
+ 'enabled': "[not(@disabled)]",
+ 'not': function(m) {
+ var e = m[6], p = Selector.patterns,
+ x = Selector.xpath, le, v;
+
+ var exclusion = [];
+ while (e && le != e && (/\S/).test(e)) {
+ le = e;
+ for (var i in p) {
+ if (m = e.match(p[i])) {
+ v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
+ exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+ e = e.replace(m[0], '');
+ break;
+ }
+ }
+ }
+ return "[not(" + exclusion.join(" and ") + ")]";
+ },
+ 'nth-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+ },
+ 'nth-last-child': function(m) {
+ return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+ },
+ 'nth-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("position() ", m);
+ },
+ 'nth-last-of-type': function(m) {
+ return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+ },
+ 'first-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+ },
+ 'last-of-type': function(m) {
+ m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+ },
+ 'only-of-type': function(m) {
+ var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+ },
+ nth: function(fragment, m) {
+ var mm, formula = m[6], predicate;
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ if (mm = formula.match(/^(\d+)$/)) // digit only
+ return '[' + fragment + "= " + mm[1] + ']';
+ if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (mm[1] == "-") mm[1] = -1;
+ var a = mm[1] ? Number(mm[1]) : 1;
+ var b = mm[2] ? Number(mm[2]) : 0;
+ predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+ "((#{fragment} - #{b}) div #{a} >= 0)]";
+ return new Template(predicate).evaluate({
+ fragment: fragment, a: a, b: b });
+ }
+ }
+ }
+ },
+
+ criteria: {
+ tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;',
+ className: 'n = h.className(n, r, "#{1}", c); c = false;',
+ id: 'n = h.id(n, r, "#{1}", c); c = false;',
+ attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+ attr: function(m) {
+ m[3] = (m[5] || m[6]);
+ return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+ },
+ pseudo: function(m) {
+ if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+ return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+ },
+ descendant: 'c = "descendant";',
+ child: 'c = "child";',
+ adjacent: 'c = "adjacent";',
+ laterSibling: 'c = "laterSibling";'
+ },
+
+ patterns: {
+ // combinators must be listed first
+ // (and descendant needs to be last combinator)
+ laterSibling: /^\s*~\s*/,
+ child: /^\s*>\s*/,
+ adjacent: /^\s*\+\s*/,
+ descendant: /^\s/,
+
+ // selectors follow
+ tagName: /^\s*(\*|[\w\-]+)(\b|$)?/,
+ id: /^#([\w\-\*]+)(\b|$)/,
+ className: /^\.([\w\-\*]+)(\b|$)/,
+ pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/,
+ attrPresence: /^\[([\w]+)\]/,
+ attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
+ },
+
+ // for Selector.match and Element#match
+ assertions: {
+ tagName: function(element, matches) {
+ return matches[1].toUpperCase() == element.tagName.toUpperCase();
+ },
+
+ className: function(element, matches) {
+ return Element.hasClassName(element, matches[1]);
+ },
+
+ id: function(element, matches) {
+ return element.id === matches[1];
+ },
+
+ attrPresence: function(element, matches) {
+ return Element.hasAttribute(element, matches[1]);
+ },
+
+ attr: function(element, matches) {
+ var nodeValue = Element.readAttribute(element, matches[1]);
+ return Selector.operators[matches[2]](nodeValue, matches[3]);
+ }
+ },
+
+ handlers: {
+ // UTILITY FUNCTIONS
+ // joins two collections
+ concat: function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ a.push(node);
+ return a;
+ },
+
+ // marks an array of nodes for counting
+ mark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = true;
+ return nodes;
+ },
+
+ unmark: function(nodes) {
+ for (var i = 0, node; node = nodes[i]; i++)
+ node._counted = undefined;
+ return nodes;
+ },
+
+ // mark each child node with its position (for nth calls)
+ // "ofType" flag indicates whether we're indexing for nth-of-type
+ // rather than nth-child
+ index: function(parentNode, reverse, ofType) {
+ parentNode._counted = true;
+ if (reverse) {
+ for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+ var node = nodes[i];
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ } else {
+ for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+ if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+ }
+ },
+
+ // filters out duplicates and extends all nodes
+ unique: function(nodes) {
+ if (nodes.length == 0) return nodes;
+ var results = [], n;
+ for (var i = 0, l = nodes.length; i < l; i++)
+ if (!(n = nodes[i])._counted) {
+ n._counted = true;
+ results.push(Element.extend(n));
+ }
+ return Selector.handlers.unmark(results);
+ },
+
+ // COMBINATOR FUNCTIONS
+ descendant: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName('*'));
+ return results;
+ },
+
+ child: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ for (var j = 0, child; child = node.childNodes[j]; j++)
+ if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+ }
+ return results;
+ },
+
+ adjacent: function(nodes) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ var next = this.nextElementSibling(node);
+ if (next) results.push(next);
+ }
+ return results;
+ },
+
+ laterSibling: function(nodes) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ h.concat(results, Element.nextSiblings(node));
+ return results;
+ },
+
+ nextElementSibling: function(node) {
+ while (node = node.nextSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ previousElementSibling: function(node) {
+ while (node = node.previousSibling)
+ if (node.nodeType == 1) return node;
+ return null;
+ },
+
+ // TOKEN FUNCTIONS
+ tagName: function(nodes, root, tagName, combinator) {
+ tagName = tagName.toUpperCase();
+ var results = [], h = Selector.handlers;
+ if (nodes) {
+ if (combinator) {
+ // fastlane for ordinary descendant combinators
+ if (combinator == "descendant") {
+ for (var i = 0, node; node = nodes[i]; i++)
+ h.concat(results, node.getElementsByTagName(tagName));
+ return results;
+ } else nodes = this[combinator](nodes);
+ if (tagName == "*") return nodes;
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName.toUpperCase() == tagName) results.push(node);
+ return results;
+ } else return root.getElementsByTagName(tagName);
+ },
+
+ id: function(nodes, root, id, combinator) {
+ var targetNode = $(id), h = Selector.handlers;
+ if (!targetNode) return [];
+ if (!nodes && root == document) return [targetNode];
+ if (nodes) {
+ if (combinator) {
+ if (combinator == 'child') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (targetNode.parentNode == node) return [targetNode];
+ } else if (combinator == 'descendant') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.descendantOf(targetNode, node)) return [targetNode];
+ } else if (combinator == 'adjacent') {
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Selector.handlers.previousElementSibling(targetNode) == node)
+ return [targetNode];
+ } else nodes = h[combinator](nodes);
+ }
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node == targetNode) return [targetNode];
+ return [];
+ }
+ return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+ },
+
+ className: function(nodes, root, className, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ return Selector.handlers.byClassName(nodes, root, className);
+ },
+
+ byClassName: function(nodes, root, className) {
+ if (!nodes) nodes = Selector.handlers.descendant([root]);
+ var needle = ' ' + className + ' ';
+ for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+ nodeClassName = node.className;
+ if (nodeClassName.length == 0) continue;
+ if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+ results.push(node);
+ }
+ return results;
+ },
+
+ attrPresence: function(nodes, root, attr) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (Element.hasAttribute(node, attr)) results.push(node);
+ return results;
+ },
+
+ attr: function(nodes, root, attr, value, operator) {
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ var handler = Selector.operators[operator], results = [];
+ for (var i = 0, node; node = nodes[i]; i++) {
+ var nodeValue = Element.readAttribute(node, attr);
+ if (nodeValue === null) continue;
+ if (handler(nodeValue, value)) results.push(node);
+ }
+ return results;
+ },
+
+ pseudo: function(nodes, name, value, root, combinator) {
+ if (nodes && combinator) nodes = this[combinator](nodes);
+ if (!nodes) nodes = root.getElementsByTagName("*");
+ return Selector.pseudos[name](nodes, value, root);
+ }
+ },
+
+ pseudos: {
+ 'first-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.previousElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'last-child': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ if (Selector.handlers.nextElementSibling(node)) continue;
+ results.push(node);
+ }
+ return results;
+ },
+ 'only-child': function(nodes, value, root) {
+ var h = Selector.handlers;
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+ results.push(node);
+ return results;
+ },
+ 'nth-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root);
+ },
+ 'nth-last-child': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true);
+ },
+ 'nth-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, false, true);
+ },
+ 'nth-last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, formula, root, true, true);
+ },
+ 'first-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, false, true);
+ },
+ 'last-of-type': function(nodes, formula, root) {
+ return Selector.pseudos.nth(nodes, "1", root, true, true);
+ },
+ 'only-of-type': function(nodes, formula, root) {
+ var p = Selector.pseudos;
+ return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+ },
+
+ // handles the an+b logic
+ getIndices: function(a, b, total) {
+ if (a == 0) return b > 0 ? [b] : [];
+ return $R(1, total).inject([], function(memo, i) {
+ if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+ return memo;
+ });
+ },
+
+ // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+ nth: function(nodes, formula, root, reverse, ofType) {
+ if (nodes.length == 0) return [];
+ if (formula == 'even') formula = '2n+0';
+ if (formula == 'odd') formula = '2n+1';
+ var h = Selector.handlers, results = [], indexed = [], m;
+ h.mark(nodes);
+ for (var i = 0, node; node = nodes[i]; i++) {
+ if (!node.parentNode._counted) {
+ h.index(node.parentNode, reverse, ofType);
+ indexed.push(node.parentNode);
+ }
+ }
+ if (formula.match(/^\d+$/)) { // just a number
+ formula = Number(formula);
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.nodeIndex == formula) results.push(node);
+ } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+ if (m[1] == "-") m[1] = -1;
+ var a = m[1] ? Number(m[1]) : 1;
+ var b = m[2] ? Number(m[2]) : 0;
+ var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+ for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+ for (var j = 0; j < l; j++)
+ if (node.nodeIndex == indices[j]) results.push(node);
+ }
+ }
+ h.unmark(nodes);
+ h.unmark(indexed);
+ return results;
+ },
+
+ 'empty': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++) {
+ // IE treats comments as element nodes
+ if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+ results.push(node);
+ }
+ return results;
+ },
+
+ 'not': function(nodes, selector, root) {
+ var h = Selector.handlers, selectorType, m;
+ var exclusions = new Selector(selector).findElements(root);
+ h.mark(exclusions);
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node._counted) results.push(node);
+ h.unmark(exclusions);
+ return results;
+ },
+
+ 'enabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (!node.disabled) results.push(node);
+ return results;
+ },
+
+ 'disabled': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.disabled) results.push(node);
+ return results;
+ },
+
+ 'checked': function(nodes, value, root) {
+ for (var i = 0, results = [], node; node = nodes[i]; i++)
+ if (node.checked) results.push(node);
+ return results;
+ }
+ },
+
+ operators: {
+ '=': function(nv, v) { return nv == v; },
+ '!=': function(nv, v) { return nv != v; },
+ '^=': function(nv, v) { return nv.startsWith(v); },
+ '$=': function(nv, v) { return nv.endsWith(v); },
+ '*=': function(nv, v) { return nv.include(v); },
+ '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+ '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+ },
+
matchElements: function(elements, expression) {
- var selector = new Selector(expression);
- return elements.select(selector.match.bind(selector)).map(Element.extend);
+ var matches = new Selector(expression).findElements(), h = Selector.handlers;
+ h.mark(matches);
+ for (var i = 0, results = [], element; element = elements[i]; i++)
+ if (element._counted) results.push(element);
+ h.unmark(matches);
+ return results;
},
findElement: function(elements, expression, index) {
- if (typeof expression == 'number') index = expression, expression = false;
+ if (Object.isNumber(expression)) {
+ 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();
+ var exprs = expressions.join(',');
+ expressions = [];
+ exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+ expressions.push(m[1].strip());
+ });
+ var results = [], h = Selector.handlers;
+ for (var i = 0, l = expressions.length, selector; i < l; i++) {
+ selector = new Selector(expressions[i].strip());
+ h.concat(results, selector.findElements(element));
+ }
+ return (l > 1) ? h.unique(results) : results;
}
});
+if (Prototype.Browser.IE) {
+ // IE returns comment nodes on getElementsByTagName("*").
+ // Filter them out.
+ Selector.handlers.concat = function(a, b) {
+ for (var i = 0, node; node = b[i]; i++)
+ if (node.tagName !== "!") a.push(node);
+ return a;
+ };
+}
+
function $$() {
return Selector.findChildElements(document, $A(arguments));
}
@@ -1876,13 +3384,19 @@ var Form = {
return form;
},
- serializeElements: function(elements, getHash) {
- var data = elements.inject({}, function(result, element) {
+ serializeElements: function(elements, options) {
+ if (typeof options != 'object') options = { hash: !!options };
+ else if (Object.isUndefined(options.hash)) options.hash = true;
+ var key, value, submitted = false, submit = options.submit;
+
+ 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]];
+ key = element.name; value = $(element).getValue();
+ if (value != null && (element.type != 'submit' || (!submitted &&
+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
+ if (key in result) {
+ // a key is already present; construct an array of values
+ if (!Object.isArray(result[key])) result[key] = [result[key]];
result[key].push(value);
}
else result[key] = value;
@@ -1891,13 +3405,13 @@ var Form = {
return result;
});
- return getHash ? data : Hash.toQueryString(data);
+ return options.hash ? data : Object.toQueryString(data);
}
};
Form.Methods = {
- serialize: function(form, getHash) {
- return Form.serializeElements(Form.getElements(form), getHash);
+ serialize: function(form, options) {
+ return Form.serializeElements(Form.getElements(form), options);
},
getElements: function(form) {
@@ -1928,25 +3442,26 @@ Form.Methods = {
disable: function(form) {
form = $(form);
- form.getElements().each(function(element) {
- element.blur();
- element.disabled = 'true';
- });
+ Form.getElements(form).invoke('disable');
return form;
},
enable: function(form) {
form = $(form);
- form.getElements().each(function(element) {
- element.disabled = '';
- });
+ Form.getElements(form).invoke('enable');
return form;
},
findFirstElement: function(form) {
- return $(form).getElements().find(function(element) {
- return element.type != 'hidden' && !element.disabled &&
- ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+ var elements = $(form).getElements().findAll(function(element) {
+ return 'hidden' != element.type && !element.disabled;
+ });
+ var firstByIndex = elements.findAll(function(element) {
+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+ }).sortBy(function(element) { return element.tabIndex }).first();
+
+ return firstByIndex ? firstByIndex : elements.find(function(element) {
+ return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
});
},
@@ -1954,10 +3469,26 @@ Form.Methods = {
form = $(form);
form.findFirstElement().activate();
return form;
- }
-}
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || { });
-Object.extend(Form, Form.Methods);
+ var params = options.parameters, action = form.readAttribute('action') || '';
+ if (action.blank()) action = window.location.href;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (Object.isString(params)) params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(action, options);
+ }
+};
/*--------------------------------------------------------------------------*/
@@ -1971,7 +3502,7 @@ Form.Element = {
$(element).select();
return element;
}
-}
+};
Form.Element.Methods = {
serialize: function(element) {
@@ -1979,9 +3510,9 @@ Form.Element.Methods = {
if (!element.disabled && element.name) {
var value = element.getValue();
if (value != undefined) {
- var pair = {};
+ var pair = { };
pair[element.name] = value;
- return Hash.toQueryString(pair);
+ return Object.toQueryString(pair);
}
}
return '';
@@ -1993,6 +3524,13 @@ Form.Element.Methods = {
return Form.Element.Serializers[method](element);
},
+ setValue: function(element, value) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ Form.Element.Serializers[method](element, value);
+ return element;
+ },
+
clear: function(element) {
$(element).value = '';
return element;
@@ -2004,55 +3542,75 @@ Form.Element.Methods = {
activate: function(element) {
element = $(element);
- element.focus();
- if (element.select && ( element.tagName.toLowerCase() != 'input' ||
- !['button', 'reset', 'submit'].include(element.type) ) )
- element.select();
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !['button', 'reset', 'submit'].include(element.type)))
+ element.select();
+ } catch (e) { }
return element;
},
disable: function(element) {
element = $(element);
+ element.blur();
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;
+var $F = Form.Element.Methods.getValue;
/*--------------------------------------------------------------------------*/
Form.Element.Serializers = {
- input: function(element) {
+ input: function(element, value) {
switch (element.type.toLowerCase()) {
case 'checkbox':
case 'radio':
- return Form.Element.Serializers.inputSelector(element);
+ return Form.Element.Serializers.inputSelector(element, value);
default:
- return Form.Element.Serializers.textarea(element);
+ return Form.Element.Serializers.textarea(element, value);
}
},
- inputSelector: function(element) {
- return element.checked ? element.value : null;
+ inputSelector: function(element, value) {
+ if (Object.isUndefined(value)) return element.checked ? element.value : null;
+ else element.checked = !!value;
},
- textarea: function(element) {
- return element.value;
+ textarea: function(element, value) {
+ if (Object.isUndefined(value)) return element.value;
+ else element.value = value;
},
- select: function(element) {
- return this[element.type == 'select-one' ?
- 'selectOne' : 'selectMany'](element);
+ select: function(element, index) {
+ if (Object.isUndefined(index))
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ else {
+ var opt, value, single = !Object.isArray(index);
+ for (var i = 0, length = element.length; i < length; i++) {
+ opt = element.options[i];
+ value = this.optionValue(opt);
+ if (single) {
+ if (value == index) {
+ opt.selected = true;
+ return;
+ }
+ }
+ else opt.selected = index.include(value);
+ }
+ }
},
selectOne: function(element) {
@@ -2075,45 +3633,34 @@ Form.Element.Serializers = {
// 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;
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+ initialize: function($super, element, frequency, callback) {
+ $super(callback, 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() {
+ execute: function() {
var value = this.getValue();
- var changed = ('string' == typeof this.lastValue && 'string' == typeof value
- ? this.lastValue != value : String(this.lastValue) != String(value));
- if (changed) {
+ if (Object.isString(this.lastValue) && Object.isString(value) ?
+ this.lastValue != value : String(this.lastValue) != String(value)) {
this.callback(this.element, value);
this.lastValue = value;
}
}
-}
+});
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
getValue: function() {
return Form.Element.getValue(this.element);
}
});
-Form.Observer = Class.create();
-Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+Form.Observer = Class.create(Abstract.TimedObserver, {
getValue: function() {
return Form.serialize(this.element);
}
@@ -2121,8 +3668,7 @@ Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
/*--------------------------------------------------------------------------*/
-Abstract.EventObserver = function() {}
-Abstract.EventObserver.prototype = {
+Abstract.EventObserver = Class.create({
initialize: function(element, callback) {
this.element = $(element);
this.callback = callback;
@@ -2143,7 +3689,7 @@ Abstract.EventObserver.prototype = {
},
registerFormCallbacks: function() {
- Form.getElements(this.element).each(this.registerCallback.bind(this));
+ Form.getElements(this.element).each(this.registerCallback, this);
},
registerCallback: function(element) {
@@ -2159,24 +3705,20 @@ Abstract.EventObserver.prototype = {
}
}
}
-}
+});
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
getValue: function() {
return Form.Element.getValue(this.element);
}
});
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+Form.EventObserver = Class.create(Abstract.EventObserver, {
getValue: function() {
return Form.serialize(this.element);
}
});
-if (!window.Event) {
- var Event = new Object();
-}
+if (!window.Event) var Event = { };
Object.extend(Event, {
KEY_BACKSPACE: 8,
@@ -2192,102 +3734,337 @@ Object.extend(Event, {
KEY_END: 35,
KEY_PAGEUP: 33,
KEY_PAGEDOWN: 34,
+ KEY_INSERT: 45,
- element: function(event) {
- return event.target || event.srcElement;
- },
+ cache: { },
- isLeftClick: function(event) {
- return (((event.which) && (event.which == 1)) ||
- ((event.button) && (event.button == 1)));
- },
+ relatedTarget: function(event) {
+ var element;
+ switch(event.type) {
+ case 'mouseover': element = event.fromElement; break;
+ case 'mouseout': element = event.toElement; break;
+ default: return null;
+ }
+ return Element.extend(element);
+ }
+});
- pointerX: function(event) {
- return event.pageX || (event.clientX +
- (document.documentElement.scrollLeft || document.body.scrollLeft));
- },
+Event.Methods = (function() {
+ var isButton;
- pointerY: function(event) {
- return event.pageY || (event.clientY +
- (document.documentElement.scrollTop || document.body.scrollTop));
- },
+ if (Prototype.Browser.IE) {
+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
+ isButton = function(event, code) {
+ return event.button == buttonMap[code];
+ };
+
+ } else if (Prototype.Browser.WebKit) {
+ isButton = function(event, code) {
+ switch (code) {
+ case 0: return event.which == 1 && !event.metaKey;
+ case 1: return event.which == 1 && event.metaKey;
+ default: return false;
+ }
+ };
+
+ } else {
+ isButton = function(event, code) {
+ return event.which ? (event.which === code + 1) : (event.button === code);
+ };
+ }
- stop: function(event) {
- if (event.preventDefault) {
+ return {
+ isLeftClick: function(event) { return isButton(event, 0) },
+ isMiddleClick: function(event) { return isButton(event, 1) },
+ isRightClick: function(event) { return isButton(event, 2) },
+
+ element: function(event) {
+ var node = Event.extend(event).target;
+ return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);
+ },
+
+ findElement: function(event, expression) {
+ var element = Event.element(event);
+ if (!expression) return element;
+ var elements = [element].concat(element.ancestors());
+ return Selector.findElement(elements, expression, 0);
+ },
+
+ pointer: function(event) {
+ return {
+ x: event.pageX || (event.clientX +
+ (document.documentElement.scrollLeft || document.body.scrollLeft)),
+ y: event.pageY || (event.clientY +
+ (document.documentElement.scrollTop || document.body.scrollTop))
+ };
+ },
+
+ pointerX: function(event) { return Event.pointer(event).x },
+ pointerY: function(event) { return Event.pointer(event).y },
+
+ stop: function(event) {
+ Event.extend(event);
event.preventDefault();
event.stopPropagation();
- } else {
- event.returnValue = false;
- event.cancelBubble = true;
+ event.stopped = 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;
- },
+Event.extend = (function() {
+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+ m[name] = Event.Methods[name].methodize();
+ return m;
+ });
+
+ if (Prototype.Browser.IE) {
+ Object.extend(methods, {
+ stopPropagation: function() { this.cancelBubble = true },
+ preventDefault: function() { this.returnValue = false },
+ inspect: function() { return "[object Event]" }
+ });
+
+ return function(event) {
+ if (!event) return false;
+ if (event._extendedByPrototype) return event;
+
+ event._extendedByPrototype = Prototype.emptyFunction;
+ var pointer = Event.pointer(event);
+ Object.extend(event, {
+ target: event.srcElement,
+ relatedTarget: Event.relatedTarget(event),
+ pageX: pointer.x,
+ pageY: pointer.y
+ });
+ return Object.extend(event, methods);
+ };
- observers: false,
+ } else {
+ Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;
+ Object.extend(Event.prototype, methods);
+ return Prototype.K;
+ }
+})();
+
+Object.extend(Event, (function() {
+ var cache = Event.cache;
+
+ function getEventID(element) {
+ if (element._eventID) return element._eventID;
+ arguments.callee.id = arguments.callee.id || 1;
+ return element._eventID = ++arguments.callee.id;
+ }
+
+ function getDOMEventName(eventName) {
+ if (eventName && eventName.include(':')) return "dataavailable";
+ return eventName;
+ }
+
+ function getCacheForID(id) {
+ return cache[id] = cache[id] || { };
+ }
+
+ function getWrappersForEventName(id, eventName) {
+ var c = getCacheForID(id);
+ return c[eventName] = c[eventName] || [];
+ }
+
+ function createWrapper(element, eventName, handler) {
+ var id = getEventID(element);
+ var c = getWrappersForEventName(id, eventName);
+ if (c.pluck("handler").include(handler)) return false;
+
+ var wrapper = function(event) {
+ if (!Event || !Event.extend ||
+ (event.eventName && event.eventName != eventName))
+ return false;
+
+ Event.extend(event);
+ handler.call(element, event)
+ };
+
+ wrapper.handler = handler;
+ c.push(wrapper);
+ return wrapper;
+ }
- _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);
+ function findWrapper(id, eventName, handler) {
+ var c = getWrappersForEventName(id, eventName);
+ return c.find(function(wrapper) { return wrapper.handler == handler });
+ }
+
+ function destroyWrapper(id, eventName, handler) {
+ var c = getCacheForID(id);
+ if (!c[eventName]) return false;
+ c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
+ }
+
+ function destroyCache() {
+ for (var id in cache)
+ for (var eventName in cache[id])
+ cache[id][eventName] = null;
+ }
+
+ if (window.attachEvent) {
+ window.attachEvent("onunload", destroyCache);
+ }
+
+ return {
+ observe: function(element, eventName, handler) {
+ element = $(element);
+ var name = getDOMEventName(eventName);
+
+ var wrapper = createWrapper(element, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.addEventListener) {
+ element.addEventListener(name, wrapper, false);
+ } else {
+ element.attachEvent("on" + name, wrapper);
+ }
+
+ return element;
+ },
+
+ stopObserving: function(element, eventName, handler) {
+ element = $(element);
+ var id = getEventID(element), name = getDOMEventName(eventName);
+
+ if (!handler && eventName) {
+ getWrappersForEventName(id, eventName).each(function(wrapper) {
+ element.stopObserving(eventName, wrapper.handler);
+ });
+ return element;
+
+ } else if (!eventName) {
+ Object.keys(getCacheForID(id)).each(function(eventName) {
+ element.stopObserving(eventName);
+ });
+ return element;
+ }
+
+ var wrapper = findWrapper(id, eventName, handler);
+ if (!wrapper) return element;
+
+ if (element.removeEventListener) {
+ element.removeEventListener(name, wrapper, false);
+ } else {
+ element.detachEvent("on" + name, wrapper);
+ }
+
+ destroyWrapper(id, eventName, handler);
+
+ return element;
+ },
+
+ fire: function(element, eventName, memo) {
+ element = $(element);
+ if (element == document && document.createEvent && !element.dispatchEvent)
+ element = document.documentElement;
+
+ if (document.createEvent) {
+ var event = document.createEvent("HTMLEvents");
+ event.initEvent("dataavailable", true, true);
+ } else {
+ var event = document.createEventObject();
+ event.eventType = "ondataavailable";
+ }
+
+ event.eventName = eventName;
+ event.memo = memo || { };
+
+ if (document.createEvent) {
+ element.dispatchEvent(event);
+ } else {
+ element.fireEvent(event.eventType, event);
+ }
+
+ return Event.extend(event);
}
- },
+ };
+})());
+
+Object.extend(Event, Event.Methods);
+
+Element.addMethods({
+ fire: Event.fire,
+ observe: Event.observe,
+ stopObserving: Event.stopObserving
+});
+
+Object.extend(document, {
+ fire: Element.Methods.fire.methodize(),
+ observe: Element.Methods.observe.methodize(),
+ stopObserving: Element.Methods.stopObserving.methodize()
+});
- 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;
+(function() {
+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+ Matthias Miller, Dean Edwards and John Resig. */
+
+ var timer, fired = false;
+
+ function fireContentLoadedEvent() {
+ if (fired) return;
+ if (timer) window.clearInterval(timer);
+ document.fire("dom:loaded");
+ fired = true;
+ }
+
+ if (document.addEventListener) {
+ if (Prototype.Browser.WebKit) {
+ timer = window.setInterval(function() {
+ if (/loaded|complete/.test(document.readyState))
+ fireContentLoadedEvent();
+ }, 0);
+
+ Event.observe(window, "load", fireContentLoadedEvent);
+
+ } else {
+ document.addEventListener("DOMContentLoaded",
+ fireContentLoadedEvent, false);
}
- Event.observers = false;
- },
- observe: function(element, name, observer, useCapture) {
- element = $(element);
- useCapture = useCapture || false;
+ } else {
+ document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
+ $("__onDOMContentLoaded").onreadystatechange = function() {
+ if (this.readyState == "complete") {
+ this.onreadystatechange = null;
+ fireContentLoadedEvent();
+ }
+ };
+ }
+})();
+/*------------------------------- DEPRECATED -------------------------------*/
- if (name == 'keypress' &&
- (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
- || element.attachEvent))
- name = 'keydown';
+Hash.toQueryString = Object.toQueryString;
- Event._observeAndCache(element, name, observer, useCapture);
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+ Before: function(element, content) {
+ return Element.insert(element, {before:content});
},
- stopObserving: function(element, name, observer, useCapture) {
- element = $(element);
- useCapture = useCapture || false;
+ Top: function(element, content) {
+ return Element.insert(element, {top:content});
+ },
- if (name == 'keypress' &&
- (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
- || element.detachEvent))
- name = 'keydown';
+ Bottom: function(element, content) {
+ return Element.insert(element, {bottom:content});
+ },
- if (element.removeEventListener) {
- element.removeEventListener(name, observer, useCapture);
- } else if (element.detachEvent) {
- try {
- element.detachEvent('on' + name, observer);
- } catch (e) {}
- }
+ After: function(element, content) {
+ return Element.insert(element, {after:content});
}
-});
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
-/* prevent memory leaks in IE */
-if (navigator.appVersion.match(/\bMSIE\b/))
- Event.observe(window, 'unload', Event.unloadCache, false);
+// This should be moved to script.aculo.us; notice the deprecated methods
+// further below, that map to the newer Element methods.
var Position = {
// set to true if needed, warning: firefox performance problems
// NOT neeeded for page scrolling, only if draggable contained in
@@ -2307,59 +4084,13 @@ var Position = {
|| 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);
+ this.offset = Element.cumulativeOffset(element);
return (y >= this.offset[1] &&
y < this.offset[1] + element.offsetHeight &&
@@ -2368,11 +4099,11 @@ var Position = {
},
withinIncludingScrolloffsets: function(element, x, y) {
- var offsetcache = this.realOffset(element);
+ var offsetcache = Element.cumulativeScrollOffset(element);
this.xcomp = x + offsetcache[0] - this.deltaX;
this.ycomp = y + offsetcache[1] - this.deltaY;
- this.offset = this.cumulativeOffset(element);
+ this.offset = Element.cumulativeOffset(element);
return (this.ycomp >= this.offset[1] &&
this.ycomp < this.offset[1] + element.offsetHeight &&
@@ -2391,125 +4122,104 @@ var Position = {
element.offsetWidth;
},
- page: function(forElement) {
- var valueT = 0, valueL = 0;
+ // Deprecation layer -- use newer Element methods now (1.5.2).
- var element = forElement;
- do {
- valueT += element.offsetTop || 0;
- valueL += element.offsetLeft || 0;
+ cumulativeOffset: Element.Methods.cumulativeOffset,
- // Safari fix
- if (element.offsetParent==document.body)
- if (Element.getStyle(element,'position')=='absolute') break;
+ positionedOffset: Element.Methods.positionedOffset,
- } 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);
+ absolutize: function(element) {
+ Position.prepare();
+ return Element.absolutize(element);
+ },
- return [valueL, valueT];
+ relativize: function(element) {
+ Position.prepare();
+ return Element.relativize(element);
},
- clone: function(source, target) {
- var options = Object.extend({
- setLeft: true,
- setTop: true,
- setWidth: true,
- setHeight: true,
- offsetTop: 0,
- offsetLeft: 0
- }, arguments[2] || {})
+ realOffset: Element.Methods.cumulativeScrollOffset,
- // find page position of source
- source = $(source);
- var p = Position.page(source);
+ offsetParent: Element.Methods.getOffsetParent,
- // 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);
- }
+ page: Element.Methods.viewportOffset,
- // correct by body offsets (fixes Safari)
- if (parent == document.body) {
- delta[0] -= document.body.offsetLeft;
- delta[1] -= document.body.offsetTop;
+ clone: function(source, target, options) {
+ options = options || { };
+ return Element.clonePosition(target, source, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+ function iter(name) {
+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+ }
+
+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+ function(element, className) {
+ className = className.toString().strip();
+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+ } : function(element, className) {
+ className = className.toString().strip();
+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+ if (!classNames && !className) return elements;
+
+ var nodes = $(element).getElementsByTagName('*');
+ className = ' ' + className + ' ';
+
+ for (var i = 0, child, cn; child = nodes[i]; i++) {
+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+ (classNames && classNames.all(function(name) {
+ return !name.toString().blank() && cn.include(' ' + name + ' ');
+ }))))
+ elements.push(Element.extend(child));
}
+ return elements;
+ };
- // 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';
- },
+ return function(className, parentElement) {
+ return $(parentElement || document.body).getElementsByClassName(className);
+ };
+}(Element.Methods);
- 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.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
- 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;
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
- element.style.position = 'absolute';
- element.style.top = top + 'px';
- element.style.left = left + 'px';
- element.style.width = width + 'px';
- element.style.height = height + 'px';
+ set: function(className) {
+ this.element.className = className;
},
- relativize: function(element) {
- element = $(element);
- if (element.style.position == 'relative') return;
- Position.prepare();
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
- element.style.position = 'relative';
- var top = parseFloat(element.style.top || 0) - (element._originalTop || 0);
- var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
- element.style.top = top + 'px';
- element.style.left = left + 'px';
- element.style.height = element._originalHeight;
- element.style.width = element._originalWidth;
+ toString: function() {
+ return $A(this).join(' ');
}
-}
-
-// 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);
+Object.extend(Element.ClassNames.prototype, Enumerable);
- return [valueL, valueT];
- }
-}
+/*--------------------------------------------------------------------------*/
Element.addMethods(); \ No newline at end of file
diff --git a/groups/public/stylesheets/application.css b/groups/public/stylesheets/application.css
index 8169beb49..ab6c83c29 100644
--- a/groups/public/stylesheets/application.css
+++ b/groups/public/stylesheets/application.css
@@ -75,12 +75,12 @@ a, a:link, a:visited{ color: #2A5685; text-decoration: none; }
a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
a img{ border: 0; }
-a.issue.closed, .issue.closed a { text-decoration: line-through; }
+a.issue.closed { text-decoration: line-through; }
/***** Tables *****/
table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
-table.list td { overflow: hidden; vertical-align: top;}
+table.list td { vertical-align: top; }
table.list td.id { width: 2%; text-align: center;}
table.list td.checkbox { width: 15px; padding: 0px;}
@@ -97,6 +97,10 @@ tr.entry td.size { text-align: right; font-size: 90%; }
tr.entry td.revision, tr.entry td.author { text-align: center; }
tr.entry td.age { text-align: right; }
+tr.entry span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
+tr.entry.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
+tr.entry.file td.filename a { margin-left: 16px; }
+
tr.changeset td.author { text-align: center; width: 15%; }
tr.changeset td.committed_on { text-align: center; width: 15%; }
@@ -160,6 +164,8 @@ input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
fieldset {border: 1px solid #e4e4e4; margin:0;}
legend {color: #484848;}
hr { width: 100%; height: 1px; background: #ccc; border: 0;}
+blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
+blockquote blockquote { margin-left: 0;}
textarea.wiki-edit { width: 99%; }
li p {margin-top: 0;}
div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
@@ -179,22 +185,32 @@ div#issue-changesets .changeset { padding: 4px;}
div#issue-changesets .changeset { border-bottom: 1px solid #ddd; }
div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
-div#activity dl { margin-left: 2em; }
-div#activity dd { margin-bottom: 1em; padding-left: 18px; }
-div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
+div#activity dl, #search-results { margin-left: 2em; }
+div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
+div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
+div#activity dt.me .time { border-bottom: 1px solid #999; }
div#activity dt .time { color: #777; font-size: 80%; }
-div#activity dd .description { font-style: italic; }
-div#activity span.project:after { content: " -"; }
-div#activity dt.issue { background-image: url(../images/ticket.png); }
-div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
-div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
-div#activity dt.changeset { background-image: url(../images/changeset.png); }
-div#activity dt.news { background-image: url(../images/news.png); }
-div#activity dt.message { background-image: url(../images/message.png); }
-div#activity dt.reply { background-image: url(../images/comments.png); }
-div#activity dt.wiki-page { background-image: url(../images/wiki_edit.png); }
-div#activity dt.attachment { background-image: url(../images/attachment.png); }
-div#activity dt.document { background-image: url(../images/document.png); }
+div#activity dd .description, #search-results dd .description { font-style: italic; }
+div#activity span.project:after, #search-results span.project:after { content: " -"; }
+div#activity dd span.description, #search-results dd span.description { display:block; }
+
+#search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
+div#search-results-counts {float:right;}
+div#search-results-counts ul { margin-top: 0.5em; }
+div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
+
+dt.issue { background-image: url(../images/ticket.png); }
+dt.issue-edit { background-image: url(../images/ticket_edit.png); }
+dt.issue-closed { background-image: url(../images/ticket_checked.png); }
+dt.issue-note { background-image: url(../images/ticket_note.png); }
+dt.changeset { background-image: url(../images/changeset.png); }
+dt.news { background-image: url(../images/news.png); }
+dt.message { background-image: url(../images/message.png); }
+dt.reply { background-image: url(../images/comments.png); }
+dt.wiki-page { background-image: url(../images/wiki_edit.png); }
+dt.attachment { background-image: url(../images/attachment.png); }
+dt.document { background-image: url(../images/document.png); }
+dt.project { background-image: url(../images/projects.png); }
div#roadmap fieldset.related-issues { margin-bottom: 1em; }
div#roadmap fieldset.related-issues ul { margin-top: 0.3em; margin-bottom: 0.3em; }
@@ -212,6 +228,10 @@ table#time-report tbody tr.last-level { font-style: normal; color: #555; }
table#time-report tbody tr.total { font-style: normal; font-weight: bold; color: #555; background-color:#EEEEEE; }
table#time-report .hours-dec { font-size: 0.9em; }
+ul.properties {padding:0; font-size: 0.9em; color: #777;}
+ul.properties li {list-style-type:none;}
+ul.properties li span {font-style:italic;}
+
.total-hours { font-size: 110%; font-weight: bold; }
.total-hours span.hours-int { font-size: 120%; }
@@ -230,6 +250,8 @@ height: 1%;
clear:left;
}
+html>body .tabular p {overflow:hidden;}
+
.tabular label{
font-weight: bold;
float: left;
@@ -246,6 +268,8 @@ text-align: left;
width: 200px;
}
+input#time_entry_comments { width: 90%;}
+
#preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
.tabular.settings p{ padding-left: 300px; }
@@ -444,31 +468,35 @@ div.wiki pre {
overflow-x: auto;
}
-div.wiki div.toc {
+div.wiki ul.toc {
background-color: #ffffdd;
border: 1px solid #e4e4e4;
padding: 4px;
line-height: 1.2em;
margin-bottom: 12px;
margin-right: 12px;
+ margin-left: 0;
display: table
}
-* html div.wiki div.toc { width: 50%; } /* IE6 doesn't autosize div */
+* html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
-div.wiki div.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
-div.wiki div.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
+div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
+div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
+div.wiki ul.toc li { list-style-type:none;}
+div.wiki ul.toc li.heading2 { margin-left: 6px; }
+div.wiki ul.toc li.heading3 { margin-left: 12px; font-size: 0.8em; }
-div.wiki div.toc a {
- display: block;
+div.wiki ul.toc a {
font-size: 0.9em;
font-weight: normal;
text-decoration: none;
color: #606060;
}
-div.wiki div.toc a:hover { color: #c61a1a; text-decoration: underline;}
+div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
-div.wiki div.toc a.heading2 { margin-left: 6px; }
-div.wiki div.toc a.heading3 { margin-left: 12px; font-size: 0.8em; }
+a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
+a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
+h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
/***** My page layout *****/
.block-receiver {
@@ -578,6 +606,7 @@ vertical-align: middle;
.icon-checked { background-image: url(../images/true.png); }
.icon-details { background-image: url(../images/zoom_in.png); }
.icon-report { background-image: url(../images/report.png); }
+.icon-comment { background-image: url(../images/comment.png); }
.icon22-projects { background-image: url(../images/22x22/projects.png); }
.icon22-users { background-image: url(../images/22x22/users.png); }
diff --git a/groups/public/stylesheets/context_menu.css b/groups/public/stylesheets/context_menu.css
index e5a83be0d..b3aa1aca0 100644
--- a/groups/public/stylesheets/context_menu.css
+++ b/groups/public/stylesheets/context_menu.css
@@ -1,4 +1,4 @@
-#context-menu { position: absolute; z-index: 10;}
+#context-menu { position: absolute; z-index: 40; font-size: 0.9em;}
#context-menu ul, #context-menu li, #context-menu a {
display:block;
@@ -20,7 +20,7 @@
#context-menu li {
position:relative;
padding:1px;
- z-index:9;
+ z-index:39;
}
#context-menu li.folder ul { position:absolute; left:168px; /* IE6 */ top:-2px; }
#context-menu li.folder>ul { left:148px; }
@@ -31,10 +31,10 @@
#context-menu a {
border:1px solid white;
- text-decoration:none;
+ text-decoration:none !important;
background-repeat: no-repeat;
background-position: 1px 50%;
- padding: 2px 0px 2px 20px;
+ padding: 1px 0px 1px 20px;
width:100%; /* IE */
}
#context-menu li>a { width:auto; } /* others */
@@ -42,7 +42,7 @@
#context-menu li a.submenu { background:url("../images/sub.gif") right no-repeat; }
#context-menu a:hover { border-color:gray; background-color:#eee; color:#2A5685; }
#context-menu li.folder a:hover { background-color:#eee; }
-#context-menu li.folder:hover { z-index:10; }
+#context-menu li.folder:hover { z-index:40; }
#context-menu ul ul, #context-menu li:hover ul ul { display:none; }
#context-menu li:hover ul, #context-menu li:hover li:hover ul { display:block; }
diff --git a/groups/public/stylesheets/jstoolbar.css b/groups/public/stylesheets/jstoolbar.css
index c4ab55711..4e9d44b6c 100644
--- a/groups/public/stylesheets/jstoolbar.css
+++ b/groups/public/stylesheets/jstoolbar.css
@@ -84,6 +84,12 @@
.jstb_ol {
background-image: url(../images/jstoolbar/bt_ol.png);
}
+.jstb_bq {
+ background-image: url(../images/jstoolbar/bt_bq.png);
+}
+.jstb_unbq {
+ background-image: url(../images/jstoolbar/bt_bq_remove.png);
+}
.jstb_pre {
background-image: url(../images/jstoolbar/bt_pre.png);
}
diff --git a/groups/public/stylesheets/scm.css b/groups/public/stylesheets/scm.css
index 66847af8c..d5a879bf1 100644
--- a/groups/public/stylesheets/scm.css
+++ b/groups/public/stylesheets/scm.css
@@ -1,14 +1,16 @@
table.filecontent { border: 1px solid #ccc; border-collapse: collapse; width:98%; }
table.filecontent th { border: 1px solid #ccc; background-color: #eee; }
-table.filecontent th.filename { background-color: #ddc; text-align: left; }
-table.filecontent tr.spacing { border: 1px solid #d7d7d7; }
+table.filecontent th.filename { background-color: #e4e4d4; text-align: left; padding: 0.2em;}
+table.filecontent tr.spacing th { text-align:center; }
+table.filecontent tr.spacing td { height: 0.4em; background: #EAF2F5;}
table.filecontent th.line-num {
border: 1px solid #d7d7d7;
font-size: 0.8em;
text-align: right;
width: 2%;
padding-right: 3px;
+ color: #999;
}
table.filecontent td.line-code pre {
white-space: pre-wrap; /* CSS2.1 compliant */
diff --git a/groups/script/dbconsole b/groups/script/dbconsole
new file mode 100644
index 000000000..caa60ce82
--- /dev/null
+++ b/groups/script/dbconsole
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../config/boot'
+require 'commands/dbconsole'
diff --git a/groups/script/performance/request b/groups/script/performance/request
new file mode 100644
index 000000000..ae3f38c74
--- /dev/null
+++ b/groups/script/performance/request
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/performance/request'
diff --git a/groups/script/process/inspector b/groups/script/process/inspector
new file mode 100644
index 000000000..bf25ad86d
--- /dev/null
+++ b/groups/script/process/inspector
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../config/boot'
+require 'commands/process/inspector'
diff --git a/groups/test/fixtures/attachments.yml b/groups/test/fixtures/attachments.yml
index 162d44720..ec57aa6dd 100644
--- a/groups/test/fixtures/attachments.yml
+++ b/groups/test/fixtures/attachments.yml
@@ -36,4 +36,53 @@ attachments_003:
filename: logo.gif
description: This is a logo
author_id: 2
+attachments_004:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_source.rb
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 4
+ filesize: 153
+ filename: source.rb
+ author_id: 2
+ description: This is a Ruby source file
+ content_type: application/x-ruby
+attachments_005:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_changeset.diff
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 5
+ filesize: 687
+ filename: changeset.diff
+ author_id: 2
+ content_type: text/x-diff
+attachments_006:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 3
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 6
+ filesize: 157
+ filename: archive.zip
+ author_id: 2
+ content_type: application/octet-stream
+attachments_007:
+ created_on: 2006-07-19 21:07:27 +02:00
+ container_type: Issue
+ container_id: 4
+ downloads: 0
+ disk_filename: 060719210727_archive.zip
+ digest: b91e08d0cf966d5c6ff411bd8c4cc3a2
+ id: 7
+ filesize: 157
+ filename: archive.zip
+ author_id: 1
+ content_type: application/octet-stream
\ No newline at end of file
diff --git a/groups/test/fixtures/changes.yml b/groups/test/fixtures/changes.yml
index 30acbd02d..56d936296 100644
--- a/groups/test/fixtures/changes.yml
+++ b/groups/test/fixtures/changes.yml
@@ -13,4 +13,11 @@ changes_002:
path: /test/some/path/elsewhere/in/the/repo
from_path:
from_revision:
+changes_003:
+ id: 3
+ changeset_id: 101
+ action: M
+ path: /test/some/path/in/the/repo
+ from_path:
+ from_revision:
\ No newline at end of file
diff --git a/groups/test/fixtures/custom_fields.yml b/groups/test/fixtures/custom_fields.yml
index 6be840fcc..1005edae4 100644
--- a/groups/test/fixtures/custom_fields.yml
+++ b/groups/test/fixtures/custom_fields.yml
@@ -4,9 +4,13 @@ custom_fields_001:
min_length: 0
regexp: ""
is_for_all: true
+ is_filter: true
type: IssueCustomField
max_length: 0
- possible_values: MySQL|PostgreSQL|Oracle
+ possible_values:
+ - MySQL
+ - PostgreSQL
+ - Oracle
id: 1
is_required: false
field_format: list
@@ -29,9 +33,14 @@ custom_fields_003:
min_length: 0
regexp: ""
is_for_all: false
+ is_filter: true
type: ProjectCustomField
max_length: 0
- possible_values: Stable|Beta|Alpha|Planning
+ possible_values:
+ - Stable
+ - Beta
+ - Alpha
+ - Planning
id: 3
is_required: true
field_format: list
diff --git a/groups/test/fixtures/enabled_modules.yml b/groups/test/fixtures/enabled_modules.yml
index 8d1565534..da63bad5d 100644
--- a/groups/test/fixtures/enabled_modules.yml
+++ b/groups/test/fixtures/enabled_modules.yml
@@ -39,4 +39,8 @@ enabled_modules_010:
name: wiki
project_id: 3
id: 10
+enabled_modules_011:
+ name: issue_tracking
+ project_id: 2
+ id: 11
\ No newline at end of file
diff --git a/groups/test/fixtures/enumerations.yml b/groups/test/fixtures/enumerations.yml
index c90a997ee..22a581ab9 100644
--- a/groups/test/fixtures/enumerations.yml
+++ b/groups/test/fixtures/enumerations.yml
@@ -19,6 +19,7 @@ enumerations_005:
name: Normal
id: 5
opt: IPRI
+ is_default: true
enumerations_006:
name: High
id: 6
@@ -39,4 +40,9 @@ enumerations_010:
name: Development
id: 10
opt: ACTI
+ is_default: true
+enumerations_011:
+ name: QA
+ id: 11
+ opt: ACTI
\ No newline at end of file
diff --git a/groups/test/fixtures/files/060719210727_archive.zip b/groups/test/fixtures/files/060719210727_archive.zip
new file mode 100644
index 000000000..5467885d4
--- /dev/null
+++ b/groups/test/fixtures/files/060719210727_archive.zip
Binary files differ
diff --git a/groups/test/fixtures/files/060719210727_changeset.diff b/groups/test/fixtures/files/060719210727_changeset.diff
new file mode 100644
index 000000000..af2c2068d
--- /dev/null
+++ b/groups/test/fixtures/files/060719210727_changeset.diff
@@ -0,0 +1,13 @@
+Index: trunk/app/controllers/issues_controller.rb
+===================================================================
+--- trunk/app/controllers/issues_controller.rb (r‚vision 1483)
++++ trunk/app/controllers/issues_controller.rb (r‚vision 1484)
+@@ -149,7 +149,7 @@
+ attach_files(@issue, params[:attachments])
+ flash[:notice] = l(:notice_successful_create)
+ Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
+- redirect_to :controller => 'issues', :action => 'show', :id => @issue, :project_id => @project
++ redirect_to :controller => 'issues', :action => 'show', :id => @issue
+ return
+ end
+ end
diff --git a/groups/test/fixtures/files/060719210727_source.rb b/groups/test/fixtures/files/060719210727_source.rb
new file mode 100644
index 000000000..dccb59165
--- /dev/null
+++ b/groups/test/fixtures/files/060719210727_source.rb
@@ -0,0 +1,10 @@
+# The Greeter class
+class Greeter
+ def initialize(name)
+ @name = name.capitalize
+ end
+
+ def salute
+ puts "Hello #{@name}!"
+ end
+end
diff --git a/groups/test/fixtures/issue_categories.yml b/groups/test/fixtures/issue_categories.yml
index 6c2a07b58..aa2f70351 100644
--- a/groups/test/fixtures/issue_categories.yml
+++ b/groups/test/fixtures/issue_categories.yml
@@ -9,3 +9,14 @@ issue_categories_002:
project_id: 1
assigned_to_id:
id: 2
+issue_categories_003:
+ name: Stock management
+ project_id: 2
+ assigned_to_id:
+ id: 3
+issue_categories_004:
+ name: Printing
+ project_id: 2
+ assigned_to_id:
+ id: 4
+ \ No newline at end of file
diff --git a/groups/test/fixtures/issues.yml b/groups/test/fixtures/issues.yml
index 4f42d93c4..9d3287c6f 100644
--- a/groups/test/fixtures/issues.yml
+++ b/groups/test/fixtures/issues.yml
@@ -13,6 +13,8 @@ issues_001:
assigned_to_id:
author_id: 2
status_id: 1
+ start_date: <%= 1.day.ago.to_date.to_s(:db) %>
+ due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
issues_002:
created_on: 2006-07-19 21:04:21 +02:00
project_id: 1
@@ -20,13 +22,15 @@ issues_002:
priority_id: 5
subject: Add ingredients categories
id: 2
- fixed_version_id:
+ fixed_version_id: 2
category_id:
description: Ingredients of the recipe should be classified by categories
tracker_id: 2
assigned_to_id: 3
author_id: 2
status_id: 2
+ start_date: <%= 2.day.ago.to_date.to_s(:db) %>
+ due_date:
issues_003:
created_on: 2006-07-19 21:07:27 +02:00
project_id: 1
@@ -38,7 +42,7 @@ issues_003:
category_id:
description: Error 281 is encountered when saving a recipe
tracker_id: 1
- assigned_to_id:
+ assigned_to_id: 3
author_id: 2
status_id: 1
start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
@@ -71,4 +75,20 @@ issues_005:
assigned_to_id:
author_id: 2
status_id: 1
-
+issues_006:
+ created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ project_id: 5
+ updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
+ priority_id: 4
+ subject: Issue of a private subproject
+ id: 6
+ fixed_version_id:
+ category_id:
+ description: This is an issue of a private subproject of cookbook
+ tracker_id: 1
+ assigned_to_id:
+ author_id: 2
+ status_id: 1
+ start_date: <%= Date.today.to_s(:db) %>
+ due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+ \ No newline at end of file
diff --git a/groups/test/fixtures/mail_handler/add_note_to_issue.txt b/groups/test/fixtures/mail_handler/add_note_to_issue.txt
deleted file mode 100644
index 4fc6b68fb..000000000
--- a/groups/test/fixtures/mail_handler/add_note_to_issue.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-x-sender: <jsmith@somenet.foo>
-x-receiver: <redmine@somenet.foo>
-Received: from somenet.foo ([127.0.0.1]) by somenet.foo;
- Sun, 25 Feb 2007 09:57:56 GMT
-Date: Sun, 25 Feb 2007 10:57:56 +0100
-From: jsmith@somenet.foo
-To: redmine@somenet.foo
-Message-Id: <45e15df440c00_b90238570a27b@osiris.tmail>
-In-Reply-To: <45e15df440c29_b90238570a27b@osiris.tmail>
-Subject: [Cookbook - Feature #2]
-Mime-Version: 1.0
-Content-Type: text/plain; charset=utf-8
-
-Note added by mail
diff --git a/groups/test/fixtures/mail_handler/ticket_on_given_project.eml b/groups/test/fixtures/mail_handler/ticket_on_given_project.eml
new file mode 100644
index 000000000..927dbc63e
--- /dev/null
+++ b/groups/test/fixtures/mail_handler/ticket_on_given_project.eml
@@ -0,0 +1,42 @@
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project: onlinestore
+Status: Resolved
+
diff --git a/groups/test/fixtures/mail_handler/ticket_reply.eml b/groups/test/fixtures/mail_handler/ticket_reply.eml
new file mode 100644
index 000000000..99fcfa0d1
--- /dev/null
+++ b/groups/test/fixtures/mail_handler/ticket_reply.eml
@@ -0,0 +1,73 @@
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>
+Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
+Date: Sat, 21 Jun 2008 18:41:39 +0200
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+This is reply
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
+<STYLE>BODY {
+ FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
+}
+BODY H1 {
+ FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
+sans-serif
+}
+A {
+ COLOR: #2a5685
+}
+A:link {
+ COLOR: #2a5685
+}
+A:visited {
+ COLOR: #2a5685
+}
+A:hover {
+ COLOR: #c61a1a
+}
+A:active {
+ COLOR: #c61a1a
+}
+HR {
+ BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
+}
+.footer {
+ FONT-SIZE: 0.8em; FONT-STYLE: italic
+}
+</STYLE>
+
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
+size=3D2>This is=20
+reply</FONT></DIV></SPAN></BODY></HTML>
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--
+
diff --git a/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml b/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml
new file mode 100644
index 000000000..ab799198b
--- /dev/null
+++ b/groups/test/fixtures/mail_handler/ticket_reply_with_status.eml
@@ -0,0 +1,75 @@
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>
+Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
+Date: Sat, 21 Jun 2008 18:41:39 +0200
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+This is reply
+
+Status: Resolved
+------=_NextPart_000_0067_01C8D3CE.711F9CC0
+Content-Type: text/html;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
+<STYLE>BODY {
+ FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
+}
+BODY H1 {
+ FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
+sans-serif
+}
+A {
+ COLOR: #2a5685
+}
+A:link {
+ COLOR: #2a5685
+}
+A:visited {
+ COLOR: #2a5685
+}
+A:hover {
+ COLOR: #c61a1a
+}
+A:active {
+ COLOR: #c61a1a
+}
+HR {
+ BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
+}
+.footer {
+ FONT-SIZE: 0.8em; FONT-STYLE: italic
+}
+</STYLE>
+
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
+size=3D2>This is=20
+reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML>
+
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--
+
diff --git a/groups/test/fixtures/mail_handler/ticket_with_attachment.eml b/groups/test/fixtures/mail_handler/ticket_with_attachment.eml
new file mode 100644
index 000000000..c85f6b4a2
--- /dev/null
+++ b/groups/test/fixtures/mail_handler/ticket_with_attachment.eml
@@ -0,0 +1,248 @@
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200
+Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: Ticket created by email with attachment
+Date: Sat, 21 Jun 2008 15:53:25 +0200
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270"
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+This is a multi-part message in MIME format.
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270
+Content-Type: multipart/alternative;
+ boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270"
+
+
+------=_NextPart_001_0020_01C8D3B6.F05C5270
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+This is a new ticket with attachments
+------=_NextPart_001_0020_01C8D3B6.F05C5270
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META http-equiv=3DContent-Type content=3D"text/html; =
+charset=3Diso-8859-1">
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR>
+<STYLE></STYLE>
+</HEAD>
+<BODY bgColor=3D#ffffff>
+<DIV><FONT face=3DArial size=3D2>This is&nbsp; a new ticket with=20
+attachments</FONT></DIV></BODY></HTML>
+
+------=_NextPart_001_0020_01C8D3B6.F05C5270--
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270
+Content-Type: image/jpeg;
+ name="Paella.jpg"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="Paella.jpg"
+
+/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU
+FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo
+KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCACmAMgDASIA
+AhEBAxEB/8QAHQAAAgMBAQEBAQAAAAAAAAAABQYABAcDCAIBCf/EADsQAAEDAwMCBQIDBQcFAQAA
+AAECAwQABREGEiExQQcTIlFhcYEUMpEVI0Kh0QhSYrHB4fAWJCUzQ3L/xAAaAQADAQEBAQAAAAAA
+AAAAAAADBAUCAQYA/8QAKhEAAgIBBAICAgIDAAMAAAAAAQIAAxEEEiExIkEFE1FhMnFCkaEjwdH/
+2gAMAwEAAhEDEQA/ACTUdSsdhRCNE54GTRaBaXHiBtNOVo0wEpSt8BKfmpWCZRPHcVbdZ3X1J9Jx
+Tla9OBpIU8Noo7Gjx4qdrCBkfxGupUSck13GJjeT1ObEdthOG04/zpX8SNXjR1njym46ZMmQ+llp
+pStuc9T9hRq/X22afhKl3iazEYHdxWCfgDqT9K83eKfiFG1RfIEi3tuC3W9KlNh0YLqyeuO3QV0D
+MznM9O2uai4QI8psYQ8gLA9virY615P034xX+zNNslLDsMKOG1J5HuAa3nQPiBZ9WtpUy4lmcE4U
+ypXP2rmMHmcI/EealD7te7ZZ2S7dLhGiN9cvOBP+dIF18btHw3C1DkSbi7nATGZJBPwTitTIyZp9
+SsCun9oJaEFUDTy0oyQFyXSOfoB/rQOL466huE9LIagxW1A48tkuKJxwBlQrm4YzNhGPE9Mmua8Y
+JrzsrXPiQ42y7+KtsZt4kpS8ltK0p91J5IzXGFr3xFef8pMqE4vJABZT6se3FDNyEZzNCh89Tfbv
+aoV2iKj3GO2+0eyh0+h7VkWq/CqTDUqXpp0uJHPkKOFj6HofvQRzxZ1bbwFTG7c+jO0lKeh+cGi8
+bxrebZZVMtjDqljKgw4Rt9uuea5vEIEceoL09ZnHQoyGy3KaOFhxO0j6g0J8QNPr3tzorHmsJSUv
+NgdQeprTIuqbfqdtD7MRxh7HO/H6ZHWlnW0e5tQnv2WgupAyEg8p9xUl7WGowpzKCoDXyJ5nvMdK
+Uuho4bSv057CqK2stIWrgEZp2kWtE+O5+MC0OKUchHFCbnaWVNeW1KU3tTtwtAUkj6jkfpXoK7gQ
+AZLsqYEmJ0mUBlLeCfeqHKl5PqJopNhriupQWyoqPpKeQfpTXYPDW+3ZlEhTTcVpXI8w+oj6Cmty
+qMxTazHAi1ZLG/PXuKClv3Ip7t2n4yI3lKZSsEc7hmicXwfu5ThN22fCUH+tXB4QX1KdzN6WVjth
+Q/1oDuG/yjCIV/xgWLouQFfiLK/5LqejbnKT9D1FStX05DRaYrTN8K232wEl1aMJV856VKF9hPc3
+9QPM32HEjxEjykBSh/ERSd4s61uGjLbBnQrcie2t4pfClEFKAM8Y704uvtsMrdfcQ20gZUtZAAHu
+SawHxt8V7PKt/wCytPp/aLrToW7JAPlNkAjAPfOfpQ0JY4E42B3Nf09ruwXvTQvjM9lmGkfvvOWE
+llXdKvn/ADrONZeNwU28zo2Ml1tHpXc5Y2spP+EHlR/5ivOzYkPPKdjMechRDjrCUHy1Ec9Aa1Lw
+l0VF10pcy4XJC0RlbTFTgKbHwnokfSibFXkzAJbiJ0tN81jc1yHXplzkEEqkPA7UjvtR2H1/SrOl
+rGu6NvP7Q8yhaWkDruVj/n616Lvl20n4Z2cpeS02tSfRHbAU69/t8nivOGoNXzNQSVRbFAbtsFal
+FESEjBOepUR1rBs3D8CFVMHjmXNYW+wWtsMrlMvyyOW4h3FB9irpn70lx7k9AeDttW4w70DgWd3+
+1NmlvDi7XpL0iShcWG0dqllO5SlHsB35NG7l4PSRG823z0YbGFqkDaFK+MZx7d6XOu09Z2M8MKHb
+OBM1vBuAkJcuUgyHXRu3KfDp+5ycVTaeU36kKUlYOQQcEVrehvC5l1Mh/VClISHFMttIVgL45VnH
+TkEH4rQbjpHTbyGWVQIzL7bYabc2AnaMfYnAxk0K35Smo7e/2IRdC7eXUwfT5m6pfbtC/wARIlLW
+VNu7yoN9MlQ9h3NO+n9Cwo8rzZU1Sm2Mlx9YLaUkHjaOv3Nc7zd7FoyY5D07HR56SfMl7961ZGNo
+9gKXrtd77dnkssoSwt7K9rZG8jHU44Tkc9q0rvbyvipnNgT9kTRLvqKy2JDgS/8AiH3hjecKXjv2
+/SkG8akmRyhqG+hKSQ4dpyofBxxV2w+Hkuda27pMW5tcSpWxati1HJGQTkYp70xoS2MW1pp+ImXN
+koJLi+UtfP1FAt1dFPHcPXQ9nPUy+/3pu4usrYZS16MOKCAkuLJypRxX5aG5ExX4VlfC/Vt98e3z
+WvL8M9NsNMtyFyVyGx6h5uPMPyMcV9Q9HQbbdWwzHQGFHKVhStw+uTQTr6tu1IQad85M46baVarV
+uVkJ/mDVCVqWUll59t4FxlW0ocOA4k+1P8uLGU35UgAhQ2kgdRWUeIMi2WyKqASFLJJbWchQI7Ul
+pWWyw5GSYZ1IXA4Ez7U12mR7q95jCWgTuCQeoPsaGqntylbCpIdxnaSM/wBK56lujtydZS4UkNIw
+CBzQO4RURywWnUupcQF7knoT1BHYg5r0lFY2DIwZKvYq5x1DjUo26WzJKEuIQoFSFDIP+9bzaL0x
++HZcZcQpC0ggewIrzYzNJQGpGVt+/cUw2PU8+0vqWEJnW8q/9KzgpHslXb6UV6yw4gBZg8z1NZbj
+Ek43LQDjkZFMLbkMcJW3+orKvDq86T1SUssrEef3iPq2rz8f3vtTZrtizaR0pOvD8XephOG2959a
+ycJH60HBBxDBhjMB+L9/RY7WpT7jam3kkNNJwSs+/NSss0Bpi4+Jmpfxl7kPOQ2k7iCfyI/hQOwz
+/vUroqrUnceZ8LnIG2Cdaa61Dq54i7SVJi5ymGwdjSf/ANe/86s6W0TLvkNySp5pcVjBUy0oAD5x
+1P1NbDbPALTQjp/aC5bj+OS27tH+VOmjPDqw6QEv9lNPFcpIQ4p5zeSB0A/WtNYoXCwK1nOWgjwk
+sFrg2wuJjtKl5IJUBwPakLxDXbNI6/alaGW6b87uL1vjJCmAogjcvHTrnb8DpVnxj1q1oOS7b9PP
+j9qSEErA58gHuf8AF7CsStOurpBjKZioQqS6sqU+vlayepPvQytu3cgz/fEPWaXfFjYEfLlo5+bM
+/aurr+X33vW6lIJUD/dyen2p80zboMNG6NBEGOygJLy04cdAGRjjn5NYRD1NcjMMme8XpST6Q4Mp
+H0HStstF4kO2lMS5vAlTfq9O04PQZ+KifILaqg3PnPodS5o0S3I0q4x2T3Kr+obzH1HsjuFFpeUU
+B5s5Snck4ST0z0p502w5HZW86qW5lXLbpSeMfHFZH4gpFutbDlrmNtujlxvzc705HAHfB5qknVSI
+VliuWK7STcHVBL7Ticc8c8f70IaMaipWq4z+oo6jT2sr8ma3qCfBky48be4zvcAOB6gR/CMd6EXF
+m9EPKhx3Vx92EJdADmOmQKJ2y5xVpiJlW+OzPSj1LbSBtURyoGjFzWqPbHljClFBLbiBnHHUmpeT
+WdqiPISuDM/e0bark4YzkEJkJ9RebGF7u+T/AKVeg6DbVdXHJ6U/hi35KAlRGU44zj/WrtpdfSlt
+D7m54jKznr/WnOAVKa9Y7cGtDVWodhaH1WnVlD7cZxPhq3NMobbeBeZQnalKlZ47cUQDSGtvlqwn
+GEp7AVQdbddWQHkp2dOea6qWHQlPmJSscEE9aET/AJCK/X+JFxUtuKecHnKxx8VXRKiBSkuKII55
+PSvq4yUQmf3qspxwc8is71fqZMeKtTO0AHn3V8UaitrDgdmcdtoyZ215q1USShq0bZClghTYPqFL
+Vr0xH1otbt1XKZkpT6cccfOaF6SZkz7q7dZYWHjz0ykJp2Yvi4YaYVHdUXjs2eSUlR7HPt89KoW5
+p8af5D3OVLldz9GLmsNLR1WZiI+oJlRB5aHgBuKe2cdaxd5tVsuy0OJbdWwvkKGUq+or0PqiyXVy
+IJ7za1NlIJbz6m/fgdv61lN000qWJ09EWQ8++6lqM01k8geokY5p/wCK1RXK2Nn/AOz75PS1vStt
+Y594iCUnOauWi5SLXMDzIQ4g8ONOp3IcT7KHcVduWn7nbWg5OgSI6SopBcQUjPtzXK1RX1OqkMtb
+0xcPO9PSkHrzV0WKRkHM86a2BwZqFm0da9c2pdw0asM3JgBT9qdd2uNH+8y51x7A/rSjrXUmq129
+Om9TuyvKhu70NyUYd4GBlX8QofG1hcLbrBF/tZ/DvtqGEDhJQONpA6gjrXq61f8AS/jDo9mXNhNu
+nGxxPR2O5jkBXX+tY3bcFhPtoPAin4H6gsMTQgLEhtM7eoyGioBYI4Tx7Yx+pqUr668ILjZXDOtS
+XZsdvlMiGkJlND/GgYDg+Rg1KwUDHIM2r7Bgiei5NwiQo635cllllAypbiwAPvWO678c4UJuRH0y
+gSHkDBkrHpz2CR3+prHbXJ1L4o6matwkKaYP7xzkhthsdVEf8NLWrzbo94fh2RKjAjqLSHFnKniO
+Cs/X/KuLSAcN3OfYW5HUD3SXJutxfnTnVOyn1lbi1HJJNPnh9otyfbJF5lLabjpJQ0FjlZHUis9C
+lDOO9bdHkS4WkbXBlIMdaGUnyhwkjqFfU5pf5K566gqe+I98TpBqb9pnB/Q9wu7kdyOGUNNp3oWp
+Owq7+3P1r9uQmqllqS+S+ghClFWR+vtT/Z7goWGOopbjodwEltQOcdR16/WrcrTFmW4tyYZHmuDc
+dhwkDHSvNvq2BC2+up6PThdIzDvMypelJN2lI8+M9JKxsZS1/Cfcn2+tF9K6Oh6ZeW5fYS5VwKgl
+locpR3Cvk0+zJTdtioi2htDe5OVL/KAPcn3r5j3ZtdmkrKFTFJ3EDG7BAzgH9a+XX2sNi8CJXaZW
+c3GIN7u0u931+KwhaGGspKQMKcKepVV5UmU1DZZtzspMVKQXm3F5B+gHIH0zQCBImKuiJMeCuEH1
+YCfVkjv+bqSKr6t1U7a7uxEgurS0yMLBASc/arlenBULiSGtOSSY6WKJKXckJU2tplSt6FA7gfvW
+gxA/sUBggDGSayGya5ed8tkNqSlXVYOVVpEZydIablRFF6ORgjGFJPyKga3Tuj5Il2rVC6sKT1L9
+tiuPTnDI3eSfc/lqrqWOuHFK4qlF1HIX7j2NWIkyQ8XEApSUcD/Ea5TmZj2SggqUMKSrp9KUByQM
+T45U5mSS9UzJMtMZ93GFcqJ7UL8Q3UOOww24Bx6h3V8/Sqev0sx7u4IqkB5w8tJ4KFfNBXG3Fuo/
+FPqLxA3FXXHtXp9PQiBXXiTGZrmIjTo68qh+Y2ygPhYSAlXIBz1rYHp04RkNRnWDOA5KyEgDrgVh
+mmSmPcCfQpWCACnINFdRXOW3GQ4+60GgcJKDgr+R70lqdP8AZaAvuUK3woDY4mqyrjeFWppZZUXW
+lnzUlYCVp+K+LLeYEoLLG5lGdxQk4wcfyrOourlyIzbDhcKVNhHB7e9XYlxatbam0dVDOAOT96Rf
+TEDBHMMpU9dTQpVxiTWXGUqDy1n0hxCSAPvXnfWVtnWO9TI8lpLHnZOGxhKkE54+K1K1XhLj4S4j
+GOnxX5qiNZ7wlpd1Di30ZS0hKtu4kdCaN8fqG0luxhwYtrdOtqZXsTA1dTWh+B+unNG6tbTIWTap
+hDUhGeE56L+oP8qSbtBXDnyWSB+7WUnadwH3rgYT6IQmEpS0VbU5WNyj8DrXr/F1/ueXIZT1P6Hh
+aVoSpJBSoZBB4IqVjPgP4ii72eHZLsSJrCPKadP8YA4B+cfrUpMgg4jK8jMybw5vUfT/AIXatujD
+iRc5S24DX95KVAkn/P8ASstODk9asPSXvwZbUEoQpzhtIwkYHt9z1q3NZiO2uNMhFLbif3chkryc
+9lAHsabbAbP5i6DI/qctPSokW9w3p0cvsIcBLY7+2fituuVxYvDbAMZ2VIUkeX5I5x3Tgdqznwz0
+xbb/ADZQuy3w2y2FISycHJz3+MVtWnNLwNMb3G0SZDvlgb3DlWPgf86V5/5e+oOAc7l/9y18WLK/
+IdH/AHB+l23bLPLMl0RkyQS22r1eWQO/tR178NEju3GS8ZahyVIc7ewA4qpKKfxzTMOGHCsBZSob
+ueveitut+XGo8tpDacEp2DAP69ahNYHO4yo1rMxJgt22RLy0l5bYQ04jckLWfM+o7frVPUMpdg0a
+65EfXvaX5XOArnp9hTtGgRbcyhL6PPbaG1ClnJAPvWeeMl0FogwnWGYkqKHSFxnUkpSojgkD79aJ
+pQbblr9ZgNRcAhMzli9zZYfS27NkPBIKAFKVnnkn2pf1PaZbMNm4PpkDzeV+c0UEK+p6/WtX8H5M
+GXDm3OS22Jq3P/W2AlIHwOgFVPF+VBfjqKi4sEHBKSAVfFegXWsmo+pV4zJZ0wareTFbw71Y1Ab/
+AAjbcNh1Q/8Ae9yaYU33VESW5KdK1wucuMpwgj3FYq4S456E7VDjimGHqa6wYqIS5HmMq42LOQBT
+Wo0AYll5z+YCjV7MA+puVmuDkgh7evZt3bsdK46s1uiNZSY6iHwSj82CPnFC7PcbdbdOxkPTiqaB
+5iQlXCf61mV9uC79dn39oDIVztGAajafRK9pPoSrZezKAOzKclyXcLgue8VLUo7sHrUaVIfeCloG
+T0Uo9qstKdbcBLZUg9DiuzkbY4VDIBGQkdBVkuBxOrRtAwf7naKlyMoqQ4pRI9RHH2qtc1/i/KS+
+p3yWchtKwcIzX7HnoQv1nbgYUR7+9NESXCmR1xdjexxOXCTg9ODSzO1bBiJvCsCBFu3eahwltCnA
+O6ATj6082K2rlltyXGSsIGEhzPP1xQa1QJNngLmMuNPMrPKE5BwKuzrw6Yu6JJVGWkZSkHIXn274
+pe8m0+H+51G2DBlu4J/DzFKbWhICiS2EgH7H2FD3JTMuclt7B2ArBzgJPvQNF1lSUFoON5JyST1P
+tmgEu5yY0wgJ2uoUd27nPtRKdEzHk8xezVLUnHudtXsRYc4rt8pxZdKvMSpWcH60M07a03W5JZcW
+UtgFSj8Dt96orKnVKUQVK6nv966R5b0dCksLLe4gkp68dOatKjBNgPMiM4Z9xHE1fwCkQx4pqYdC
+vJcC1RwT0WkZH8s1KVPDm+Psa208ogAtysqWOqyo4JP2qUtanPM2jDEL+OWn49u8R5UK0MbGClDg
+bSOApYyQPvSzM0rKt9qiXCRs8uSSlCeQoHnII+1aJ/aAZWjxImL3FILTSwR/+RX7bhqJ561XC5Jj
+O20pSnyFYJWMZypJ6djWLdSa1BzxDUaYWnaOzH/RlmZ0nYWPJab9SQqS5t/eLV2+wzj7UfZmouM8
+MNtlsNoKlFZAV8H4FULPfmrmtyCtwJfQjKggFIVx2orHsbUZ1TzCktFwfvVKJJUB05968jqHaxyz
+y3t+sBeiJJTLSXA6hAWscFSTjke561yfkAlte4h88BIJwB3q5Hjx297RUpWfUD+YYqs5Gjx3HJJK
+ywRylIGM+/vShBMIrDMtpKiyVKcWtvaP3aRnn3HevOfi9eZM/UEiEv8A7eOHgkhfT0jg4+5r0JJu
+ENLad0plpWM9c8dqUtTaMtGoJS37gyXH3UANyEHH6iqXx99entD2CK31m1CqmZZomd+HjORbXte8
+hOVLSk4USeTRm4xrvqbTjseUGmozTmVPLH5fgfNNNhYtWmJardbw3tf59XqIwepNM2poyJVpdKEt
++SRuCR/EfemLdWou3oO/cJXVmsI08z3BiFp7UakMuonR0jk47+31oG7iTM/dkNoWvCdx/KCe9P8A
+dIzR1PAZfjtI3gx3QsAJHznFKOqbfbbXKSzbriZrwJ8390UJRjpgnrXpdNeLAM9kSDqKDWT+AYcu
+1ivcK2x1KdiyYSejrCgSnPZXehTLqou7cghKRkgd6Px9SWp2xsMT23HF7QgpaOCFDoaCxFee4UKC
+gCT14P3oKs5B+xccx+kIpG0wlaJKZLB9KglB5Uo9KsLeDj2GzjI+1AjmPLH4ZzCVEApPAIopGCFR
+1rSpW4naaFbWB5DqUabMnaYEuTGyc40le4deO1fMZam17krwAOua7yYjyZCiG8hZ65ya57WW3W2y
+lS3FDkFW0CmgdygdydZ4MT1HezzUy4iCwVKLKcFtSuD74r9uVtRJabLZ8obckpTlP60ItSLXOeDT
+KlR1spG9W7clw/ejN4mXa0MDYA9FLn7olIxtxyFCprVkWbU7/cY+0FNx6/UU70GYDBQw6FrUcAgH
+ke9Lq3FHkkk980xXedHuYWt6D5L4A2rQrCQO4xV+yaaiTrW5JL29GRgflUCOoJ5wPmqaOKUy/cl3
+Zufw6itbriuAJHloSVPNlvJ/hB61RCwVAKPHc1YubQZmvNpSlKUqIACtwH371Tzk/FOKAeR7ibEj
+g+o06QWy7riziG2pDf4lsJCjknnrUrv4TtIe1/ZQ50Q+Fk/TkfzxUpW7ggQ1a7xmbF/aGsKEX83N
+U4IU8wFJZWMbtvBwf04pOieITadOMxXmWRJR6CsD1HHTH2xWx/2irAu9aJTIjJJkQXgsYHJSrg/6
+V5os1rjsynVXOQY8uMsER1t8r+M9j0pSymu1P/J6j+ktatxtE23QtvmwYar3cX0JjyE+hhQ9ROeC
+a0CJJaLTe+Uhfm/l7/YUhWKUxfbKxCztdQkJStWdySf7o/rTHZLC7bW3g5M819Y2pLiPy/TmvLak
+AsSeCPUp7i1hB6h+Ytbnl+US2AfVx/nXyWg4kpeOQ4CPT2FVX0JacS6qWpASnC0qIINDLlKKGyGp
+QaLmADgYA74xzSY7zDpWW4Eq2e0N2yXMdmKS6twlCUO4IQj3+po86RGWzGjtNgO4AATwlPXNAmPK
+dLanH15K04SEE5x7GrsGWLnclJ9SHGuCrOCU+1E2s5zNfSE/7mJniFFciyHJ6XEktoIylWBjPPHv
+SnC1HKlFK25Kls7cBpSvy4PtWwXHSsCXIUqUt15Tg2qStfpx7kUIc0JZIqHlpGwqTgFJxgZzx809
+XfWE22DJgwQD49TGr0pN2nlL7i2JKjvC1DCc9qUtRR47sjLQWiYkYdbX0PyDWwax09bZpcZtpdbl
+FJO5aztJxkD46Vl83TclMT8SlDjh28lIJwfY/NXdDqK8Ag4iGsosYHK8QVKiRIztv/BqccWUhT6l
+jASruBVpEoKkOAYLhJO0D9KGIUoqQ2vucYPaidptb0i6lCMNt8lSlq/N8VRcDblz1J9Tbf4CEGYb
+rzbjiEBLqQQAtQAzUs7jrqnGFNJy0fUMcA/WjlutUySrLT0dLGw5C08hQ6fbNCrTBuVlubjjkJ58
+pJwU5Lef72B1pQMLFYZGY0bHQggS7KYUw35ivUlXU9xSfdCp5QWltSUp/iPfNaBLtv4KGiVOkYcf
+X5imS2dyE9uM8DvjrQc2hyYsg+WGSfSQKxRatfJMLepvXA7iilxtKmlMJcQ4nlSlKzn7U4wbou7Y
+RK9SGeUpzjJPciuLmi5ayDF8t3nsrHFfFx0lcbeSptYWhKUlS0EjBP8ADR2votx5DMSFF1eRjiGF
+OWuK4mO+y2lTyFIWpw5SCeivgZpNuCzBU4zEmBbTnUtq4UP+ZoxaNIXG6So5ebX5C3NillXQd/pV
+zWlmYtEJmEiARLz6XEerf78jrXy3VK4XO4mDsSzbwMYiQI8iQlx5tpa2kfmWBwK4BKVdDiicpq5t
+NGItl1DbbYdUgDgAjO40JZSpxwBA5zVBDnn1EnGD+5rn9n+1pXeZlzcQFIYbCEEjoo9x9galN/hp
+BFn06wwQA89+9cPfJ7fpUpG072zHql2Libtf225NukRX+WnWyhX0Iry9drM3ar2i4XN0h6BKS28r
+O5TiByleD8Yr0ldJyHWtyOD0UKzHW9taloXM8jzkhBbkN4yVt+4HunqPvQXBxkTqH1E2dck2u5wp
+9rUW0yiVPKCdwQgkYJx361pca9NSGG3C5kIR6nkD0g/Ws5uMMT4DJtFyZTCdSlAjlsJKTnHpP+hr
+hapk+yxP2fNW7+DeSrAIyN3uP0qJfQtij8/9lPTlkznmPNwdh3FgILzgcK/3bqSfUfZQpW1BMuNr
+hKeeQlCyrCWeu0DjdXL9oW2NAadjuLbdj4UFBQIWoe6Scg/NEo5cu81h+5JAQtvcgdE++Tmlvr+o
+5YZEbpvstyvRlPSGtFvNJjzox4JKHknHP0pq03c2GlTAp5j8Spw7d5CVEYHANL9xsrTbMibHUCUJ
+IKEt8JPvxSey4ZylLX/8yOSMbqIK67stXwIT0NxyZubSDKUX1lbawkAZ9u+KHXeez5ja3HwhpPxy
+D2HNZu1rG7W5zeqS0EgbUggHA+nvVaNqOXdr5HVNcQhCV71BKQNx7ZzxQxoW7PUIgGcmNs6SqW+W
+2hvdc53qRgkHgc0YsdpVGgluSGygrUdqQClJ+TXVu2sSSu4x3PxD20qDa14yccAe2KruPvNw23Lg
+z+HDytqh1Chjoo9utAJ9LC22h0CqMRc15omyXhCnLc0mLc0c7mcBKiBnCk/PuKy646YvkCU0qLuL
+iWylQUPyE9cH5/WtkRLs0VhTLzqW22sEqLm5xXPTjtV2bLt88sttrCSpQxsOSCPeqGn191ACnyH7
+k27RI/K8TFdFOOYcTcAWENqIcUpJBz23DvTqvWMRElm3uQiUpIQ08BgJV259qdFWjzorsd8RXQ7k
+KJHCh7E9yBWWatszVpmsKRuCRgJTn0g5P9KKt9WrtJYYM+q07IgQGWpsNN/lsTH5W7yF7H22+Nqc
+ZJz84r8sMda284IRztBHal19yRbslgltMjKVA01abvCmLamK6AprbtGeoo1ysKwF5Eao0TsxK9xu
+03BS6hS9gU4DzkUWj26G4osKbSpRysBQJGaE2W822NHDbyngM7s4wM/avmZqdhrelhorSoEbxknn
+5qVtctnEOdLZnkQvKjIhuNojNZyraQMYTx1PtXzeYMZtDS30IS4lQWhWMkH4+tIxvz8GT5iQt1Bz
+vSoHBPbNVjPvGo33HWnSEsgqTgcE9NtMJpWyGJwJ9dQVGOxAGt9QruazbYxQGMAOOjBUo9hn4pf0
+vYiu7AvEKQ0rcQOh9hX47bJMW5qjlrCyohKSoEgfOKboflWmIhhsb5S+Sfk16SsCmsLX1PLWoXsz
+Z2I6QZ3kBKc5dPGPapSw28qMn1q3PK/Mc9PipQ4YVMwyJt2oHV2uZuGVML/mKoKWlwbkHchQ4qkN
+ZaevsQxzcmQsj0byUkH71TgOvRVqbeG6Ks+l5PqSD9RXxBioihqTS8Vm7JlNyHGIqlZWWujDmQQr
+H9339q/bihUVLqVvh1ak7S6g8KHwO1OshQIIUAoHg96z7VdpkxIEw2chTDqTmOr/AOZ90Ht9KWv0
+7WkYMf0Oqr075sXIgLTkZl7Uy1zZCQhpsuDOOuQOa05NvYkS0J8h1UUDd5w5UOOAfisK026yJZj3
+YOR3i56XRzkn+EitUsN4uEvEeCpDCGlEOL67ldMikfk6HUg54Ef02pS9i6jEcLpcGUMLSW9iU43J
+6EjH+VZ9NuLDmQqCIsdxR7e30rQWNPKaebmOTVrdXysq5C+OhFfcm129Y/7ptghJ3JKU8j6VLqtS
+rvmNFNx4mNXGMy6jEQqeUF5V8D2oS63JalpaQdrhxjdyQK2O6Ls8SOGm0hO7ohKeVH2FIl205Pdd
+cmMskrICkNg+pIz0IqrptWGGDwP3M3VhFye4w2hmVGYaUmUUsrwcpOSn5xTpcpUJu1vOmQpwObUK
+S6njfnjjtzWOu6iu3luRnIhQGTtJHBB/pRq1u3G5hhKFlIVneVdz9+lKXaRgdzkCdRxYMg9S9qB+
+A/MS0tpYIVudaZTgOqwAPtUdjTkORXGmhHbKgltKVBJSMd+9Mtv/ABrcWRFLUdxATl0lGFlWOx7/
+AAaEOJhuLZipYdksr6BokraVnnd7VhbOl7xBfWwctnj8T9m39strVFa9aMggZKlK+lLGpXLhc47d
+smsKjlSgpJWg5A65B7dfrWk2vTdus8p+clS1vYyEurB2H+pqs9erVc32zJIbeZXtS2oZO8fH+tap
+sVH3VrnHucXftIeZf/0zdZDYbKlPlpJWVnkZ7D704WLRhTbkOzg6XVpxsB2+Wfr3p0hzIylPPtth
+KEr2uFQxuI7ChV61IhaTGay24okBST0J6GutrLLPACMJY6DxMze/Ldtdzcik7gnlJ+DVJF2KTlVO
+0O2M3WK8mQ0h5/HoIOFdepPalq5aTuapziQhptrPUkHA609VZW3i3cbHyRVfKU03RLishXIpfVqe
+Q2lyJC/dZWQpfzmqF5f/AGdcSw08hwJxnb3V7CqcNl5qWp6U2lKRnYnOefeqlOjQDcw4kX5D5g2Y
+Wn13GOKsQklxR8yU51UecUSt+5GX3vU8rue1CbeypxfnO/YUWB9jRGIHAiVNZc72lgLJVzzUrmg1
+KFiOjjqIwUpPKSR96KWnUl1tLoXCmOt+4CuD9qFlOe9fm3nrT5wexPN5I6msWHxHjzili+Nhlw4A
+faGBn5HSmicCI6X2loeiufkeb5Sf6GvPqknrTJpPVs2wPbMh+EvhxhzlKh9KA1XtYZbM9xj1Laos
+/K1ICHv74/1qnbryuwBtCIYQgDatbayQv5wehpnu8NiXaBebK6X7csgOIPK4yj/Cr49jSbJXwQel
+BesWLseGrsNTbkjx/wBWQ4FvYfdntLW8NwZC8qT9RQ9Gq3bo8ERlBDajgrJ/KPekB1ltLqZCAlK0
+HcCUgjP0NfIuy1Tg+yw2y4kEL8kYSv52nj9KSPxNQ/jyZRr+UYfyGJt+nm7Kje95pflEAFxR6H/C
+DQW+OSocpBjL/EFZOHmzyR7GkzSl9ZLr5uE2LFBOPLWlWSPccYFaxpS8WZlP4aEpDri8OKO4KBP+
+lTL9NZQ/kMxg21agBi3MXo9ulOvB1uC8p0j1LV0PH86JQ7QpiSh94mO3tUFBSeMn2zTsJjKFrde8
+g8DbsIJA78VzbuEd6MVLaSWFZSCUZI985pRnJjCviI2nbncJNzXDUhL7aSU5C8J2/OKcbTaodsU7
+K8hLL6zuUndkA/GaU7tM/ZUlQjBlu3bdzbkdHKTnkE+59qU77q+4zISmGY8lbyVH96hKjlPHHFGG
+me0+HAM7bcmMxv1V/wCQkLFvcdxzktd6RbNDC71lDgbS2dy3F9sHmh8PVF5ZQtEdteFDar0eof0o
+8q7abXHYNxdDEhgYUUnYpffkdxmqFelspGMZz+Io2qQ+51v9/wDw7KkwZflxlElIKgTnPJNcH7mz
+Asjbi1smU8QouE/PBH2pd1DreyOwnojMGPIK8+tLe3HGAfrSE9cVrjtJjFfozwv1bfpnj+VOaf40
+so3DETv+RReF5m53LUNis0Bp9ExK3QkAoQ5nPfisq1druXd3CmMVtsDITlXOPn3pcMGS/HW84VKd
+zwF9SKFKCs7T27U/pvjqaju7Mm6jW2uMdCE4tsukyI5cmY77sdtYSt4DICuoBNMFoWiapJcVhY6o
+V7138N9XK0/JWw42l+BIT5cmMv8AK6jv9COxpi1XpBtE2LctJvfi7bOBdbAI8xrH5krHYj370zaf
+R4gqCQwxzOCMJGE9K6A4rm20ttnDysuJ4OBxmq0uWllv08rNIjyOBPRsCg5GJLnODDZQg+s/yqUs
+zJKlqUVHJNSmkqGOZOt1TBvGfZIxkVwWsg1KlaEmT8DhxX7u3dqlStTka/D3Ur2nrylKkfiIEr9z
+IjK/K4g9fvR/xBsyLDqF+IwsrjqSl5rd1CFjcAfkZqVKHYIZOonyclpZz0oeygoUpWetSpWVmz1O
+c6Ol9o9lDoaBIkPMOZS4obTg4URUqUzWAeDE7SVPEYrXrSZb30ORGwhwDG4rUr/M0SXri+SpYcYu
+EiMMcJbVx9alSgtpad27aMw6ai0pjdKFz1nqJuSn/wAtIJIznj+lfQu11VueVdJm9weohwjNSpWj
+UigYAmfsck8wPPlPKz5jzyz33LJoOt1SieSB7VKlGQQDk5n2w35qwCaYLbEQEBwgY7CpUrlphaAC
+3MIkBKc0DuUUKC5CcJIPI96lSh18GH1AyINiI8x9CM4x3Fat4f6okWOY0qKkFv8AKpCgCFp75qVK
+xqfUY+MUENmMmv7bHbDV5tqPJjTFcsK6pVgE4+Kz68xy41vZUEKPvUqUovDyufKjmfrVmYbiHd6n
+cbis+/WpUqUcMZKdF44n/9k=
+
+------=_NextPart_000_001F_01C8D3B6.F05C5270--
+
diff --git a/groups/test/fixtures/mail_handler/ticket_with_attributes.eml b/groups/test/fixtures/mail_handler/ticket_with_attributes.eml
new file mode 100644
index 000000000..118523496
--- /dev/null
+++ b/groups/test/fixtures/mail_handler/ticket_with_attributes.eml
@@ -0,0 +1,43 @@
+Return-Path: <jsmith@somenet.foo>
+Received: from osiris ([127.0.0.1])
+ by OSIRIS
+ with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
+From: "John Smith" <jsmith@somenet.foo>
+To: <redmine@somenet.foo>
+Subject: New ticket on a given project
+Date: Sun, 22 Jun 2008 12:28:07 +0200
+MIME-Version: 1.0
+Content-Type: text/plain;
+ format=flowed;
+ charset="iso-8859-1";
+ reply-type=original
+Content-Transfer-Encoding: 7bit
+X-Priority: 3
+X-MSMail-Priority: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
+platea dictumst.
+
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
+
+Project: onlinestore
+Tracker: Feature request
+category: Stock management
+priority: Urgent
diff --git a/groups/test/fixtures/members.yml b/groups/test/fixtures/members.yml
index 4e0a5a739..186c35506 100644
--- a/groups/test/fixtures/members.yml
+++ b/groups/test/fixtures/members.yml
@@ -34,7 +34,7 @@ members_005:
project_id: 1
role_id: 3
principal_type: Group
- principal_id: 2 # Clients
+ principal_id: 2
members_006:
id: 6
created_on: 2008-01-19 19:35:36 +02:00
@@ -43,4 +43,12 @@ members_006:
principal_type: User
principal_id: 7
inherited_from: 5
+members_007:
+ id: 7
+ created_on: 2006-07-19 19:35:33 +02:00
+ project_id: 5
+ role_id: 1
+ principal_type: User
+ principal_id: 2
+ inherited_from:
\ No newline at end of file
diff --git a/groups/test/fixtures/projects.yml b/groups/test/fixtures/projects.yml
index ad5cf4aa2..8e1b3fe1d 100644
--- a/groups/test/fixtures/projects.yml
+++ b/groups/test/fixtures/projects.yml
@@ -3,7 +3,7 @@ projects_001:
created_on: 2006-07-19 19:13:59 +02:00
name: eCookbook
updated_on: 2006-07-19 22:53:01 +02:00
- projects_count: 2
+ projects_count: 3
id: 1
description: Recipes management application
homepage: http://ecookbook.somenet.foo/
@@ -43,3 +43,15 @@ projects_004:
is_public: true
identifier: subproject2
parent_id: 1
+projects_005:
+ created_on: 2006-07-19 19:15:51 +02:00
+ name: Private child of eCookbook
+ updated_on: 2006-07-19 19:17:07 +02:00
+ projects_count: 0
+ id: 5
+ description: This is a private subproject of a public project
+ homepage: ""
+ is_public: false
+ identifier: private_child
+ parent_id: 1
+ \ No newline at end of file
diff --git a/groups/test/fixtures/projects_trackers.yml b/groups/test/fixtures/projects_trackers.yml
index 8eb7d85ab..31f7f943a 100644
--- a/groups/test/fixtures/projects_trackers.yml
+++ b/groups/test/fixtures/projects_trackers.yml
@@ -38,3 +38,7 @@ projects_trackers_010:
projects_trackers_011:
project_id: 4
tracker_id: 2
+projects_trackers_012:
+ project_id: 1
+ tracker_id: 3
+ \ No newline at end of file
diff --git a/groups/test/fixtures/repositories/filesystem_repository.tar.gz b/groups/test/fixtures/repositories/filesystem_repository.tar.gz
new file mode 100644
index 000000000..7e8a4ac56
--- /dev/null
+++ b/groups/test/fixtures/repositories/filesystem_repository.tar.gz
Binary files differ
diff --git a/groups/test/fixtures/roles.yml b/groups/test/fixtures/roles.yml
index 95240e846..cf52db9e0 100644
--- a/groups/test/fixtures/roles.yml
+++ b/groups/test/fixtures/roles.yml
@@ -15,6 +15,8 @@ manager:
- :add_issue_notes
- :move_issues
- :delete_issues
+ - :view_issue_watchers
+ - :add_issue_watchers
- :manage_public_queries
- :save_queries
- :view_gantt
@@ -29,6 +31,7 @@ manager:
- :manage_documents
- :view_wiki_pages
- :edit_wiki_pages
+ - :protect_wiki_pages
- :delete_wiki_pages
- :rename_wiki_pages
- :add_messages
@@ -57,6 +60,7 @@ developer:
- :add_issue_notes
- :move_issues
- :delete_issues
+ - :view_issue_watchers
- :save_queries
- :view_gantt
- :view_calendar
@@ -69,6 +73,7 @@ developer:
- :manage_documents
- :view_wiki_pages
- :edit_wiki_pages
+ - :protect_wiki_pages
- :delete_wiki_pages
- :add_messages
- :manage_boards
@@ -93,6 +98,7 @@ reporter:
- :manage_issue_relations
- :add_issue_notes
- :move_issues
+ - :view_issue_watchers
- :save_queries
- :view_gantt
- :view_calendar
diff --git a/groups/test/fixtures/versions.yml b/groups/test/fixtures/versions.yml
index bf08660d5..62c5e6f99 100644
--- a/groups/test/fixtures/versions.yml
+++ b/groups/test/fixtures/versions.yml
@@ -14,7 +14,7 @@ versions_002:
updated_on: 2006-07-19 21:00:33 +02:00
id: 2
description: Stable release
- effective_date: 2006-07-19
+ effective_date: <%= 20.day.from_now.to_date.to_s(:db) %>
versions_003:
created_on: 2006-07-19 21:00:33 +02:00
name: "2.0"
diff --git a/groups/test/fixtures/watchers.yml b/groups/test/fixtures/watchers.yml
new file mode 100644
index 000000000..a8c482955
--- /dev/null
+++ b/groups/test/fixtures/watchers.yml
@@ -0,0 +1,6 @@
+---
+watchers_001:
+ watchable_type: Issue
+ watchable_id: 2
+ user_id: 3
+ \ No newline at end of file
diff --git a/groups/test/fixtures/wiki_contents.yml b/groups/test/fixtures/wiki_contents.yml
index 5d6d3f1de..8c53d4d97 100644
--- a/groups/test/fixtures/wiki_contents.yml
+++ b/groups/test/fixtures/wiki_contents.yml
@@ -2,7 +2,7 @@
wiki_contents_001:
text: |-
h1. CookBook documentation
-
+ {{child_pages}}
Some updated [[documentation]] here with gzipped history
updated_on: 2007-03-07 00:10:51 +01:00
page_id: 1
diff --git a/groups/test/fixtures/wiki_pages.yml b/groups/test/fixtures/wiki_pages.yml
index f89832e44..e285441ff 100644
--- a/groups/test/fixtures/wiki_pages.yml
+++ b/groups/test/fixtures/wiki_pages.yml
@@ -4,19 +4,27 @@ wiki_pages_001:
title: CookBook_documentation
id: 1
wiki_id: 1
+ protected: true
+ parent_id:
wiki_pages_002:
created_on: 2007-03-08 00:18:07 +01:00
title: Another_page
id: 2
wiki_id: 1
+ protected: false
+ parent_id:
wiki_pages_003:
created_on: 2007-03-08 00:18:07 +01:00
title: Start_page
id: 3
wiki_id: 2
+ protected: false
+ parent_id:
wiki_pages_004:
created_on: 2007-03-08 00:18:07 +01:00
title: Page_with_an_inline_image
id: 4
wiki_id: 1
+ protected: false
+ parent_id: 1
\ No newline at end of file
diff --git a/groups/test/functional/account_controller_test.rb b/groups/test/functional/account_controller_test.rb
index 666acf0dd..26218d177 100644
--- a/groups/test/functional/account_controller_test.rb
+++ b/groups/test/functional/account_controller_test.rb
@@ -44,6 +44,17 @@ class AccountControllerTest < Test::Unit::TestCase
assert_nil assigns(:user)
end
+ def test_login_should_redirect_to_back_url_param
+ # request.uri is "test.host" in test environment
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.host/issues/show/1'
+ assert_redirected_to '/issues/show/1'
+ end
+
+ def test_login_should_not_redirect_to_another_host
+ post :login, :username => 'jsmith', :password => 'jsmith', :back_url => 'http://test.foo/fake'
+ assert_redirected_to '/my/page'
+ end
+
def test_login_with_wrong_password
post :login, :username => 'admin', :password => 'bad'
assert_response :success
diff --git a/groups/test/functional/attachments_controller_test.rb b/groups/test/functional/attachments_controller_test.rb
new file mode 100644
index 000000000..06a6343ba
--- /dev/null
+++ b/groups/test/functional/attachments_controller_test.rb
@@ -0,0 +1,79 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'attachments_controller'
+
+# Re-raise errors caught by the controller.
+class AttachmentsController; def rescue_action(e) raise e end; end
+
+
+class AttachmentsControllerTest < Test::Unit::TestCase
+ fixtures :users, :projects, :issues, :attachments
+
+ def setup
+ @controller = AttachmentsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ Attachment.storage_path = "#{RAILS_ROOT}/test/fixtures/files"
+ User.current = nil
+ end
+
+ def test_routing
+ assert_routing('/attachments/1', :controller => 'attachments', :action => 'show', :id => '1')
+ assert_routing('/attachments/1/filename.ext', :controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext')
+ assert_routing('/attachments/download/1', :controller => 'attachments', :action => 'download', :id => '1')
+ assert_routing('/attachments/download/1/filename.ext', :controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext')
+ end
+
+ def test_recognizes
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/1')
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1'}, '/attachments/show/1')
+ assert_recognizes({:controller => 'attachments', :action => 'show', :id => '1', :filename => 'filename.ext'}, '/attachments/1/filename.ext')
+ assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1'}, '/attachments/download/1')
+ assert_recognizes({:controller => 'attachments', :action => 'download', :id => '1', :filename => 'filename.ext'},'/attachments/download/1/filename.ext')
+ end
+
+ def test_show_diff
+ get :show, :id => 5
+ assert_response :success
+ assert_template 'diff'
+ end
+
+ def test_show_text_file
+ get :show, :id => 4
+ assert_response :success
+ assert_template 'file'
+ end
+
+ def test_show_other
+ get :show, :id => 6
+ assert_response :success
+ assert_equal 'application/octet-stream', @response.content_type
+ end
+
+ def test_download_text_file
+ get :download, :id => 4
+ assert_response :success
+ assert_equal 'application/x-ruby', @response.content_type
+ end
+
+ def test_anonymous_on_private_private
+ get :download, :id => 7
+ assert_redirected_to 'account/login'
+ end
+end
diff --git a/groups/test/functional/documents_controller_test.rb b/groups/test/functional/documents_controller_test.rb
index f150a5b7a..7c1f0213a 100644
--- a/groups/test/functional/documents_controller_test.rb
+++ b/groups/test/functional/documents_controller_test.rb
@@ -40,6 +40,8 @@ class DocumentsControllerTest < Test::Unit::TestCase
def test_new_with_one_attachment
@request.session[:user_id] = 2
+ set_tmp_attachments_directory
+
post :new, :project_id => 'ecookbook',
:document => { :title => 'DocumentsControllerTest#test_post_new',
:description => 'This is a new document',
diff --git a/groups/test/functional/enumerations_controller.rb b/groups/test/functional/enumerations_controller.rb
new file mode 100644
index 000000000..36ff86720
--- /dev/null
+++ b/groups/test/functional/enumerations_controller.rb
@@ -0,0 +1,61 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'enumerations_controller'
+
+# Re-raise errors caught by the controller.
+class EnumerationsController; def rescue_action(e) raise e end; end
+
+class EnumerationsControllerTest < Test::Unit::TestCase
+ fixtures :enumerations, :issues, :users
+
+ def setup
+ @controller = EnumerationsController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @request.session[:user_id] = 1 # admin
+ end
+
+ def test_index
+ get :index
+ assert_response :success
+ assert_template 'list'
+ end
+
+ def test_destroy_enumeration_not_in_use
+ post :destroy, :id => 7
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(7)
+ end
+
+ def test_destroy_enumeration_in_use
+ post :destroy, :id => 4
+ assert_response :success
+ assert_template 'destroy'
+ assert_not_nil Enumeration.find_by_id(4)
+ end
+
+ def test_destroy_enumeration_in_use_with_reassignment
+ issue = Issue.find(:first, :conditions => {:priority_id => 4})
+ post :destroy, :id => 4, :reassign_to_id => 6
+ assert_redirected_to :controller => 'enumerations', :action => 'index'
+ assert_nil Enumeration.find_by_id(4)
+ # check that the issue was reassign
+ assert_equal 6, issue.reload.priority_id
+ end
+end
diff --git a/groups/test/functional/issues_controller_test.rb b/groups/test/functional/issues_controller_test.rb
index 042a8f3f2..a248d8bde 100644
--- a/groups/test/functional/issues_controller_test.rb
+++ b/groups/test/functional/issues_controller_test.rb
@@ -38,7 +38,9 @@ class IssuesControllerTest < Test::Unit::TestCase
:custom_fields,
:custom_values,
:custom_fields_trackers,
- :time_entries
+ :time_entries,
+ :journals,
+ :journal_details
def setup
@controller = IssuesController.new
@@ -53,13 +55,44 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_template 'index.rhtml'
assert_not_nil assigns(:issues)
assert_nil assigns(:project)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ # private projects hidden
+ assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
+ assert_no_tag :tag => 'a', :content => /Issue on project 2/
end
def test_index_with_project
+ Setting.display_subprojects_issues = 0
get :index, :project_id => 1
assert_response :success
assert_template 'index.rhtml'
assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_no_tag :tag => 'a', :content => /Subproject issue/
+ end
+
+ def test_index_with_project_and_subprojects
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
+ end
+
+ def test_index_with_project_and_subprojects_should_show_private_subprojects
+ @request.session[:user_id] = 2
+ Setting.display_subprojects_issues = 1
+ get :index, :project_id => 1
+ assert_response :success
+ assert_template 'index.rhtml'
+ assert_not_nil assigns(:issues)
+ assert_tag :tag => 'a', :content => /Can't print recipes/
+ assert_tag :tag => 'a', :content => /Subproject issue/
+ assert_tag :tag => 'a', :content => /Issue of a private subproject/
end
def test_index_with_project_and_filter
@@ -137,7 +170,7 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'new'
- assert_tag :tag => 'input', :attributes => { :name => 'custom_fields[2]',
+ assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
:value => 'Default string' }
end
@@ -166,17 +199,20 @@ class IssuesControllerTest < Test::Unit::TestCase
def test_post_new
@request.session[:user_id] = 2
post :new, :project_id => 1,
- :issue => {:tracker_id => 1,
+ :issue => {:tracker_id => 3,
:subject => 'This is the test_new issue',
:description => 'This is the description',
- :priority_id => 5},
- :custom_fields => {'2' => 'Value for field 2'}
+ :priority_id => 5,
+ :estimated_hours => '',
+ :custom_field_values => {'2' => 'Value for field 2'}}
assert_redirected_to 'issues/show'
issue = Issue.find_by_subject('This is the test_new issue')
assert_not_nil issue
assert_equal 2, issue.author_id
- v = issue.custom_values.find_by_custom_field_id(2)
+ assert_equal 3, issue.tracker_id
+ assert_nil issue.estimated_hours
+ v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
assert_not_nil v
assert_equal 'Value for field 2', v.value
end
@@ -191,6 +227,50 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_redirected_to 'issues/show'
end
+ def test_post_new_with_required_custom_field_and_without_custom_fields_param
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:is_required, true)
+
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ :description => 'This is the description',
+ :priority_id => 5}
+ assert_response :success
+ assert_template 'new'
+ issue = assigns(:issue)
+ assert_not_nil issue
+ assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+ end
+
+ def test_post_should_preserve_fields_values_on_validation_failure
+ @request.session[:user_id] = 2
+ post :new, :project_id => 1,
+ :issue => {:tracker_id => 1,
+ :subject => 'This is the test_new issue',
+ # empty description
+ :description => '',
+ :priority_id => 6,
+ :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
+ assert_response :success
+ assert_template 'new'
+
+ assert_tag :input, :attributes => { :name => 'issue[subject]',
+ :value => 'This is the test_new issue' }
+ assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
+ :child => { :tag => 'option', :attributes => { :selected => 'selected',
+ :value => '6' },
+ :content => 'High' }
+ # Custom fields
+ assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
+ :child => { :tag => 'option', :attributes => { :selected => 'selected',
+ :value => 'Oracle' },
+ :content => 'Oracle' }
+ assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
+ :value => 'Value for field 2'}
+ end
+
def test_copy_issue
@request.session[:user_id] = 2
get :new, :project_id => 1, :copy_from => 1
@@ -230,19 +310,43 @@ class IssuesControllerTest < Test::Unit::TestCase
:content => 'Urgent',
:attributes => { :selected => 'selected' } }
end
+
+ def test_reply_to_issue
+ @request.session[:user_id] = 2
+ get :reply, :id => 1
+ assert_response :success
+ assert_select_rjs :show, "update"
+ end
+
+ def test_reply_to_note
+ @request.session[:user_id] = 2
+ get :reply, :id => 1, :journal_id => 2
+ assert_response :success
+ assert_select_rjs :show, "update"
+ end
- def test_post_edit
+ def test_post_edit_without_custom_fields_param
@request.session[:user_id] = 2
ActionMailer::Base.deliveries.clear
issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
old_subject = issue.subject
new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
- post :edit, :id => 1, :issue => {:subject => new_subject}
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 2) do
+ post :edit, :id => 1, :issue => {:subject => new_subject,
+ :priority_id => '6',
+ :category_id => '1' # no change
+ }
+ end
+ end
assert_redirected_to 'issues/show/1'
issue.reload
assert_equal new_subject, issue.subject
+ # Make sure custom fields were not cleared
+ assert_equal '125', issue.custom_value_for(2).value
mail = ActionMailer::Base.deliveries.last
assert_kind_of TMail::Mail, mail
@@ -250,14 +354,40 @@ class IssuesControllerTest < Test::Unit::TestCase
assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
end
+ def test_post_edit_with_custom_field_change
+ @request.session[:user_id] = 2
+ issue = Issue.find(1)
+ assert_equal '125', issue.custom_value_for(2).value
+
+ assert_difference('Journal.count') do
+ assert_difference('JournalDetail.count', 3) do
+ post :edit, :id => 1, :issue => {:subject => 'Custom field change',
+ :priority_id => '6',
+ :category_id => '1', # no change
+ :custom_field_values => { '2' => 'New custom value' }
+ }
+ end
+ end
+ assert_redirected_to 'issues/show/1'
+ issue.reload
+ assert_equal 'New custom value', issue.custom_value_for(2).value
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_kind_of TMail::Mail, mail
+ assert mail.body.include?("Searchable field changed from 125 to New custom value")
+ end
+
def test_post_edit_with_status_and_assignee_change
issue = Issue.find(1)
assert_equal 1, issue.status_id
@request.session[:user_id] = 2
- post :edit,
- :id => 1,
- :issue => { :status_id => 2, :assigned_to_id => 3 },
- :notes => 'Assigned to dlopper'
+ assert_difference('TimeEntry.count', 0) do
+ post :edit,
+ :id => 1,
+ :issue => { :status_id => 2, :assigned_to_id => 3 },
+ :notes => 'Assigned to dlopper',
+ :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
+ end
assert_redirected_to 'issues/show/1'
issue.reload
assert_equal 2, issue.status_id
@@ -288,10 +418,12 @@ class IssuesControllerTest < Test::Unit::TestCase
def test_post_edit_with_note_and_spent_time
@request.session[:user_id] = 2
spent_hours_before = Issue.find(1).spent_hours
- post :edit,
- :id => 1,
- :notes => '2.5 hours added',
- :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
+ assert_difference('TimeEntry.count') do
+ post :edit,
+ :id => 1,
+ :notes => '2.5 hours added',
+ :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
+ end
assert_redirected_to 'issues/show/1'
issue = Issue.find(1)
@@ -307,6 +439,8 @@ class IssuesControllerTest < Test::Unit::TestCase
end
def test_post_edit_with_attachment_only
+ set_tmp_attachments_directory
+
# anonymous user
post :edit,
:id => 1,
@@ -398,10 +532,10 @@ class IssuesControllerTest < Test::Unit::TestCase
:attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
:class => '' }
assert_tag :tag => 'a', :content => 'Immediate',
- :attributes => { :href => '/issues/edit/1?issue%5Bpriority_id%5D=8',
+ :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
:class => '' }
assert_tag :tag => 'a', :content => 'Dave Lopper',
- :attributes => { :href => '/issues/edit/1?issue%5Bassigned_to_id%5D=3',
+ :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
:class => '' }
assert_tag :tag => 'a', :content => 'Copy',
:attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
@@ -431,6 +565,12 @@ class IssuesControllerTest < Test::Unit::TestCase
assert_tag :tag => 'a', :content => 'Edit',
:attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
:class => 'icon-edit' }
+ assert_tag :tag => 'a', :content => 'Immediate',
+ :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
+ :class => '' }
+ assert_tag :tag => 'a', :content => 'Dave Lopper',
+ :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
+ :class => '' }
assert_tag :tag => 'a', :content => 'Move',
:attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
:class => 'icon-move' }
diff --git a/groups/test/functional/mail_handler_controller_test.rb b/groups/test/functional/mail_handler_controller_test.rb
new file mode 100644
index 000000000..6c5af23f0
--- /dev/null
+++ b/groups/test/functional/mail_handler_controller_test.rb
@@ -0,0 +1,53 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'mail_handler_controller'
+
+# Re-raise errors caught by the controller.
+class MailHandlerController; def rescue_action(e) raise e end; end
+
+class MailHandlerControllerTest < Test::Unit::TestCase
+ fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :issue_statuses, :trackers, :enumerations
+
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
+
+ def setup
+ @controller = MailHandlerController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_should_create_issue
+ # Enable API and set a key
+ Setting.mail_handler_api_enabled = 1
+ Setting.mail_handler_api_key = 'secret'
+
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ assert_response 201
+ end
+
+ def test_should_not_allow
+ # Disable API
+ Setting.mail_handler_api_enabled = 0
+ Setting.mail_handler_api_key = 'secret'
+
+ post :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
+ assert_response 403
+ end
+end
diff --git a/groups/test/functional/messages_controller_test.rb b/groups/test/functional/messages_controller_test.rb
index 1fe8d086a..b1b3ea942 100644
--- a/groups/test/functional/messages_controller_test.rb
+++ b/groups/test/functional/messages_controller_test.rb
@@ -40,6 +40,15 @@ class MessagesControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:topic)
end
+ def test_show_with_reply_permission
+ @request.session[:user_id] = 2
+ get :show, :board_id => 1, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :div, :attributes => { :id => 'reply' },
+ :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
+ end
+
def test_show_message_not_found
get :show, :board_id => 1, :id => 99999
assert_response 404
@@ -108,4 +117,11 @@ class MessagesControllerTest < Test::Unit::TestCase
assert_redirected_to 'boards/show'
assert_nil Message.find_by_id(1)
end
+
+ def test_quote
+ @request.session[:user_id] = 2
+ xhr :get, :quote, :board_id => 1, :id => 3
+ assert_response :success
+ assert_select_rjs :show, 'reply'
+ end
end
diff --git a/groups/test/functional/projects_controller_test.rb b/groups/test/functional/projects_controller_test.rb
index eb5795152..03773ccdb 100644
--- a/groups/test/functional/projects_controller_test.rb
+++ b/groups/test/functional/projects_controller_test.rb
@@ -29,24 +29,27 @@ class ProjectsControllerTest < Test::Unit::TestCase
@controller = ProjectsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
+ @request.session[:user_id] = nil
end
def test_index
get :index
assert_response :success
- assert_template 'list'
- end
-
- def test_list
- get :list
- assert_response :success
- assert_template 'list'
+ assert_template 'index'
assert_not_nil assigns(:project_tree)
# Root project as hash key
- assert assigns(:project_tree).has_key?(Project.find(1))
+ assert assigns(:project_tree).keys.include?(Project.find(1))
# Subproject in corresponding value
assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3))
end
+
+ def test_index_atom
+ get :index, :format => 'atom'
+ assert_response :success
+ assert_template 'common/feed.atom.rxml'
+ assert_select 'feed>title', :text => 'Redmine: Latest projects'
+ assert_select 'feed>entry', :count => Project.count(:conditions => Project.visible_by(User.current))
+ end
def test_show_by_id
get :show, :id => 1
@@ -63,6 +66,21 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_equal Project.find_by_identifier('ecookbook'), assigns(:project)
end
+ def test_private_subprojects_hidden
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_no_tag :tag => 'a', :content => /Private child/
+ end
+
+ def test_private_subprojects_visible
+ @request.session[:user_id] = 2 # manager who is a member of the private subproject
+ get :show, :id => 'ecookbook'
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'a', :content => /Private child/
+ end
+
def test_settings
@request.session[:user_id] = 2 # manager
get :settings, :id => 1
@@ -73,7 +91,7 @@ class ProjectsControllerTest < Test::Unit::TestCase
def test_edit
@request.session[:user_id] = 2 # manager
post :edit, :id => 1, :project => {:name => 'Test changed name',
- :custom_field_ids => ['']}
+ :issue_custom_field_ids => ['']}
assert_redirected_to 'projects/settings/ecookbook'
project = Project.find(1)
assert_equal 'Test changed name', project.name
@@ -135,22 +153,20 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'activity'
assert_not_nil assigns(:events_by_day)
- assert_not_nil assigns(:events)
-
- # subproject issue not included by default
- assert !assigns(:events).include?(Issue.find(5))
assert_tag :tag => "h3",
:content => /#{2.days.ago.to_date.day}/,
:sibling => { :tag => "dl",
:child => { :tag => "dt",
- :attributes => { :class => 'issue-edit' },
+ :attributes => { :class => /issue-edit/ },
:child => { :tag => "a",
:content => /(#{IssueStatus.find(2).name})/,
}
}
}
-
+ end
+
+ def test_previous_project_activity
get :activity, :id => 1, :from => 3.days.ago.to_date
assert_response :success
assert_template 'activity'
@@ -160,7 +176,7 @@ class ProjectsControllerTest < Test::Unit::TestCase
:content => /#{3.day.ago.to_date.day}/,
:sibling => { :tag => "dl",
:child => { :tag => "dt",
- :attributes => { :class => 'issue' },
+ :attributes => { :class => /issue/ },
:child => { :tag => "a",
:content => /#{Issue.find(1).subject}/,
}
@@ -168,53 +184,30 @@ class ProjectsControllerTest < Test::Unit::TestCase
}
end
- def test_activity_with_subprojects
- get :activity, :id => 1, :with_subprojects => 1
- assert_response :success
- assert_template 'activity'
- assert_not_nil assigns(:events)
-
- assert assigns(:events).include?(Issue.find(1))
- assert !assigns(:events).include?(Issue.find(4))
- # subproject issue
- assert assigns(:events).include?(Issue.find(5))
- end
-
- def test_global_activity_anonymous
+ def test_global_activity
get :activity
assert_response :success
assert_template 'activity'
- assert_not_nil assigns(:events)
+ assert_not_nil assigns(:events_by_day)
- assert assigns(:events).include?(Issue.find(1))
- # Issue of a private project
- assert !assigns(:events).include?(Issue.find(4))
+ assert_tag :tag => "h3",
+ :content => /#{5.day.ago.to_date.day}/,
+ :sibling => { :tag => "dl",
+ :child => { :tag => "dt",
+ :attributes => { :class => /issue/ },
+ :child => { :tag => "a",
+ :content => /#{Issue.find(5).subject}/,
+ }
+ }
+ }
end
- def test_global_activity_logged_user
- @request.session[:user_id] = 2 # manager
- get :activity
+ def test_activity_atom_feed
+ get :activity, :format => 'atom'
assert_response :success
- assert_template 'activity'
- assert_not_nil assigns(:events)
-
- assert assigns(:events).include?(Issue.find(1))
- # Issue of a private project the user belongs to
- assert assigns(:events).include?(Issue.find(4))
+ assert_template 'common/feed.atom.rxml'
end
-
- def test_global_activity_with_all_types
- get :activity, :show_issues => 1, :show_news => 1, :show_files => 1, :show_documents => 1, :show_changesets => 1, :show_wiki_pages => 1, :show_messages => 1
- assert_response :success
- assert_template 'activity'
- assert_not_nil assigns(:events)
-
- assert assigns(:events).include?(Issue.find(1))
- assert !assigns(:events).include?(Issue.find(4))
- assert assigns(:events).include?(Message.find(5))
- end
-
def test_calendar
get :calendar, :id => 1
assert_response :success
@@ -222,27 +215,56 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:calendar)
end
- def test_calendar_with_subprojects
+ def test_calendar_with_subprojects_should_not_show_private_subprojects
+ get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
+ assert_response :success
+ assert_template 'calendar'
+ assert_not_nil assigns(:calendar)
+ assert_no_tag :tag => 'a', :content => /#6/
+ end
+
+ def test_calendar_with_subprojects_should_show_private_subprojects
+ @request.session[:user_id] = 2
get :calendar, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
assert_response :success
assert_template 'calendar'
assert_not_nil assigns(:calendar)
+ assert_tag :tag => 'a', :content => /#6/
end
def test_gantt
get :gantt, :id => 1
assert_response :success
assert_template 'gantt.rhtml'
- assert_not_nil assigns(:events)
+ events = assigns(:events)
+ assert_not_nil events
+ # Issue with start and due dates
+ i = Issue.find(1)
+ assert_not_nil i.due_date
+ assert events.include?(Issue.find(1))
+ # Issue with without due date but targeted to a version with date
+ i = Issue.find(2)
+ assert_nil i.due_date
+ assert events.include?(i)
end
- def test_gantt_with_subprojects
+ def test_gantt_with_subprojects_should_not_show_private_subprojects
get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
assert_response :success
assert_template 'gantt.rhtml'
assert_not_nil assigns(:events)
+ assert_no_tag :tag => 'a', :content => /#6/
end
+ def test_gantt_with_subprojects_should_show_private_subprojects
+ @request.session[:user_id] = 2
+ get :gantt, :id => 1, :with_subprojects => 1, :tracker_ids => [1, 2]
+ assert_response :success
+ assert_template 'gantt.rhtml'
+ assert_not_nil assigns(:events)
+ assert_tag :tag => 'a', :content => /#6/
+ end
+
def test_gantt_export_to_pdf
get :gantt, :id => 1, :format => 'pdf'
assert_response :success
@@ -265,4 +287,33 @@ class ProjectsControllerTest < Test::Unit::TestCase
assert_redirected_to 'admin/projects'
assert Project.find(1).active?
end
+
+ def test_project_menu
+ assert_no_difference 'Redmine::MenuManager.items(:project_menu).size' do
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.push :foo, { :controller => 'projects', :action => 'show' }, :cation => 'Foo'
+ menu.push :bar, { :controller => 'projects', :action => 'show' }, :before => :activity
+ menu.push :hello, { :controller => 'projects', :action => 'show' }, :caption => Proc.new {|p| p.name.upcase }, :after => :bar
+ end
+
+ get :show, :id => 1
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Foo' } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'Bar' },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' } } }
+
+ assert_tag :div, :attributes => { :id => 'main-menu' },
+ :descendant => { :tag => 'li', :child => { :tag => 'a', :content => 'ECOOKBOOK' },
+ :before => { :tag => 'li', :child => { :tag => 'a', :content => 'Activity' } } }
+
+ # Remove the menu items
+ Redmine::MenuManager.map :project_menu do |menu|
+ menu.delete :foo
+ menu.delete :bar
+ menu.delete :hello
+ end
+ end
+ end
end
diff --git a/groups/test/functional/repositories_controller_test.rb b/groups/test/functional/repositories_controller_test.rb
index 47455dc55..2892f3bd1 100644
--- a/groups/test/functional/repositories_controller_test.rb
+++ b/groups/test/functional/repositories_controller_test.rb
@@ -43,10 +43,10 @@ class RepositoriesControllerTest < Test::Unit::TestCase
assert_response :success
assert_template 'revision'
assert_no_tag :tag => "div", :attributes => { :class => "contextual" },
- :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=0'}
+ :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook/0'}
}
assert_tag :tag => "div", :attributes => { :class => "contextual" },
- :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook?rev=2'}
+ :child => { :tag => "a", :attributes => { :href => '/repositories/revision/ecookbook/2'}
}
end
diff --git a/groups/test/functional/repositories_cvs_controller_test.rb b/groups/test/functional/repositories_cvs_controller_test.rb
index e12bb53ac..2207d6ab6 100644
--- a/groups/test/functional/repositories_cvs_controller_test.rb
+++ b/groups/test/functional/repositories_cvs_controller_test.rb
@@ -25,7 +25,7 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase
# No '..' in the repository path
REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
- REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
# CVS module
MODULE_NAME = 'test'
@@ -89,6 +89,19 @@ class RepositoriesCvsControllerTest < Test::Unit::TestCase
get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb']
assert_response :success
assert_template 'entry'
+ assert_no_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /before_filter/
+ end
+
+ def test_entry_at_given_revision
+ # changesets must be loaded
+ Project.find(1).repository.fetch_changesets
+ get :entry, :id => 1, :path => ['sources', 'watchers_controller.rb'], :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3
+ assert_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /before_filter/
end
def test_entry_not_found
diff --git a/groups/test/functional/repositories_git_controller_test.rb b/groups/test/functional/repositories_git_controller_test.rb
index 339e22897..201a50677 100644
--- a/groups/test/functional/repositories_git_controller_test.rb
+++ b/groups/test/functional/repositories_git_controller_test.rb
@@ -26,7 +26,7 @@ class RepositoriesGitControllerTest < Test::Unit::TestCase
# No '..' in the repository path
REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
- REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
def setup
@controller = RepositoriesController.new
diff --git a/groups/test/functional/repositories_subversion_controller_test.rb b/groups/test/functional/repositories_subversion_controller_test.rb
index dd56947fc..35dfbb1a1 100644
--- a/groups/test/functional/repositories_subversion_controller_test.rb
+++ b/groups/test/functional/repositories_subversion_controller_test.rb
@@ -71,6 +71,19 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase
assert_not_nil assigns(:entries)
assert_equal ['folder', '.project', 'helloworld.c', 'helloworld.rb', 'textfile.txt'], assigns(:entries).collect(&:name)
end
+
+ def test_changes
+ get :changes, :id => 1, :path => ['subversion_test', 'folder', 'helloworld.rb' ]
+ assert_response :success
+ assert_template 'changes'
+ # svn properties
+ assert_not_nil assigns(:properties)
+ assert_equal 'native', assigns(:properties)['svn:eol-style']
+ assert_tag :ul,
+ :child => { :tag => 'li',
+ :child => { :tag => 'b', :content => 'svn:eol-style' },
+ :child => { :tag => 'span', :content => 'native' } }
+ end
def test_entry
get :entry, :id => 1, :path => ['subversion_test', 'helloworld.c']
@@ -78,6 +91,15 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase
assert_template 'entry'
end
+ def test_entry_at_given_revision
+ get :entry, :id => 1, :path => ['subversion_test', 'helloworld.rb'], :rev => 2
+ assert_response :success
+ assert_template 'entry'
+ # this line was removed in r3 and file was moved in r6
+ assert_tag :tag => 'td', :attributes => { :class => /line-code/},
+ :content => /Here's the code/
+ end
+
def test_entry_not_found
get :entry, :id => 1, :path => ['subversion_test', 'zzz.c']
assert_tag :tag => 'div', :attributes => { :class => /error/ },
@@ -97,6 +119,37 @@ class RepositoriesSubversionControllerTest < Test::Unit::TestCase
assert_equal 'folder', assigns(:entry).name
end
+ def test_revision
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+ assert_tag :tag => 'tr',
+ :child => { :tag => 'td',
+ # link to the entry at rev 2
+ :child => { :tag => 'a', :attributes => {:href => 'repositories/entry/ecookbook/test/some/path/in/the/repo?rev=2'},
+ :content => %r{/test/some/path/in/the/repo} }
+ },
+ :child => { :tag => 'td',
+ # link to partial diff
+ :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/test/some/path/in/the/repo?rev=2' } }
+ }
+ end
+
+ def test_revision_with_repository_pointing_to_a_subdirectory
+ r = Project.find(1).repository
+ # Changes repository url to a subdirectory
+ r.update_attribute :url, (r.url + '/test/some')
+
+ get :revision, :id => 1, :rev => 2
+ assert_response :success
+ assert_template 'revision'
+ assert_tag :tag => 'tr',
+ :child => { :tag => 'td', :content => %r{/test/some/path/in/the/repo} },
+ :child => { :tag => 'td',
+ :child => { :tag => 'a', :attributes => { :href => '/repositories/diff/ecookbook/path/in/the/repo?rev=2' } }
+ }
+ end
+
def test_diff
get :diff, :id => 1, :rev => 3
assert_response :success
diff --git a/groups/test/functional/search_controller_test.rb b/groups/test/functional/search_controller_test.rb
index 49004c7e6..ce06ec298 100644
--- a/groups/test/functional/search_controller_test.rb
+++ b/groups/test/functional/search_controller_test.rb
@@ -5,7 +5,10 @@ require 'search_controller'
class SearchController; def rescue_action(e) raise e end; end
class SearchControllerTest < Test::Unit::TestCase
- fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values
+ fixtures :projects, :enabled_modules, :roles, :users,
+ :issues, :trackers, :issue_statuses,
+ :custom_fields, :custom_values,
+ :repositories, :changesets
def setup
@controller = SearchController.new
@@ -25,6 +28,31 @@ class SearchControllerTest < Test::Unit::TestCase
assert assigns(:results).include?(Project.find(1))
end
+ def test_search_all_projects
+ get :index, :q => 'recipe subproject commit', :submit => 'Search'
+ assert_response :success
+ assert_template 'index'
+
+ assert assigns(:results).include?(Issue.find(2))
+ assert assigns(:results).include?(Issue.find(5))
+ assert assigns(:results).include?(Changeset.find(101))
+ assert_tag :dt, :attributes => { :class => /issue/ },
+ :child => { :tag => 'a', :content => /Add ingredients categories/ },
+ :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
+
+ assert assigns(:results_by_type).is_a?(Hash)
+ assert_equal 4, assigns(:results_by_type)['changesets']
+ assert_tag :a, :content => 'Changesets (4)'
+ end
+
+ def test_search_project_and_subprojects
+ get :index, :id => 1, :q => 'recipe subproject', :scope => 'subprojects', :submit => 'Search'
+ assert_response :success
+ assert_template 'index'
+ assert assigns(:results).include?(Issue.find(1))
+ assert assigns(:results).include?(Issue.find(5))
+ end
+
def test_search_without_searchable_custom_fields
CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
diff --git a/groups/test/functional/timelog_controller_test.rb b/groups/test/functional/timelog_controller_test.rb
index e80a67728..7b4622daa 100644
--- a/groups/test/functional/timelog_controller_test.rb
+++ b/groups/test/functional/timelog_controller_test.rb
@@ -30,11 +30,22 @@ class TimelogControllerTest < Test::Unit::TestCase
@response = ActionController::TestResponse.new
end
- def test_create
+ def test_get_edit
+ @request.session[:user_id] = 3
+ get :edit, :project_id => 1
+ assert_response :success
+ assert_template 'edit'
+ # Default activity selected
+ assert_tag :tag => 'option', :attributes => { :selected => 'selected' },
+ :content => 'Development'
+ end
+
+ def test_post_edit
@request.session[:user_id] = 3
post :edit, :project_id => 1,
:time_entry => {:comments => 'Some work on TimelogControllerTest',
- :activity_id => '10',
+ # Not the default activity
+ :activity_id => '11',
:spent_on => '2008-03-14',
:issue_id => '1',
:hours => '7.3'}
@@ -43,6 +54,7 @@ class TimelogControllerTest < Test::Unit::TestCase
i = Issue.find(1)
t = TimeEntry.find_by_comments('Some work on TimelogControllerTest')
assert_not_nil t
+ assert_equal 11, t.activity_id
assert_equal 7.3, t.hours
assert_equal 3, t.user_id
assert_equal i, t.issue
@@ -198,6 +210,14 @@ class TimelogControllerTest < Test::Unit::TestCase
assert_equal '2007-04-22'.to_date, assigns(:to)
end
+ def test_details_atom_feed
+ get :details, :project_id => 1, :format => 'atom'
+ assert_response :success
+ assert_equal 'application/atom+xml', @response.content_type
+ assert_not_nil assigns(:items)
+ assert assigns(:items).first.is_a?(TimeEntry)
+ end
+
def test_details_csv_export
get :details, :project_id => 1, :format => 'csv'
assert_response :success
diff --git a/groups/test/functional/versions_controller_test.rb b/groups/test/functional/versions_controller_test.rb
index 3477c5edd..3a118701a 100644
--- a/groups/test/functional/versions_controller_test.rb
+++ b/groups/test/functional/versions_controller_test.rb
@@ -22,7 +22,7 @@ require 'versions_controller'
class VersionsController; def rescue_action(e) raise e end; end
class VersionsControllerTest < Test::Unit::TestCase
- fixtures :projects, :versions, :users, :roles, :members, :enabled_modules
+ fixtures :projects, :versions, :issues, :users, :roles, :members, :enabled_modules
def setup
@controller = VersionsController.new
@@ -60,9 +60,9 @@ class VersionsControllerTest < Test::Unit::TestCase
def test_destroy
@request.session[:user_id] = 2
- post :destroy, :id => 2
+ post :destroy, :id => 3
assert_redirected_to 'projects/settings/ecookbook'
- assert_nil Version.find_by_id(2)
+ assert_nil Version.find_by_id(3)
end
def test_issue_status_by
diff --git a/groups/test/functional/watchers_controller_test.rb b/groups/test/functional/watchers_controller_test.rb
new file mode 100644
index 000000000..cd6539410
--- /dev/null
+++ b/groups/test/functional/watchers_controller_test.rb
@@ -0,0 +1,70 @@
+# Redmine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'watchers_controller'
+
+# Re-raise errors caught by the controller.
+class WatchersController; def rescue_action(e) raise e end; end
+
+class WatchersControllerTest < Test::Unit::TestCase
+ fixtures :projects, :users, :roles, :members, :enabled_modules,
+ :issues, :trackers, :projects_trackers, :issue_statuses, :enumerations, :watchers
+
+ def setup
+ @controller = WatchersController.new
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ User.current = nil
+ end
+
+ def test_get_watch_should_be_invalid
+ @request.session[:user_id] = 3
+ get :watch, :object_type => 'issue', :object_id => '1'
+ assert_response 405
+ end
+
+ def test_watch
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count') do
+ xhr :post, :watch, :object_type => 'issue', :object_id => '1'
+ assert_response :success
+ assert_select_rjs :replace_html, 'watcher'
+ end
+ assert Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_unwatch
+ @request.session[:user_id] = 3
+ assert_difference('Watcher.count', -1) do
+ xhr :post, :unwatch, :object_type => 'issue', :object_id => '2'
+ assert_response :success
+ assert_select_rjs :replace_html, 'watcher'
+ end
+ assert !Issue.find(1).watched_by?(User.find(3))
+ end
+
+ def test_new_watcher
+ @request.session[:user_id] = 2
+ assert_difference('Watcher.count') do
+ xhr :post, :new, :object_type => 'issue', :object_id => '2', :watcher => {:user_id => '4'}
+ assert_response :success
+ assert_select_rjs :replace_html, 'watchers'
+ end
+ assert Issue.find(2).watched_by?(User.find(4))
+ end
+end
diff --git a/groups/test/functional/welcome_controller_test.rb b/groups/test/functional/welcome_controller_test.rb
index 18146c6aa..df565a751 100644
--- a/groups/test/functional/welcome_controller_test.rb
+++ b/groups/test/functional/welcome_controller_test.rb
@@ -46,4 +46,18 @@ class WelcomeControllerTest < Test::Unit::TestCase
get :index
assert_equal :fr, @controller.current_language
end
+
+ def test_browser_language_alternate
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-TW'
+ get :index
+ assert_equal :"zh-tw", @controller.current_language
+ end
+
+ def test_browser_language_alternate_not_valid
+ Setting.default_language = 'en'
+ @request.env['HTTP_ACCEPT_LANGUAGE'] = 'fr-CA'
+ get :index
+ assert_equal :fr, @controller.current_language
+ end
end
diff --git a/groups/test/functional/wiki_controller_test.rb b/groups/test/functional/wiki_controller_test.rb
index bf31e6614..b5325357c 100644
--- a/groups/test/functional/wiki_controller_test.rb
+++ b/groups/test/functional/wiki_controller_test.rb
@@ -32,10 +32,16 @@ class WikiControllerTest < Test::Unit::TestCase
end
def test_show_start_page
- get :index, :id => 1
+ get :index, :id => 'ecookbook'
assert_response :success
assert_template 'show'
assert_tag :tag => 'h1', :content => /CookBook documentation/
+
+ # child_pages macro
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } }
end
def test_show_page_with_name
@@ -86,14 +92,35 @@ class WikiControllerTest < Test::Unit::TestCase
assert_tag :tag => 'strong', :content => /previewed text/
end
+ def test_preview_new_page
+ @request.session[:user_id] = 2
+ xhr :post, :preview, :id => 1, :page => 'New page',
+ :content => { :text => 'h1. New page',
+ :comments => '',
+ :version => 0 }
+ assert_response :success
+ assert_template 'common/_preview'
+ assert_tag :tag => 'h1', :content => /New page/
+ end
+
def test_history
get :history, :id => 1, :page => 'CookBook_documentation'
assert_response :success
assert_template 'history'
assert_not_nil assigns(:versions)
assert_equal 3, assigns(:versions).size
+ assert_select "input[type=submit][name=commit]"
end
-
+
+ def test_history_with_one_version
+ get :history, :id => 1, :page => 'Another_page'
+ assert_response :success
+ assert_template 'history'
+ assert_not_nil assigns(:versions)
+ assert_equal 1, assigns(:versions).size
+ assert_select "input[type=submit][name=commit]", false
+ end
+
def test_diff
get :diff, :id => 1, :page => 'CookBook_documentation', :version => 2, :version_from => 1
assert_response :success
@@ -152,12 +179,76 @@ class WikiControllerTest < Test::Unit::TestCase
pages = assigns(:pages)
assert_not_nil pages
assert_equal Project.find(1).wiki.pages.size, pages.size
- assert_tag :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
- :content => /CookBook documentation/
+
+ assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/CookBook_documentation' },
+ :content => 'CookBook documentation' },
+ :child => { :tag => 'ul',
+ :child => { :tag => 'li',
+ :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Page_with_an_inline_image' },
+ :content => 'Page with an inline image' } } } },
+ :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/wiki/ecookbook/Another_page' },
+ :content => 'Another page' } }
end
def test_not_found
get :index, :id => 999
assert_response 404
end
+
+ def test_protect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
+ assert !page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :id => 1, :page => page.title, :protected => '1'
+ assert_redirected_to 'wiki/ecookbook/Another_page'
+ assert page.reload.protected?
+ end
+
+ def test_unprotect_page
+ page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
+ assert page.protected?
+ @request.session[:user_id] = 2
+ post :protect, :id => 1, :page => page.title, :protected => '0'
+ assert_redirected_to 'wiki/ecookbook'
+ assert !page.reload.protected?
+ end
+
+ def test_show_page_with_edit_link
+ @request.session[:user_id] = 2
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
+ end
+
+ def test_show_page_without_edit_link
+ @request.session[:user_id] = 4
+ get :index, :id => 1
+ assert_response :success
+ assert_template 'show'
+ assert_no_tag :tag => 'a', :attributes => { :href => '/wiki/1/CookBook_documentation/edit' }
+ end
+
+ def test_edit_unprotected_page
+ # Non members can edit unprotected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :id => 1, :page => 'Another_page'
+ assert_response :success
+ assert_template 'edit'
+ end
+
+ def test_edit_protected_page_by_nonmember
+ # Non members can't edit protected wiki pages
+ @request.session[:user_id] = 4
+ get :edit, :id => 1, :page => 'CookBook_documentation'
+ assert_response 403
+ end
+
+ def test_edit_protected_page_by_member
+ @request.session[:user_id] = 2
+ get :edit, :id => 1, :page => 'CookBook_documentation'
+ assert_response :success
+ assert_template 'edit'
+ end
end
diff --git a/groups/test/integration/account_test.rb b/groups/test/integration/account_test.rb
index e9d665d19..c349200d3 100644
--- a/groups/test/integration/account_test.rb
+++ b/groups/test/integration/account_test.rb
@@ -17,6 +17,12 @@
require "#{File.dirname(__FILE__)}/../test_helper"
+begin
+ require 'mocha'
+rescue
+ # Won't run some tests
+end
+
class AccountTest < ActionController::IntegrationTest
fixtures :users
@@ -67,8 +73,12 @@ class AccountTest < ActionController::IntegrationTest
post 'account/register', :user => {:login => "newuser", :language => "en", :firstname => "New", :lastname => "User", :mail => "newuser@foo.bar"},
:password => "newpass", :password_confirmation => "newpass"
- assert_redirected_to 'account/login'
- log_user('newuser', 'newpass')
+ assert_redirected_to 'my/account'
+ follow_redirect!
+ assert_response :success
+ assert_template 'my/account'
+
+ assert User.find_by_login('newuser').active?
end
def test_register_with_manual_activation
@@ -98,4 +108,46 @@ class AccountTest < ActionController::IntegrationTest
assert_redirected_to 'account/login'
log_user('newuser', 'newpass')
end
+
+ if Object.const_defined?(:Mocha)
+
+ def test_onthefly_registration
+ # disable registration
+ Setting.self_registration = '0'
+ AuthSource.expects(:authenticate).returns([:login => 'foo', :firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com', :auth_source_id => 66])
+
+ post 'account/login', :username => 'foo', :password => 'bar'
+ assert_redirected_to 'my/page'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+
+ def test_onthefly_registration_with_invalid_attributes
+ # disable registration
+ Setting.self_registration = '0'
+ AuthSource.expects(:authenticate).returns([:login => 'foo', :lastname => 'Smith', :auth_source_id => 66])
+
+ post 'account/login', :username => 'foo', :password => 'bar'
+ assert_response :success
+ assert_template 'account/register'
+ assert_tag :input, :attributes => { :name => 'user[firstname]', :value => '' }
+ assert_tag :input, :attributes => { :name => 'user[lastname]', :value => 'Smith' }
+ assert_no_tag :input, :attributes => { :name => 'user[login]' }
+ assert_no_tag :input, :attributes => { :name => 'user[password]' }
+
+ post 'account/register', :user => {:firstname => 'Foo', :lastname => 'Smith', :mail => 'foo@bar.com'}
+ assert_redirected_to 'my/account'
+
+ user = User.find_by_login('foo')
+ assert user.is_a?(User)
+ assert_equal 66, user.auth_source_id
+ assert user.hashed_password.blank?
+ end
+
+ else
+ puts 'Mocha is missing. Skipping tests.'
+ end
end
diff --git a/groups/test/integration/admin_test.rb b/groups/test/integration/admin_test.rb
index a424247cc..6e385873e 100644
--- a/groups/test/integration/admin_test.rb
+++ b/groups/test/integration/admin_test.rb
@@ -48,8 +48,9 @@ class AdminTest < ActionController::IntegrationTest
post "projects/add", :project => { :name => "blog",
:description => "weblog",
:identifier => "blog",
- :is_public => 1 },
- 'custom_fields[3]' => 'Beta'
+ :is_public => 1,
+ :custom_field_values => { '3' => 'Beta' }
+ }
assert_redirected_to "admin/projects"
assert_equal 'Successful creation.', flash[:notice]
diff --git a/groups/test/integration/issues_test.rb b/groups/test/integration/issues_test.rb
index b9e21719c..2ef933fc2 100644
--- a/groups/test/integration/issues_test.rb
+++ b/groups/test/integration/issues_test.rb
@@ -1,10 +1,30 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
require "#{File.dirname(__FILE__)}/../test_helper"
class IssuesTest < ActionController::IntegrationTest
fixtures :projects,
:users,
+ :roles,
+ :members,
:trackers,
:projects_trackers,
+ :enabled_modules,
:issue_statuses,
:issues,
:enumerations,
@@ -47,6 +67,7 @@ class IssuesTest < ActionController::IntegrationTest
# add then remove 2 attachments to an issue
def test_issue_attachements
log_user('jsmith', 'jsmith')
+ set_tmp_attachments_directory
post 'issues/edit/1',
:notes => 'Some notes',
diff --git a/groups/test/test_helper.rb b/groups/test/test_helper.rb
index 61670318a..f61b88d8c 100644
--- a/groups/test/test_helper.rb
+++ b/groups/test/test_helper.rb
@@ -57,21 +57,11 @@ class Test::Unit::TestCase
def test_uploaded_file(name, mime)
ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + "/files/#{name}", mime)
end
-end
-
-
-# ActionController::TestUploadedFile bug
-# see http://dev.rubyonrails.org/ticket/4635
-class String
- def original_filename
- "testfile.txt"
- end
-
- def content_type
- "text/plain"
- end
- def read
- self.to_s
+ # Use a temporary directory for attachment related tests
+ def set_tmp_attachments_directory
+ Dir.mkdir "#{RAILS_ROOT}/tmp/test" unless File.directory?("#{RAILS_ROOT}/tmp/test")
+ Dir.mkdir "#{RAILS_ROOT}/tmp/test/attachments" unless File.directory?("#{RAILS_ROOT}/tmp/test/attachments")
+ Attachment.storage_path = "#{RAILS_ROOT}/tmp/test/attachments"
end
end
diff --git a/groups/test/unit/activity_test.rb b/groups/test/unit/activity_test.rb
new file mode 100644
index 000000000..ccda9f119
--- /dev/null
+++ b/groups/test/unit/activity_test.rb
@@ -0,0 +1,71 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ActivityTest < Test::Unit::TestCase
+ fixtures :projects, :versions, :users, :roles, :members, :issues, :journals, :journal_details,
+ :trackers, :projects_trackers, :issue_statuses, :enabled_modules, :enumerations, :boards, :messages
+
+ def setup
+ @project = Project.find(1)
+ end
+
+ def test_activity_without_subprojects
+ events = find_events(User.anonymous, :project => @project)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert !events.include?(Issue.find(4))
+ # subproject issue
+ assert !events.include?(Issue.find(5))
+ end
+
+ def test_activity_with_subprojects
+ events = find_events(User.anonymous, :project => @project, :with_subprojects => 1)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # subproject issue
+ assert events.include?(Issue.find(5))
+ end
+
+ def test_global_activity_anonymous
+ events = find_events(User.anonymous)
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ assert events.include?(Message.find(5))
+ # Issue of a private project
+ assert !events.include?(Issue.find(4))
+ end
+
+ def test_global_activity_logged_user
+ events = find_events(User.find(2)) # manager
+ assert_not_nil events
+
+ assert events.include?(Issue.find(1))
+ # Issue of a private project the user belongs to
+ assert events.include?(Issue.find(4))
+ end
+
+ private
+
+ def find_events(user, options={})
+ Redmine::Activity::Fetcher.new(user, options).events(Date.today - 30, Date.today + 1)
+ end
+end
diff --git a/groups/test/unit/attachment_test.rb b/groups/test/unit/attachment_test.rb
new file mode 100644
index 000000000..99f7c29f9
--- /dev/null
+++ b/groups/test/unit/attachment_test.rb
@@ -0,0 +1,32 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class AttachmentTest < Test::Unit::TestCase
+
+ def setup
+ end
+
+ def test_diskfilename
+ assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
+ assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
+ assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentué.txt")[13..-1]
+ assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentué")[13..-1]
+ assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentué.ça")[13..-1]
+ end
+end
diff --git a/groups/test/unit/changeset_test.rb b/groups/test/unit/changeset_test.rb
index bbfe6952d..6cc53d852 100644
--- a/groups/test/unit/changeset_test.rb
+++ b/groups/test/unit/changeset_test.rb
@@ -39,6 +39,17 @@ class ChangesetTest < Test::Unit::TestCase
assert fixed.closed?
assert_equal 90, fixed.done_ratio
end
+
+ def test_ref_keywords_any_line_start
+ Setting.commit_ref_keywords = '*'
+
+ c = Changeset.new(:repository => Project.find(1).repository,
+ :committed_on => Time.now,
+ :comments => '#1 is the reason of this commit')
+ c.scan_comment_for_issue_ids
+
+ assert_equal [1], c.issue_ids.sort
+ end
def test_previous
changeset = Changeset.find_by_revision('3')
diff --git a/groups/test/unit/default_data_test.rb b/groups/test/unit/default_data_test.rb
new file mode 100644
index 000000000..39616135e
--- /dev/null
+++ b/groups/test/unit/default_data_test.rb
@@ -0,0 +1,45 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class DefaultDataTest < Test::Unit::TestCase
+ fixtures :roles
+
+ def test_no_data
+ assert !Redmine::DefaultData::Loader::no_data?
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::no_data?
+ end
+
+ def test_load
+ GLoc.valid_languages.each do |lang|
+ begin
+ Role.delete_all("builtin = 0")
+ Tracker.delete_all
+ IssueStatus.delete_all
+ Enumeration.delete_all
+ assert Redmine::DefaultData::Loader::load(lang)
+ rescue ActiveRecord::RecordInvalid => e
+ assert false, ":#{lang} default data is invalid (#{e.message})."
+ end
+ end
+ end
+end
diff --git a/groups/test/unit/enumeration_test.rb b/groups/test/unit/enumeration_test.rb
new file mode 100644
index 000000000..9b7bfd174
--- /dev/null
+++ b/groups/test/unit/enumeration_test.rb
@@ -0,0 +1,45 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EnumerationTest < Test::Unit::TestCase
+ fixtures :enumerations, :issues
+
+ def setup
+ end
+
+ def test_objects_count
+ # low priority
+ assert_equal 5, Enumeration.find(4).objects_count
+ # urgent
+ assert_equal 0, Enumeration.find(7).objects_count
+ end
+
+ def test_in_use
+ # low priority
+ assert Enumeration.find(4).in_use?
+ # urgent
+ assert !Enumeration.find(7).in_use?
+ end
+
+ def test_destroy_with_reassign
+ Enumeration.find(4).destroy(Enumeration.find(6))
+ assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
+ assert_equal 5, Enumeration.find(6).objects_count
+ end
+end
diff --git a/groups/test/unit/filesystem_adapter_test.rb b/groups/test/unit/filesystem_adapter_test.rb
new file mode 100644
index 000000000..720d1e92c
--- /dev/null
+++ b/groups/test/unit/filesystem_adapter_test.rb
@@ -0,0 +1,42 @@
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+
+class FilesystemAdapterTest < Test::Unit::TestCase
+
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
+
+ if File.directory?(REPOSITORY_PATH)
+ def setup
+ @adapter = Redmine::Scm::Adapters::FilesystemAdapter.new(REPOSITORY_PATH)
+ end
+
+ def test_entries
+ assert_equal 2, @adapter.entries.size
+ assert_equal ["dir", "test"], @adapter.entries.collect(&:name)
+ assert_equal ["dir", "test"], @adapter.entries(nil).collect(&:name)
+ assert_equal ["dir", "test"], @adapter.entries("/").collect(&:name)
+ ["dir", "/dir", "/dir/", "dir/"].each do |path|
+ assert_equal ["subdir", "dirfile"], @adapter.entries(path).collect(&:name)
+ end
+ # If y try to use "..", the path is ignored
+ ["/../","dir/../", "..", "../", "/..", "dir/.."].each do |path|
+ assert_equal ["dir", "test"], @adapter.entries(path).collect(&:name), ".. must be ignored in path argument"
+ end
+ end
+
+ def test_cat
+ assert_equal "TEST CAT\n", @adapter.cat("test")
+ assert_equal "TEST CAT\n", @adapter.cat("/test")
+ # Revision number is ignored
+ assert_equal "TEST CAT\n", @adapter.cat("/test", 1)
+ end
+
+ else
+ puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
+ def test_fake; assert true end
+ end
+
+end
+
+
diff --git a/groups/test/unit/helpers/application_helper_test.rb b/groups/test/unit/helpers/application_helper_test.rb
index fa2109131..452c0b535 100644
--- a/groups/test/unit/helpers/application_helper_test.rb
+++ b/groups/test/unit/helpers/application_helper_test.rb
@@ -20,7 +20,11 @@ require File.dirname(__FILE__) + '/../../test_helper'
class ApplicationHelperTest < HelperTestCase
include ApplicationHelper
include ActionView::Helpers::TextHelper
- fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules
+ fixtures :projects, :roles, :enabled_modules,
+ :repositories, :changesets,
+ :trackers, :issue_statuses, :issues, :versions, :documents,
+ :wikis, :wiki_pages, :wiki_contents,
+ :boards, :messages
def setup
super
@@ -31,10 +35,14 @@ class ApplicationHelperTest < HelperTestCase
'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
+ 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
+ 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
- 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>'
+ 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
+ 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
+ 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
}
to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
end
@@ -58,7 +66,10 @@ class ApplicationHelperTest < HelperTestCase
to_test = {
'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
- '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>'
+ '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
+ "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
+ # no multiline link text
+ "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
}
to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
end
@@ -76,7 +87,10 @@ class ApplicationHelperTest < HelperTestCase
version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
:class => 'version')
- source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'}
+ message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
+
+ source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
+ source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
to_test = {
# tickets
@@ -92,10 +106,20 @@ class ApplicationHelperTest < HelperTestCase
'version:"1.0"' => version_link,
# source
'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
+ 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
+ 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
+ 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
+ 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
+ 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
+ # message
+ 'message#4' => link_to('Post 2', message_url, :class => 'message'),
+ 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
# escaping
'!#3.' => '#3.',
'!r1' => 'r1',
@@ -106,7 +130,9 @@ class ApplicationHelperTest < HelperTestCase
'!version:"1.0"' => 'version:"1.0"',
'!source:/some/file' => 'source:/some/file',
# invalid expressions
- 'source:' => 'source:'
+ 'source:' => 'source:',
+ # url hash
+ "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
}
@project = Project.find(1)
to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
@@ -116,6 +142,9 @@ class ApplicationHelperTest < HelperTestCase
to_test = {
'[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
'[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
+ # link with anchor
+ '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
+ '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
# page that doesn't exist
'[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
'[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
@@ -125,6 +154,8 @@ class ApplicationHelperTest < HelperTestCase
'[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
'[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
'[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
+ # striked through link
+ '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
# escaping
'![[Another page|Page]]' => '[[Another page|Page]]',
}
@@ -141,17 +172,22 @@ class ApplicationHelperTest < HelperTestCase
"<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
"<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
"<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
+ "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
+ "<!-- opening comment" => "<p>&lt;!-- opening comment</p>"
+ }
+ to_test.each { |text, result| assert_equal result, textilizable(text) }
+ end
+
+ def test_allowed_html_tags
+ to_test = {
+ "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
+ "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
}
to_test.each { |text, result| assert_equal result, textilizable(text) }
end
def test_wiki_links_in_tables
- to_test = {"|Cell 11|Cell 12|Cell 13|\n|Cell 21|Cell 22||\n|Cell 31||Cell 33|" =>
- '<tr><td>Cell 11</td><td>Cell 12</td><td>Cell 13</td></tr>' +
- '<tr><td>Cell 21</td><td>Cell 22</td></tr>' +
- '<tr><td>Cell 31</td><td>Cell 33</td></tr>',
-
- "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
+ to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
'<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
'<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
'</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
@@ -160,6 +196,108 @@ class ApplicationHelperTest < HelperTestCase
to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
end
+ def test_text_formatting
+ to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
+ '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
+ }
+ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
+ end
+
+ def test_wiki_horizontal_rule
+ assert_equal '<hr />', textilizable('---')
+ assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
+ end
+
+ def test_table_of_content
+ raw = <<-RAW
+{{toc}}
+
+h1. Title
+
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+
+h2. Subtitle
+
+Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+
+h2. Subtitle with %{color:red}red text%
+
+h1. Another title
+
+RAW
+
+ expected = '<ul class="toc">' +
+ '<li class="heading1"><a href="#Title">Title</a></li>' +
+ '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
+ '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
+ '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
+ '</ul>'
+
+ assert textilizable(raw).gsub("\n", "").include?(expected)
+ end
+
+ def test_blockquote
+ # orig raw text
+ raw = <<-RAW
+John said:
+> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+> Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+> * Donec odio lorem,
+> * sagittis ac,
+> * malesuada in,
+> * adipiscing eu, dolor.
+>
+> >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
+> Proin a tellus. Nam vel neque.
+
+He's right.
+RAW
+
+ # expected html
+ expected = <<-EXPECTED
+<p>John said:</p>
+<blockquote>
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
+Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
+<ul>
+ <li>Donec odio lorem,</li>
+ <li>sagittis ac,</li>
+ <li>malesuada in,</li>
+ <li>adipiscing eu, dolor.</li>
+</ul>
+<blockquote>
+<p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
+</blockquote>
+<p>Proin a tellus. Nam vel neque.</p>
+</blockquote>
+<p>He's right.</p>
+EXPECTED
+
+ assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
+ end
+
+ def test_table
+ raw = <<-RAW
+This is a table with empty cells:
+
+|cell11|cell12||
+|cell21||cell23|
+|cell31|cell32|cell33|
+RAW
+
+ expected = <<-EXPECTED
+<p>This is a table with empty cells:</p>
+
+<table>
+ <tr><td>cell11</td><td>cell12</td><td></td></tr>
+ <tr><td>cell21</td><td></td><td>cell23</td></tr>
+ <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
+</table>
+EXPECTED
+
+ assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
+ end
+
def test_macro_hello_world
text = "{{hello_world}}"
assert textilizable(text).match(/Hello world!/)
diff --git a/groups/test/unit/issue_test.rb b/groups/test/unit/issue_test.rb
index 36ba1fb45..12b4da336 100644
--- a/groups/test/unit/issue_test.rb
+++ b/groups/test/unit/issue_test.rb
@@ -18,7 +18,13 @@
require File.dirname(__FILE__) + '/../test_helper'
class IssueTest < Test::Unit::TestCase
- fixtures :projects, :users, :members, :trackers, :projects_trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :time_entries
+ fixtures :projects, :users, :members,
+ :trackers, :projects_trackers,
+ :issue_statuses, :issue_categories,
+ :enumerations,
+ :issues,
+ :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
+ :time_entries
def test_create
issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
@@ -27,6 +33,76 @@ class IssueTest < Test::Unit::TestCase
assert_equal 1.5, issue.estimated_hours
end
+ def test_create_with_required_custom_field
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:is_required, true)
+
+ issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
+ assert issue.available_custom_fields.include?(field)
+ # No value for the custom field
+ assert !issue.save
+ assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+ # Blank value
+ issue.custom_field_values = { field.id => '' }
+ assert !issue.save
+ assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+ # Invalid value
+ issue.custom_field_values = { field.id => 'SQLServer' }
+ assert !issue.save
+ assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
+ # Valid value
+ issue.custom_field_values = { field.id => 'PostgreSQL' }
+ assert issue.save
+ issue.reload
+ assert_equal 'PostgreSQL', issue.custom_value_for(field).value
+ end
+
+ def test_update_issue_with_required_custom_field
+ field = IssueCustomField.find_by_name('Database')
+ field.update_attribute(:is_required, true)
+
+ issue = Issue.find(1)
+ assert_nil issue.custom_value_for(field)
+ assert issue.available_custom_fields.include?(field)
+ # No change to custom values, issue can be saved
+ assert issue.save
+ # Blank value
+ issue.custom_field_values = { field.id => '' }
+ assert !issue.save
+ # Valid value
+ issue.custom_field_values = { field.id => 'PostgreSQL' }
+ assert issue.save
+ issue.reload
+ assert_equal 'PostgreSQL', issue.custom_value_for(field).value
+ end
+
+ def test_should_not_update_attributes_if_custom_fields_validation_fails
+ issue = Issue.find(1)
+ field = IssueCustomField.find_by_name('Database')
+ assert issue.available_custom_fields.include?(field)
+
+ issue.custom_field_values = { field.id => 'Invalid' }
+ issue.subject = 'Should be not be saved'
+ assert !issue.save
+
+ issue.reload
+ assert_equal "Can't print recipes", issue.subject
+ end
+
+ def test_should_not_recreate_custom_values_objects_on_update
+ field = IssueCustomField.find_by_name('Database')
+
+ issue = Issue.find(1)
+ issue.custom_field_values = { field.id => 'PostgreSQL' }
+ assert issue.save
+ custom_value = issue.custom_value_for(field)
+ issue.reload
+ issue.custom_field_values = { field.id => 'MySQL' }
+ assert issue.save
+ issue.reload
+ assert_equal custom_value.id, issue.custom_value_for(field).id
+ end
+
def test_category_based_assignment
issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
@@ -42,7 +118,7 @@ class IssueTest < Test::Unit::TestCase
assert_equal orig.custom_values.first.value, issue.custom_values.first.value
end
- def test_close_duplicates
+ def test_should_close_duplicates
# Create 3 issues
issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
assert issue1.save
@@ -52,12 +128,12 @@ class IssueTest < Test::Unit::TestCase
assert issue3.save
# 2 is a dupe of 1
- IssueRelation.create(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
+ IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
# And 3 is a dupe of 2
- IssueRelation.create(:issue_from => issue2, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
+ IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
# And 3 is a dupe of 1 (circular duplicates)
- IssueRelation.create(:issue_from => issue1, :issue_to => issue3, :relation_type => IssueRelation::TYPE_DUPLICATES)
-
+ IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
+
assert issue1.reload.duplicates.include?(issue2)
# Closing issue 1
@@ -69,17 +145,46 @@ class IssueTest < Test::Unit::TestCase
assert issue3.reload.closed?
end
- def test_move_to_another_project
+ def test_should_not_close_duplicated_issue
+ # Create 3 issues
+ issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => Enumeration.get_values('IPRI').first, :subject => 'Duplicates test', :description => 'Duplicates test')
+ assert issue1.save
+ issue2 = issue1.clone
+ assert issue2.save
+
+ # 2 is a dupe of 1
+ IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
+ # 2 is a dup of 1 but 1 is not a duplicate of 2
+ assert !issue2.reload.duplicates.include?(issue1)
+
+ # Closing issue 2
+ issue2.init_journal(User.find(:first), "Closing issue2")
+ issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
+ assert issue2.save
+ # 1 should not be also closed
+ assert !issue1.reload.closed?
+ end
+
+ def test_move_to_another_project_with_same_category
issue = Issue.find(1)
assert issue.move_to(Project.find(2))
issue.reload
assert_equal 2, issue.project_id
- # Category removed
- assert_nil issue.category
+ # Category changes
+ assert_equal 4, issue.category_id
# Make sure time entries were move to the target project
assert_equal 2, issue.time_entries.first.project_id
end
+ def test_move_to_another_project_without_same_category
+ issue = Issue.find(2)
+ assert issue.move_to(Project.find(2))
+ issue.reload
+ assert_equal 2, issue.project_id
+ # Category cleared
+ assert_nil issue.category_id
+ end
+
def test_issue_destroy
Issue.find(1).destroy
assert_nil Issue.find_by_id(1)
diff --git a/groups/test/unit/mail_handler_test.rb b/groups/test/unit/mail_handler_test.rb
index d0fc68de8..b3628e0d5 100644
--- a/groups/test/unit/mail_handler_test.rb
+++ b/groups/test/unit/mail_handler_test.rb
@@ -18,40 +18,111 @@
require File.dirname(__FILE__) + '/../test_helper'
class MailHandlerTest < Test::Unit::TestCase
- fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations
+ fixtures :users, :projects,
+ :enabled_modules,
+ :roles,
+ :members,
+ :issues,
+ :trackers,
+ :projects_trackers,
+ :enumerations,
+ :issue_categories
+
+ FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
- FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
- CHARSET = "utf-8"
-
- include ActionMailer::Quoting
-
def setup
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
+ ActionMailer::Base.deliveries.clear
+ end
+
+ def test_add_issue
+ # This email contains: 'Project: onlinestore'
+ issue = submit_email('ticket_on_given_project.eml')
+ assert issue.is_a?(Issue)
+ assert !issue.new_record?
+ issue.reload
+ assert_equal 'New ticket on a given project', issue.subject
+ assert_equal User.find_by_login('jsmith'), issue.author
+ assert_equal Project.find(2), issue.project
+ assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
+ end
- @expected = TMail::Mail.new
- @expected.set_content_type "text", "plain", { "charset" => CHARSET }
- @expected.mime_version = '1.0'
+ def test_add_issue_with_status
+ # This email contains: 'Project: onlinestore' and 'Status: Resolved'
+ issue = submit_email('ticket_on_given_project.eml')
+ assert issue.is_a?(Issue)
+ assert !issue.new_record?
+ issue.reload
+ assert_equal Project.find(2), issue.project
+ assert_equal IssueStatus.find_by_name("Resolved"), issue.status
+ end
+
+ def test_add_issue_with_attributes_override
+ issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
+ assert issue.is_a?(Issue)
+ assert !issue.new_record?
+ issue.reload
+ assert_equal 'New ticket on a given project', issue.subject
+ assert_equal User.find_by_login('jsmith'), issue.author
+ assert_equal Project.find(2), issue.project
+ assert_equal 'Feature request', issue.tracker.to_s
+ assert_equal 'Stock management', issue.category.to_s
+ assert_equal 'Urgent', issue.priority.to_s
+ assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end
- def test_add_note_to_issue
- raw = read_fixture("add_note_to_issue.txt").join
- MailHandler.receive(raw)
+ def test_add_issue_with_partial_attributes_override
+ issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
+ assert issue.is_a?(Issue)
+ assert !issue.new_record?
+ issue.reload
+ assert_equal 'New ticket on a given project', issue.subject
+ assert_equal User.find_by_login('jsmith'), issue.author
+ assert_equal Project.find(2), issue.project
+ assert_equal 'Feature request', issue.tracker.to_s
+ assert_nil issue.category
+ assert_equal 'High', issue.priority.to_s
+ assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
+ end
+
+ def test_add_issue_with_attachment_to_specific_project
+ issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
+ assert issue.is_a?(Issue)
+ assert !issue.new_record?
+ issue.reload
+ assert_equal 'Ticket created by email with attachment', issue.subject
+ assert_equal User.find_by_login('jsmith'), issue.author
+ assert_equal Project.find(2), issue.project
+ assert_equal 'This is a new ticket with attachments', issue.description
+ # Attachment properties
+ assert_equal 1, issue.attachments.size
+ assert_equal 'Paella.jpg', issue.attachments.first.filename
+ assert_equal 'image/jpeg', issue.attachments.first.content_type
+ assert_equal 10790, issue.attachments.first.filesize
+ end
+
+ def test_add_issue_note
+ journal = submit_email('ticket_reply.eml')
+ assert journal.is_a?(Journal)
+ assert_equal User.find_by_login('jsmith'), journal.user
+ assert_equal Issue.find(2), journal.journalized
+ assert_match /This is reply/, journal.notes
+ end
- issue = Issue.find(2)
- journal = issue.journals.find(:first, :order => "created_on DESC")
- assert journal
- assert_equal User.find_by_mail("jsmith@somenet.foo"), journal.user
- assert_equal "Note added by mail", journal.notes
+ def test_add_issue_note_with_status_change
+ # This email contains: 'Status: Resolved'
+ journal = submit_email('ticket_reply_with_status.eml')
+ assert journal.is_a?(Journal)
+ issue = Issue.find(journal.issue.id)
+ assert_equal User.find_by_login('jsmith'), journal.user
+ assert_equal Issue.find(2), journal.journalized
+ assert_match /This is reply/, journal.notes
+ assert_equal IssueStatus.find_by_name("Resolved"), issue.status
end
private
- def read_fixture(action)
- IO.readlines("#{FIXTURES_PATH}/mail_handler/#{action}")
- end
-
- def encode(subject)
- quoted_printable(subject, CHARSET)
- end
+
+ def submit_email(filename, options={})
+ raw = IO.read(File.join(FIXTURES_PATH, filename))
+ MailHandler.receive(raw, options)
+ end
end
diff --git a/groups/test/unit/mailer_test.rb b/groups/test/unit/mailer_test.rb
index 64648b94c..402624f5f 100644
--- a/groups/test/unit/mailer_test.rb
+++ b/groups/test/unit/mailer_test.rb
@@ -36,7 +36,7 @@ class MailerTest < Test::Unit::TestCase
# link to a referenced ticket
assert mail.body.include?('<a href="https://mydomain.foo/issues/show/2" class="issue" title="Add ingredients categories (Assigned)">#2</a>')
# link to a changeset
- assert mail.body.include?('<a href="https://mydomain.foo/repositories/revision/ecookbook?rev=2" class="changeset" title="This commit fixes #1, #2 and references #1 &amp; #3">r2</a>')
+ assert mail.body.include?('<a href="https://mydomain.foo/repositories/revision/ecookbook/2" class="changeset" title="This commit fixes #1, #2 and references #1 &amp; #3">r2</a>')
end
# test mailer methods for each language
@@ -116,4 +116,13 @@ class MailerTest < Test::Unit::TestCase
assert Mailer.deliver_register(token)
end
end
+
+ def test_reminders
+ ActionMailer::Base.deliveries.clear
+ Mailer.reminders(:days => 42)
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ mail = ActionMailer::Base.deliveries.last
+ assert mail.bcc.include?('dlopper@somenet.foo')
+ assert mail.body.include?('Bug #3: Error 281 when updating a recipe')
+ end
end
diff --git a/groups/test/unit/mercurial_adapter_test.rb b/groups/test/unit/mercurial_adapter_test.rb
new file mode 100644
index 000000000..a2673ad42
--- /dev/null
+++ b/groups/test/unit/mercurial_adapter_test.rb
@@ -0,0 +1,53 @@
+require File.dirname(__FILE__) + '/../test_helper'
+begin
+ require 'mocha'
+
+ class MercurialAdapterTest < Test::Unit::TestCase
+
+ TEMPLATES_DIR = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATES_DIR
+ TEMPLATE_NAME = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_NAME
+ TEMPLATE_EXTENSION = Redmine::Scm::Adapters::MercurialAdapter::TEMPLATE_EXTENSION
+
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/mercurial_repository'
+
+ def test_hgversion
+ to_test = { "0.9.5" => [0,9,5],
+ "1.0" => [1,0],
+ "1e4ddc9ac9f7+20080325" => nil,
+ "1.0.1+20080525" => [1,0,1],
+ "1916e629a29d" => nil}
+
+ to_test.each do |s, v|
+ test_hgversion_for(s, v)
+ end
+ end
+
+ def test_template_path
+ to_test = { [0,9,5] => "0.9.5",
+ [1,0] => "1.0",
+ [] => "1.0",
+ [1,0,1] => "1.0"}
+
+ to_test.each do |v, template|
+ test_template_path_for(v, template)
+ end
+ end
+
+ private
+
+ def test_hgversion_for(hgversion, version)
+ Redmine::Scm::Adapters::MercurialAdapter.expects(:hgversion_from_command_line).returns(hgversion)
+ adapter = Redmine::Scm::Adapters::MercurialAdapter
+ assert_equal version, adapter.hgversion
+ end
+
+ def test_template_path_for(version, template)
+ adapter = Redmine::Scm::Adapters::MercurialAdapter
+ assert_equal "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{template}.#{TEMPLATE_EXTENSION}", adapter.template_path_for(version)
+ assert File.exist?(adapter.template_path_for(version))
+ end
+ end
+
+rescue LoadError
+ def test_fake; assert(false, "Requires mocha to run those tests") end
+end
diff --git a/groups/test/unit/project_test.rb b/groups/test/unit/project_test.rb
index 60106bc07..0bd28dbc9 100644
--- a/groups/test/unit/project_test.rb
+++ b/groups/test/unit/project_test.rb
@@ -101,7 +101,7 @@ class ProjectTest < Test::Unit::TestCase
assert sub.save
assert_equal @ecookbook.id, sub.parent.id
@ecookbook.reload
- assert_equal 3, @ecookbook.children.size
+ assert_equal 4, @ecookbook.children.size
end
def test_subproject_invalid
@@ -118,6 +118,7 @@ class ProjectTest < Test::Unit::TestCase
def test_rolled_up_trackers
parent = Project.find(1)
+ parent.trackers = Tracker.find([1,2])
child = parent.children.find(3)
assert_equal [1, 2], parent.tracker_ids
diff --git a/groups/test/unit/query_test.rb b/groups/test/unit/query_test.rb
index d291018fb..c243dfbad 100644
--- a/groups/test/unit/query_test.rb
+++ b/groups/test/unit/query_test.rb
@@ -20,15 +20,109 @@ require File.dirname(__FILE__) + '/../test_helper'
class QueryTest < Test::Unit::TestCase
fixtures :projects, :users, :members, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries
+ def test_custom_fields_for_all_projects_should_be_available_in_global_queries
+ query = Query.new(:project => nil, :name => '_')
+ assert query.available_filters.has_key?('cf_1')
+ assert !query.available_filters.has_key?('cf_3')
+ end
+
+ def find_issues_with_query(query)
+ Issue.find :all,
+ :include => [ :assigned_to, :status, :tracker, :project, :priority ],
+ :conditions => query.statement
+ end
+
def test_query_with_multiple_custom_fields
query = Query.find(1)
assert query.valid?
assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
- issues = Issue.find :all,:include => [ :assigned_to, :status, :tracker, :project, :priority ], :conditions => query.statement
+ issues = find_issues_with_query(query)
assert_equal 1, issues.length
assert_equal Issue.find(3), issues.first
end
+ def test_operator_none
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('fixed_version_id', '!*', [''])
+ query.add_filter('cf_1', '!*', [''])
+ assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
+ assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_none_for_integer
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('estimated_hours', '!*', [''])
+ issues = find_issues_with_query(query)
+ assert !issues.empty?
+ assert issues.all? {|i| !i.estimated_hours}
+ end
+
+ def test_operator_all
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('fixed_version_id', '*', [''])
+ query.add_filter('cf_1', '*', [''])
+ assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
+ assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_greater_than
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('done_ratio', '>=', ['40'])
+ assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_in_more_than
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', '>t+', ['15'])
+ assert query.statement.include?("#{Issue.table_name}.due_date >=")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_in_less_than
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', '<t+', ['15'])
+ assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_today
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 't', [''])
+ assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_this_week_on_date
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('due_date', 'w', [''])
+ assert query.statement.include?("#{Issue.table_name}.due_date BETWEEN")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_this_week_on_datetime
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('created_on', 'w', [''])
+ assert query.statement.include?("#{Issue.table_name}.created_on BETWEEN")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_contains
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('subject', '~', ['string'])
+ assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
+ find_issues_with_query(query)
+ end
+
+ def test_operator_does_not_contains
+ query = Query.new(:project => Project.find(1), :name => '_')
+ query.add_filter('subject', '!~', ['string'])
+ assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
+ find_issues_with_query(query)
+ end
+
def test_default_columns
q = Query.new
assert !q.columns.empty?
@@ -42,6 +136,11 @@ class QueryTest < Test::Unit::TestCase
assert q.has_column?(c)
end
+ def test_label_for
+ q = Query.new
+ assert_equal 'assigned_to', q.label_for('assigned_to_id')
+ end
+
def test_editable_by
admin = User.find(1)
manager = User.find(2)
diff --git a/groups/test/unit/repository_cvs_test.rb b/groups/test/unit/repository_cvs_test.rb
index b14d9d964..6615f73bf 100644
--- a/groups/test/unit/repository_cvs_test.rb
+++ b/groups/test/unit/repository_cvs_test.rb
@@ -22,7 +22,7 @@ class RepositoryCvsTest < Test::Unit::TestCase
# No '..' in the repository path
REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/cvs_repository'
- REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
# CVS module
MODULE_NAME = 'test'
diff --git a/groups/test/unit/repository_darcs_test.rb b/groups/test/unit/repository_darcs_test.rb
index 1c8c1b8dd..ca8c267f2 100644
--- a/groups/test/unit/repository_darcs_test.rb
+++ b/groups/test/unit/repository_darcs_test.rb
@@ -48,6 +48,13 @@ class RepositoryDarcsTest < Test::Unit::TestCase
@repository.fetch_changesets
assert_equal 6, @repository.changesets.count
end
+
+ def test_cat
+ @repository.fetch_changesets
+ cat = @repository.cat("sources/welcome_controller.rb", 2)
+ assert_not_nil cat
+ assert cat.include?('class WelcomeController < ApplicationController')
+ end
else
puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
def test_fake; assert true end
diff --git a/groups/test/unit/repository_filesystem_test.rb b/groups/test/unit/repository_filesystem_test.rb
new file mode 100644
index 000000000..6b643f96f
--- /dev/null
+++ b/groups/test/unit/repository_filesystem_test.rb
@@ -0,0 +1,54 @@
+# redMine - project management software
+# Copyright (C) 2006-2007 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryFilesystemTest < Test::Unit::TestCase
+ fixtures :projects
+
+ # No '..' in the repository path
+ REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/filesystem_repository'
+
+ def setup
+ @project = Project.find(1)
+ Setting.enabled_scm << 'Filesystem' unless Setting.enabled_scm.include?('Filesystem')
+ assert @repository = Repository::Filesystem.create(:project => @project, :url => REPOSITORY_PATH)
+ end
+
+ if File.directory?(REPOSITORY_PATH)
+ def test_fetch_changesets
+ @repository.fetch_changesets
+ @repository.reload
+
+ assert_equal 0, @repository.changesets.count
+ assert_equal 0, @repository.changes.count
+ end
+
+ def test_entries
+ assert_equal 2, @repository.entries("", 2).size
+ assert_equal 2, @repository.entries("dir", 3).size
+ end
+
+ def test_cat
+ assert_equal "TEST CAT\n", @repository.scm.cat("test")
+ end
+
+ else
+ puts "Filesystem test repository NOT FOUND. Skipping unit tests !!! See doc/RUNNING_TESTS."
+ def test_fake; assert true end
+ end
+end
diff --git a/groups/test/unit/repository_git_test.rb b/groups/test/unit/repository_git_test.rb
index c7bd84a6e..8a6f1ddd0 100644
--- a/groups/test/unit/repository_git_test.rb
+++ b/groups/test/unit/repository_git_test.rb
@@ -22,7 +22,7 @@ class RepositoryGitTest < Test::Unit::TestCase
# No '..' in the repository path
REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
- REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+ REPOSITORY_PATH.gsub!(/\//, "\\") if Redmine::Platform.mswin?
def setup
@project = Project.find(1)
diff --git a/groups/test/unit/repository_mercurial_test.rb b/groups/test/unit/repository_mercurial_test.rb
index 21ddf1e3a..0f993ac16 100644
--- a/groups/test/unit/repository_mercurial_test.rb
+++ b/groups/test/unit/repository_mercurial_test.rb
@@ -48,6 +48,26 @@ class RepositoryMercurialTest < Test::Unit::TestCase
@repository.fetch_changesets
assert_equal 6, @repository.changesets.count
end
+
+ def test_entries
+ assert_equal 2, @repository.entries("sources", 2).size
+ assert_equal 1, @repository.entries("sources", 3).size
+ end
+
+ def test_locate_on_outdated_repository
+ # Change the working dir state
+ %x{hg -R #{REPOSITORY_PATH} up -r 0}
+ assert_equal 1, @repository.entries("images", 0).size
+ assert_equal 2, @repository.entries("images").size
+ assert_equal 2, @repository.entries("images", 2).size
+ end
+
+
+ def test_cat
+ assert @repository.scm.cat("sources/welcome_controller.rb", 2)
+ assert_nil @repository.scm.cat("sources/welcome_controller.rb")
+ end
+
else
puts "Mercurial test repository NOT FOUND. Skipping unit tests !!!"
def test_fake; assert true end
diff --git a/groups/test/unit/repository_test.rb b/groups/test/unit/repository_test.rb
index 7764ee04a..9ea9fdd45 100644
--- a/groups/test/unit/repository_test.rb
+++ b/groups/test/unit/repository_test.rb
@@ -45,6 +45,26 @@ class RepositoryTest < Test::Unit::TestCase
assert_equal repository, project.repository
end
+ def test_destroy
+ changesets = Changeset.count(:all, :conditions => "repository_id = 10")
+ changes = Change.count(:all, :conditions => "repository_id = 10", :include => :changeset)
+ assert_difference 'Changeset.count', -changesets do
+ assert_difference 'Change.count', -changes do
+ Repository.find(10).destroy
+ end
+ end
+ end
+
+ def test_should_not_create_with_disabled_scm
+ # disable Subversion
+ Setting.enabled_scm = ['Darcs', 'Git']
+ repository = Repository::Subversion.new(:project => Project.find(3), :url => "svn://localhost")
+ assert !repository.save
+ assert_equal :activerecord_error_invalid, repository.errors.on(:type)
+ # re-enable Subversion for following tests
+ Setting.delete_all
+ end
+
def test_scan_changesets_for_issue_ids
# choosing a status to apply to fix issues
Setting.commit_fix_status_id = IssueStatus.find(:first, :conditions => ["is_closed = ?", true]).id
diff --git a/groups/test/unit/role_test.rb b/groups/test/unit/role_test.rb
index 5e0d16753..b98af2e36 100644
--- a/groups/test/unit/role_test.rb
+++ b/groups/test/unit/role_test.rb
@@ -26,7 +26,7 @@ class RoleTest < Test::Unit::TestCase
target = Role.new(:name => 'Target')
assert target.save
- assert target.workflows.copy(source)
+ target.workflows.copy(source)
target.reload
assert_equal 90, target.workflows.size
end
diff --git a/groups/test/unit/search_test.rb b/groups/test/unit/search_test.rb
new file mode 100644
index 000000000..1b32df733
--- /dev/null
+++ b/groups/test/unit/search_test.rb
@@ -0,0 +1,143 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SearchTest < Test::Unit::TestCase
+ fixtures :users,
+ :members,
+ :projects,
+ :roles,
+ :enabled_modules,
+ :issues,
+ :trackers,
+ :journals,
+ :journal_details,
+ :repositories,
+ :changesets
+
+ def setup
+ @project = Project.find(1)
+ @issue_keyword = '%unable to print recipes%'
+ @issue = Issue.find(1)
+ @changeset_keyword = '%very first commit%'
+ @changeset = Changeset.find(100)
+ end
+
+ def test_search_by_anonymous
+ User.current = nil
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Removes the :view_changesets permission from Anonymous role
+ remove_permission Role.anonymous, :view_changesets
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert !r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_by_user
+ User.current = User.find_by_login('rhill')
+ assert User.current.memberships.empty?
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Removes the :view_changesets permission from Non member role
+ remove_permission Role.non_member, :view_changesets
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert !r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_by_allowed_member
+ User.current = User.find_by_login('jsmith')
+ assert User.current.projects.include?(@project)
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert r.include?(@changeset)
+ end
+
+ def test_search_by_unallowed_member
+ # Removes the :view_changesets permission from user's and non member role
+ remove_permission Role.find(1), :view_changesets
+ remove_permission Role.non_member, :view_changesets
+
+ User.current = User.find_by_login('jsmith')
+ assert User.current.projects.include?(@project)
+
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+
+ # Make the project private
+ @project.update_attribute :is_public, false
+ r = Issue.search(@issue_keyword).first
+ assert r.include?(@issue)
+ r = Changeset.search(@changeset_keyword).first
+ assert !r.include?(@changeset)
+ end
+
+ def test_search_issue_with_multiple_hits_in_journals
+ i = Issue.find(1)
+ assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'")
+
+ r = Issue.search('%notes%').first
+ assert_equal 1, r.size
+ assert_equal i, r.first
+ end
+
+ private
+
+ def remove_permission(role, permission)
+ role.permissions = role.permissions - [ permission ]
+ role.save
+ end
+end
diff --git a/groups/test/unit/subversion_adapter_test.rb b/groups/test/unit/subversion_adapter_test.rb
new file mode 100644
index 000000000..9f208839a
--- /dev/null
+++ b/groups/test/unit/subversion_adapter_test.rb
@@ -0,0 +1,33 @@
+# redMine - project management software
+# Copyright (C) 2006-2008 Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+require 'mkmf'
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class SubversionAdapterTest < Test::Unit::TestCase
+
+ if find_executable0('svn')
+ def test_client_version
+ v = Redmine::Scm::Adapters::SubversionAdapter.client_version
+ assert v.is_a?(Array)
+ end
+ else
+ puts "Subversion binary NOT FOUND. Skipping unit tests !!!"
+ def test_fake; assert true end
+ end
+end
diff --git a/groups/test/unit/tracker_test.rb b/groups/test/unit/tracker_test.rb
index 406bdd6db..6dab8890c 100644
--- a/groups/test/unit/tracker_test.rb
+++ b/groups/test/unit/tracker_test.rb
@@ -26,7 +26,7 @@ class TrackerTest < Test::Unit::TestCase
target = Tracker.new(:name => 'Target')
assert target.save
- assert target.workflows.copy(source)
+ target.workflows.copy(source)
target.reload
assert_equal 89, target.workflows.size
end
diff --git a/groups/test/unit/user_test.rb b/groups/test/unit/user_test.rb
index 3209f261a..925549544 100644
--- a/groups/test/unit/user_test.rb
+++ b/groups/test/unit/user_test.rb
@@ -57,6 +57,12 @@ class UserTest < Test::Unit::TestCase
assert_equal "john", @admin.login
end
+ def test_destroy
+ User.find(2).destroy
+ assert_nil User.find_by_id(2)
+ assert Member.find_all_by_principal_type_and_principal_id('User', 2).empty?
+ end
+
def test_validate
@admin.login = ""
assert !@admin.save
diff --git a/groups/test/unit/wiki_page_test.rb b/groups/test/unit/wiki_page_test.rb
index bb8111176..e5ebeeea6 100644
--- a/groups/test/unit/wiki_page_test.rb
+++ b/groups/test/unit/wiki_page_test.rb
@@ -48,6 +48,50 @@ class WikiPageTest < Test::Unit::TestCase
assert page.new_record?
end
+ def test_parent_title
+ page = WikiPage.find_by_title('Another_page')
+ assert_nil page.parent_title
+
+ page = WikiPage.find_by_title('Page_with_an_inline_image')
+ assert_equal 'CookBook documentation', page.parent_title
+ end
+
+ def test_assign_parent
+ page = WikiPage.find_by_title('Another_page')
+ page.parent_title = 'CookBook documentation'
+ assert page.save
+ page.reload
+ assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
+ end
+
+ def test_unassign_parent
+ page = WikiPage.find_by_title('Page_with_an_inline_image')
+ page.parent_title = ''
+ assert page.save
+ page.reload
+ assert_nil page.parent
+ end
+
+ def test_parent_validation
+ page = WikiPage.find_by_title('CookBook_documentation')
+
+ # A page that doesn't exist
+ page.parent_title = 'Unknown title'
+ assert !page.save
+ assert_equal :activerecord_error_invalid, page.errors.on(:parent_title)
+ # A child page
+ page.parent_title = 'Page_with_an_inline_image'
+ assert !page.save
+ assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title)
+ # The page itself
+ page.parent_title = 'CookBook_documentation'
+ assert !page.save
+ assert_equal :activerecord_error_circular_dependency, page.errors.on(:parent_title)
+
+ page.parent_title = 'Another_page'
+ assert page.save
+ end
+
def test_destroy
page = WikiPage.find(1)
page.destroy
diff --git a/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb
index d7f437a5e..0b7ad21f5 100644
--- a/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb
+++ b/groups/vendor/plugins/acts_as_event/lib/acts_as_event.rb
@@ -25,14 +25,15 @@ module Redmine
module ClassMethods
def acts_as_event(options = {})
return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
- options[:datetime] ||= :created_on
- options[:title] ||= :title
- options[:description] ||= :description
- options[:author] ||= :author
- options[:url] ||= {:controller => 'welcome'}
- options[:type] ||= self.name.underscore.dasherize
+ default_options = { :datetime => :created_on,
+ :title => :title,
+ :description => :description,
+ :author => :author,
+ :url => {:controller => 'welcome'},
+ :type => self.name.underscore.dasherize }
+
cattr_accessor :event_options
- self.event_options = options
+ self.event_options = default_options.merge(options)
send :include, Redmine::Acts::Event::InstanceMethods
end
end
diff --git a/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb
index dff76b913..9a81f363f 100644
--- a/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb
+++ b/groups/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb
@@ -23,6 +23,12 @@ module Redmine
end
module ClassMethods
+ # Options:
+ # * :columns - a column or an array of columns to search
+ # * :project_key - project foreign key (default to project_id)
+ # * :date_column - name of the datetime column (default to created_on)
+ # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
+ # * :permission - permission required to search the model (default to :view_"objects")
def acts_as_searchable(options = {})
return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
@@ -35,19 +41,12 @@ module Redmine
searchable_options[:columns] = [] << searchable_options[:columns]
end
- if searchable_options[:project_key]
- elsif column_names.include?('project_id')
- searchable_options[:project_key] = "#{table_name}.project_id"
- else
- raise 'No project key defined.'
- end
+ searchable_options[:project_key] ||= "#{table_name}.project_id"
+ searchable_options[:date_column] ||= "#{table_name}.created_on"
+ searchable_options[:order_column] ||= searchable_options[:date_column]
- if searchable_options[:date_column]
- elsif column_names.include?('created_on')
- searchable_options[:date_column] = "#{table_name}.created_on"
- else
- raise 'No date column defined defined.'
- end
+ # Permission needed to search this model
+ searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
# Should we search custom fields on this model ?
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
@@ -62,13 +61,24 @@ module Redmine
end
module ClassMethods
- def search(tokens, project, options={})
+ # Searches the model for the given tokens
+ # projects argument can be either nil (will search all projects), a project or an array of projects
+ # Returns the results and the results count
+ def search(tokens, projects=nil, options={})
tokens = [] << tokens unless tokens.is_a?(Array)
+ projects = [] << projects unless projects.nil? || projects.is_a?(Array)
+
find_options = {:include => searchable_options[:include]}
- find_options[:limit] = options[:limit] if options[:limit]
- find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
+ find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
+
+ limit_options = {}
+ limit_options[:limit] = options[:limit] if options[:limit]
+ if options[:offset]
+ limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
+ end
+
columns = searchable_options[:columns]
- columns.slice!(1..-1) if options[:titles_only]
+ columns = columns[0..0] if options[:titles_only]
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
@@ -87,21 +97,23 @@ module Redmine
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
- if options[:offset]
- sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
- end
find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
- results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do
- find(:all, find_options)
- end
- if searchable_options[:with] && !options[:titles_only]
- searchable_options[:with].each do |model, assoc|
- results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc}
+ project_conditions = []
+ project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
+ Project.allowed_to_condition(User.current, searchable_options[:permission]))
+ project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
+
+ results = []
+ results_count = 0
+
+ with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
+ with_scope(:find => find_options) do
+ results_count = count(:all)
+ results = find(:all, limit_options)
end
- results.uniq!
end
- results
+ [results, results_count]
end
end
end
diff --git a/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb
index 1f00e90a9..6a6827ee6 100644
--- a/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb
+++ b/groups/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb
@@ -70,6 +70,13 @@ module ActiveRecord
nodes
end
+ # Returns list of descendants.
+ #
+ # root.descendants # => [child1, subchild1, subchild2]
+ def descendants
+ children + children.collect(&:children).flatten
+ end
+
# Returns the root node of the tree.
def root
node = self
diff --git a/groups/vendor/plugins/acts_as_versioned/Rakefile b/groups/vendor/plugins/acts_as_versioned/Rakefile
index 3ae69e961..5bccb5d8d 100644
--- a/groups/vendor/plugins/acts_as_versioned/Rakefile
+++ b/groups/vendor/plugins/acts_as_versioned/Rakefile
@@ -1,182 +1,182 @@
-require 'rubygems'
-
-Gem::manage_gems
-
-require 'rake/rdoctask'
-require 'rake/packagetask'
-require 'rake/gempackagetask'
-require 'rake/testtask'
-require 'rake/contrib/rubyforgepublisher'
-
-PKG_NAME = 'acts_as_versioned'
-PKG_VERSION = '0.3.1'
-PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
-PROD_HOST = "technoweenie@bidwell.textdrive.com"
-RUBY_FORGE_PROJECT = 'ar-versioned'
-RUBY_FORGE_USER = 'technoweenie'
-
-desc 'Default: run unit tests.'
-task :default => :test
-
-desc 'Test the calculations plugin.'
-Rake::TestTask.new(:test) do |t|
- t.libs << 'lib'
- t.pattern = 'test/**/*_test.rb'
- t.verbose = true
-end
-
-desc 'Generate documentation for the calculations plugin.'
-Rake::RDocTask.new(:rdoc) do |rdoc|
- rdoc.rdoc_dir = 'rdoc'
- rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
- rdoc.options << '--line-numbers --inline-source'
- rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
- rdoc.rdoc_files.include('lib/**/*.rb')
-end
-
-spec = Gem::Specification.new do |s|
- s.name = PKG_NAME
- s.version = PKG_VERSION
- s.platform = Gem::Platform::RUBY
- s.summary = "Simple versioning with active record models"
- s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
- s.files.delete "acts_as_versioned_plugin.sqlite.db"
- s.files.delete "acts_as_versioned_plugin.sqlite3.db"
- s.files.delete "test/debug.log"
- s.require_path = 'lib'
- s.autorequire = 'acts_as_versioned'
- s.has_rdoc = true
- s.test_files = Dir['test/**/*_test.rb']
- s.add_dependency 'activerecord', '>= 1.10.1'
- s.add_dependency 'activesupport', '>= 1.1.1'
- s.author = "Rick Olson"
- s.email = "technoweenie@gmail.com"
- s.homepage = "http://techno-weenie.net"
-end
-
-Rake::GemPackageTask.new(spec) do |pkg|
- pkg.need_tar = true
-end
-
-desc "Publish the API documentation"
-task :pdoc => [:rdoc] do
- Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
-end
-
-desc 'Publish the gem and API docs'
-task :publish => [:pdoc, :rubyforge_upload]
-
-desc "Publish the release files to RubyForge."
-task :rubyforge_upload => :package do
- files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
-
- if RUBY_FORGE_PROJECT then
- require 'net/http'
- require 'open-uri'
-
- project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
- project_data = open(project_uri) { |data| data.read }
- group_id = project_data[/[?&]group_id=(\d+)/, 1]
- raise "Couldn't get group id" unless group_id
-
- # This echos password to shell which is a bit sucky
- if ENV["RUBY_FORGE_PASSWORD"]
- password = ENV["RUBY_FORGE_PASSWORD"]
- else
- print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
- password = STDIN.gets.chomp
- end
-
- login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
- data = [
- "login=1",
- "form_loginname=#{RUBY_FORGE_USER}",
- "form_pw=#{password}"
- ].join("&")
- http.post("/account/login.php", data)
- end
-
- cookie = login_response["set-cookie"]
- raise "Login failed" unless cookie
- headers = { "Cookie" => cookie }
-
- release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
- release_data = open(release_uri, headers) { |data| data.read }
- package_id = release_data[/[?&]package_id=(\d+)/, 1]
- raise "Couldn't get package id" unless package_id
-
- first_file = true
- release_id = ""
-
- files.each do |filename|
- basename = File.basename(filename)
- file_ext = File.extname(filename)
- file_data = File.open(filename, "rb") { |file| file.read }
-
- puts "Releasing #{basename}..."
-
- release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
- release_date = Time.now.strftime("%Y-%m-%d %H:%M")
- type_map = {
- ".zip" => "3000",
- ".tgz" => "3110",
- ".gz" => "3110",
- ".gem" => "1400"
- }; type_map.default = "9999"
- type = type_map[file_ext]
- boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
-
- query_hash = if first_file then
- {
- "group_id" => group_id,
- "package_id" => package_id,
- "release_name" => PKG_FILE_NAME,
- "release_date" => release_date,
- "type_id" => type,
- "processor_id" => "8000", # Any
- "release_notes" => "",
- "release_changes" => "",
- "preformatted" => "1",
- "submit" => "1"
- }
- else
- {
- "group_id" => group_id,
- "release_id" => release_id,
- "package_id" => package_id,
- "step2" => "1",
- "type_id" => type,
- "processor_id" => "8000", # Any
- "submit" => "Add This File"
- }
- end
-
- query = "?" + query_hash.map do |(name, value)|
- [name, URI.encode(value)].join("=")
- end.join("&")
-
- data = [
- "--" + boundary,
- "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
- "Content-Type: application/octet-stream",
- "Content-Transfer-Encoding: binary",
- "", file_data, ""
- ].join("\x0D\x0A")
-
- release_headers = headers.merge(
- "Content-Type" => "multipart/form-data; boundary=#{boundary}"
- )
-
- target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
- http.post(target + query, data, release_headers)
- end
-
- if first_file then
- release_id = release_response.body[/release_id=(\d+)/, 1]
- raise("Couldn't get release id") unless release_id
- end
-
- first_file = false
- end
- end
+require 'rubygems'
+
+Gem::manage_gems
+
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/testtask'
+require 'rake/contrib/rubyforgepublisher'
+
+PKG_NAME = 'acts_as_versioned'
+PKG_VERSION = '0.3.1'
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+PROD_HOST = "technoweenie@bidwell.textdrive.com"
+RUBY_FORGE_PROJECT = 'ar-versioned'
+RUBY_FORGE_USER = 'technoweenie'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the calculations plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the calculations plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
+ rdoc.options << '--line-numbers --inline-source'
+ rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = "Simple versioning with active record models"
+ s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
+ s.files.delete "acts_as_versioned_plugin.sqlite.db"
+ s.files.delete "acts_as_versioned_plugin.sqlite3.db"
+ s.files.delete "test/debug.log"
+ s.require_path = 'lib'
+ s.autorequire = 'acts_as_versioned'
+ s.has_rdoc = true
+ s.test_files = Dir['test/**/*_test.rb']
+ s.add_dependency 'activerecord', '>= 1.10.1'
+ s.add_dependency 'activesupport', '>= 1.1.1'
+ s.author = "Rick Olson"
+ s.email = "technoweenie@gmail.com"
+ s.homepage = "http://techno-weenie.net"
+end
+
+Rake::GemPackageTask.new(spec) do |pkg|
+ pkg.need_tar = true
+end
+
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
+end
+
+desc 'Publish the gem and API docs'
+task :publish => [:pdoc, :rubyforge_upload]
+
+desc "Publish the release files to RubyForge."
+task :rubyforge_upload => :package do
+ files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
+
+ if RUBY_FORGE_PROJECT then
+ require 'net/http'
+ require 'open-uri'
+
+ project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
+ project_data = open(project_uri) { |data| data.read }
+ group_id = project_data[/[?&]group_id=(\d+)/, 1]
+ raise "Couldn't get group id" unless group_id
+
+ # This echos password to shell which is a bit sucky
+ if ENV["RUBY_FORGE_PASSWORD"]
+ password = ENV["RUBY_FORGE_PASSWORD"]
+ else
+ print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
+ password = STDIN.gets.chomp
+ end
+
+ login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ data = [
+ "login=1",
+ "form_loginname=#{RUBY_FORGE_USER}",
+ "form_pw=#{password}"
+ ].join("&")
+ http.post("/account/login.php", data)
+ end
+
+ cookie = login_response["set-cookie"]
+ raise "Login failed" unless cookie
+ headers = { "Cookie" => cookie }
+
+ release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
+ release_data = open(release_uri, headers) { |data| data.read }
+ package_id = release_data[/[?&]package_id=(\d+)/, 1]
+ raise "Couldn't get package id" unless package_id
+
+ first_file = true
+ release_id = ""
+
+ files.each do |filename|
+ basename = File.basename(filename)
+ file_ext = File.extname(filename)
+ file_data = File.open(filename, "rb") { |file| file.read }
+
+ puts "Releasing #{basename}..."
+
+ release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
+ release_date = Time.now.strftime("%Y-%m-%d %H:%M")
+ type_map = {
+ ".zip" => "3000",
+ ".tgz" => "3110",
+ ".gz" => "3110",
+ ".gem" => "1400"
+ }; type_map.default = "9999"
+ type = type_map[file_ext]
+ boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
+
+ query_hash = if first_file then
+ {
+ "group_id" => group_id,
+ "package_id" => package_id,
+ "release_name" => PKG_FILE_NAME,
+ "release_date" => release_date,
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "release_notes" => "",
+ "release_changes" => "",
+ "preformatted" => "1",
+ "submit" => "1"
+ }
+ else
+ {
+ "group_id" => group_id,
+ "release_id" => release_id,
+ "package_id" => package_id,
+ "step2" => "1",
+ "type_id" => type,
+ "processor_id" => "8000", # Any
+ "submit" => "Add This File"
+ }
+ end
+
+ query = "?" + query_hash.map do |(name, value)|
+ [name, URI.encode(value)].join("=")
+ end.join("&")
+
+ data = [
+ "--" + boundary,
+ "Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
+ "Content-Type: application/octet-stream",
+ "Content-Transfer-Encoding: binary",
+ "", file_data, ""
+ ].join("\x0D\x0A")
+
+ release_headers = headers.merge(
+ "Content-Type" => "multipart/form-data; boundary=#{boundary}"
+ )
+
+ target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
+ http.post(target + query, data, release_headers)
+ end
+
+ if first_file then
+ release_id = release_response.body[/release_id=(\d+)/, 1]
+ raise("Couldn't get release id") unless release_id
+ end
+
+ first_file = false
+ end
+ end
end \ No newline at end of file
diff --git a/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb
index 5e6f6e636..bba10c437 100644
--- a/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb
+++ b/groups/vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb
@@ -22,7 +22,7 @@
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
- # versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version
+ # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
# column is present as well.
#
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
@@ -49,9 +49,24 @@ module ActiveRecord #:nodoc:
# page.revert_to(page.versions.last) # using versioned instance
# page.title # => 'hello world'
#
+ # page.versions.earliest # efficient query to find the first version
+ # page.versions.latest # efficient query to find the most recently created version
+ #
+ #
+ # Simple Queries to page between versions
+ #
+ # page.versions.before(version)
+ # page.versions.after(version)
+ #
+ # Access the previous/next versions from the versioned model itself
+ #
+ # version = page.versions.latest
+ # version.previous # go back one version
+ # version.next # go forward one version
+ #
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
module Versioned
- CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_changed_attributes]
+ CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
def self.included(base) # :nodoc:
base.extend ClassMethods
end
@@ -80,7 +95,7 @@ module ActiveRecord #:nodoc:
# end
#
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
- # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
+ # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
#
# def name=(new_name)
@@ -133,7 +148,7 @@ module ActiveRecord #:nodoc:
# # that create_table does
# Post.create_versioned_table
# end
- #
+ #
# def self.down
# Post.drop_versioned_table
# end
@@ -157,11 +172,11 @@ module ActiveRecord #:nodoc:
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
send :include, ActiveRecord::Acts::Versioned::ActMethods
-
+
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
- :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
+ :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
:version_association_options
-
+
# legacy
alias_method :non_versioned_fields, :non_versioned_columns
alias_method :non_versioned_fields=, :non_versioned_columns=
@@ -171,7 +186,7 @@ module ActiveRecord #:nodoc:
alias_method :non_versioned_fields=, :non_versioned_columns=
end
- send :attr_accessor, :changed_attributes
+ send :attr_accessor, :altered_attributes
self.versioned_class_name = options[:class_name] || "Version"
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
@@ -184,8 +199,7 @@ module ActiveRecord #:nodoc:
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
self.version_association_options = {
:class_name => "#{self.to_s}::#{versioned_class_name}",
- :foreign_key => "#{versioned_foreign_key}",
- :order => 'version',
+ :foreign_key => versioned_foreign_key,
:dependent => :delete_all
}.merge(options[:association_options] || {})
@@ -194,20 +208,30 @@ module ActiveRecord #:nodoc:
silence_warnings do
self.const_set(extension_module_name, Module.new(&extension))
end
-
+
options[:extend] = self.const_get(extension_module_name)
end
class_eval do
- has_many :versions, version_association_options
+ has_many :versions, version_association_options do
+ # finds earliest version of this record
+ def earliest
+ @earliest ||= find(:first, :order => 'version')
+ end
+
+ # find latest version of this record
+ def latest
+ @latest ||= find(:first, :order => 'version desc')
+ end
+ end
before_save :set_new_version
after_create :save_version_on_create
after_update :save_version
after_save :clear_old_versions
- after_save :clear_changed_attributes
-
+ after_save :clear_altered_attributes
+
unless options[:if_changed].nil?
- self.track_changed_attributes = true
+ self.track_altered_attributes = true
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
options[:if_changed].each do |attr_name|
define_method("#{attr_name}=") do |value|
@@ -215,15 +239,40 @@ module ActiveRecord #:nodoc:
end
end
end
-
+
include options[:extend] if options[:extend].is_a?(Module)
end
# create the dynamic versioned model
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
def self.reloadable? ; false ; end
+ # find first version before the given version
+ def self.before(version)
+ find :first, :order => 'version desc',
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
+ end
+
+ # find first version after the given version.
+ def self.after(version)
+ find :first, :order => 'version',
+ :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
+ end
+
+ def previous
+ self.class.before(self)
+ end
+
+ def next
+ self.class.after(self)
+ end
+
+ def versions_count
+ page.version
+ end
end
-
+
+ versioned_class.cattr_accessor :original_class
+ versioned_class.original_class = self
versioned_class.set_table_name versioned_table_name
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
:class_name => "::#{self.to_s}",
@@ -232,17 +281,22 @@ module ActiveRecord #:nodoc:
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end
end
-
+
module ActMethods
def self.included(base) # :nodoc:
base.extend ClassMethods
end
-
+
+ # Finds a specific version of this record
+ def find_version(version = nil)
+ self.class.find_version(id, version)
+ end
+
# Saves a version of the model if applicable
def save_version
save_version_on_create if save_version?
end
-
+
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
def save_version_on_create
rev = self.class.versioned_class.new
@@ -263,16 +317,8 @@ module ActiveRecord #:nodoc:
end
end
- # Finds a specific version of this model.
- def find_version(version)
- return version if version.is_a?(self.class.versioned_class)
- return nil if version.is_a?(ActiveRecord::Base)
- find_versions(:conditions => ['version = ?', version], :limit => 1).first
- end
-
- # Finds versions of this model. Takes an options hash like <tt>find</tt>
- def find_versions(options = {})
- versions.find(:all, options)
+ def versions_count
+ version
end
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
@@ -280,14 +326,14 @@ module ActiveRecord #:nodoc:
if version.is_a?(self.class.versioned_class)
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
else
- return false unless version = find_version(version)
+ return false unless version = versions.find_by_version(version)
end
self.clone_versioned_model(version, self)
self.send("#{self.class.version_column}=", version.version)
true
end
- # Reverts a model to a given version and saves the model.
+ # Reverts a model to a given version and saves the model.
# Takes either a version number or an instance of the versioned model
def revert_to!(version)
revert_to(version) ? save_without_revision : false
@@ -313,36 +359,36 @@ module ActiveRecord #:nodoc:
def versioned_attributes
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
end
-
+
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
# If called with a single parameter, gets whether the parameter has changed.
def changed?(attr_name = nil)
attr_name.nil? ?
- (!self.class.track_changed_attributes || (changed_attributes && changed_attributes.length > 0)) :
- (changed_attributes && changed_attributes.include?(attr_name.to_s))
+ (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
+ (altered_attributes && altered_attributes.include?(attr_name.to_s))
end
-
+
# keep old dirty? method
alias_method :dirty?, :changed?
-
+
# Clones a model. Used when saving a new version or reverting a model's version.
def clone_versioned_model(orig_model, new_model)
self.versioned_attributes.each do |key|
- new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.has_attribute?(key)
+ new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
end
-
+
if orig_model.is_a?(self.class.versioned_class)
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
elsif new_model.is_a?(self.class.versioned_class)
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
end
end
-
+
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
def save_version?
version_condition_met? && changed?
end
-
+
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
# custom version condition checking.
def version_condition_met?
@@ -353,7 +399,7 @@ module ActiveRecord #:nodoc:
version_condition.call(self)
else
version_condition
- end
+ end
end
# Executes the block with the versioning callbacks disabled.
@@ -378,43 +424,45 @@ module ActiveRecord #:nodoc:
def empty_callback() end #:nodoc:
- protected
+ protected
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
def set_new_version
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
end
-
+
# Gets the next available version for the current record, or 1 for a new record
def next_version
return 1 if new_record?
(versions.calculate(:max, :version) || 0) + 1
end
-
+
# clears current changed attributes. Called after save.
- def clear_changed_attributes
- self.changed_attributes = []
+ def clear_altered_attributes
+ self.altered_attributes = []
end
def write_changed_attribute(attr_name, attr_value)
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
- (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
+ (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
write_attribute(attr_name, attr_value_for_db)
end
- private
- CALLBACKS.each do |attr_name|
- alias_method "orig_#{attr_name}".to_sym, attr_name
- end
-
module ClassMethods
# Finds a specific version of a specific row of this model
- def find_version(id, version)
- find_versions(id,
- :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version],
- :limit => 1).first
+ def find_version(id, version = nil)
+ return find(id) unless version
+
+ conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
+ options = { :conditions => conditions, :limit => 1 }
+
+ if result = find_versions(id, options).first
+ result
+ else
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
+ end
end
-
+
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
def find_versions(id, options = {})
versioned_class.find :all, {
@@ -426,7 +474,7 @@ module ActiveRecord #:nodoc:
def versioned_columns
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
end
-
+
# Returns an instance of the dynamic versioned model
def versioned_class
const_get versioned_class_name
@@ -438,36 +486,40 @@ module ActiveRecord #:nodoc:
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
self.connection.add_column table_name, :version, :integer
end
-
+
self.connection.create_table(versioned_table_name, create_table_options) do |t|
t.column versioned_foreign_key, :integer
t.column :version, :integer
end
-
+
updated_col = nil
self.versioned_columns.each do |col|
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
self.connection.add_column versioned_table_name, col.name, col.type,
:limit => col.limit,
- :default => col.default
+ :default => col.default,
+ :scale => col.scale,
+ :precision => col.precision
end
-
+
if type_col = self.columns_hash[inheritance_column]
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
:limit => type_col.limit,
- :default => type_col.default
+ :default => type_col.default,
+ :scale => type_col.scale,
+ :precision => type_col.precision
end
-
+
if updated_col.nil?
self.connection.add_column versioned_table_name, :updated_at, :timestamp
end
end
-
+
# Rake migration task to drop the versioned table
def drop_versioned_table
self.connection.drop_table versioned_table_name
end
-
+
# Executes the block with the versioning callbacks disabled.
#
# Foo.without_revision do
@@ -476,17 +528,18 @@ module ActiveRecord #:nodoc:
#
def without_revision(&block)
class_eval do
- CALLBACKS.each do |attr_name|
+ CALLBACKS.each do |attr_name|
+ alias_method "orig_#{attr_name}".to_sym, attr_name
alias_method attr_name, :empty_callback
end
end
- result = block.call
+ block.call
+ ensure
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, "orig_#{attr_name}".to_sym
end
end
- result
end
# Turns off optimistic locking for the duration of the block
@@ -501,7 +554,7 @@ module ActiveRecord #:nodoc:
result = block.call
ActiveRecord::Base.lock_optimistically = true if current
result
- end
+ end
end
end
end
diff --git a/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb
index 1740db8dc..86f50620c 100644
--- a/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb
+++ b/groups/vendor/plugins/acts_as_versioned/test/abstract_unit.rb
@@ -1,12 +1,21 @@
+$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
+$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
$:.unshift(File.dirname(__FILE__) + '/../lib')
-
require 'test/unit'
-require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
-require 'active_record/fixtures'
+begin
+ require 'active_support'
+ require 'active_record'
+ require 'active_record/fixtures'
+rescue LoadError
+ require 'rubygems'
+ retry
+end
+require 'acts_as_versioned'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
-ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite'])
+ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
+ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
load(File.dirname(__FILE__) + "/schema.rb")
@@ -19,17 +28,9 @@ if ENV['DB'] == 'postgresql'
end
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
-$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
+$:.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
- 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
-
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
diff --git a/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb
index 3c38f2fcf..086ac2b40 100644
--- a/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb
+++ b/groups/vendor/plugins/acts_as_versioned/test/fixtures/widget.rb
@@ -1,6 +1,6 @@
class Widget < ActiveRecord::Base
acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
- :dependent => nil, :order => 'version desc'
+ :dependent => :nullify, :order => 'version desc'
}
non_versioned_columns << 'foo'
end \ No newline at end of file
diff --git a/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb
index d85e95883..4ead4a8fe 100644
--- a/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb
+++ b/groups/vendor/plugins/acts_as_versioned/test/migration_test.rb
@@ -9,9 +9,14 @@ if ActiveRecord::Base.connection.supports_migrations?
class MigrationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
def teardown
- ActiveRecord::Base.connection.initialize_schema_information
- ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
-
+ if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
+ ActiveRecord::Base.connection.initialize_schema_information
+ ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
+ else
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
+ ActiveRecord::Base.connection.assume_migrated_upto_version(0)
+ end
+
Thing.connection.drop_table "things" rescue nil
Thing.connection.drop_table "thing_versions" rescue nil
Thing.reset_column_information
@@ -21,8 +26,17 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
# take 'er up
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
- t = Thing.create :title => 'blah blah'
+ t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
assert_equal 1, t.versions.size
+
+ # check that the price column has remembered its value correctly
+ assert_equal t.price, t.versions.first.price
+ assert_equal t.title, t.versions.first.title
+ assert_equal t[:type], t.versions.first[:type]
+
+ # make sure that the precision of the price column has been preserved
+ assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
+ assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
# now lets take 'er back down
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
diff --git a/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb
index c1e1a4b98..a7bc2082b 100644
--- a/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb
+++ b/groups/vendor/plugins/acts_as_versioned/test/versioned_test.rb
@@ -4,9 +4,10 @@ require File.join(File.dirname(__FILE__), 'fixtures/widget')
class VersionedTest < Test::Unit::TestCase
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
+ set_fixture_class :page_versions => Page::Version
def test_saves_versioned_copy
- p = Page.create :title => 'first title', :body => 'first body'
+ p = Page.create! :title => 'first title', :body => 'first body'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_equal 1, p.version
@@ -16,13 +17,13 @@ class VersionedTest < Test::Unit::TestCase
def test_saves_without_revision
p = pages(:welcome)
old_versions = p.versions.count
-
+
p.save_without_revision
-
+
p.without_revision do
p.update_attributes :title => 'changed'
end
-
+
assert_equal old_versions, p.versions.count
end
@@ -30,7 +31,7 @@ class VersionedTest < Test::Unit::TestCase
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
-
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
@@ -57,56 +58,56 @@ class VersionedTest < Test::Unit::TestCase
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
-
+
assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
-
+
def test_rollback_fails_with_invalid_revision
p = locked_pages(:welcome)
assert !p.revert_to!(locked_pages(:thinking))
end
def test_saves_versioned_copy_with_options
- p = LockedPage.create :title => 'first title'
+ p = LockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
end
-
+
def test_rollback_with_version_number_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
-
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
-
+
def test_rollback_with_version_class_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
-
+
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
-
+
def test_saves_versioned_copy_with_sti
- p = SpecialLockedPage.create :title => 'first title'
+ p = SpecialLockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
-
+
def test_rollback_with_version_number_with_sti
p = locked_pages(:thinking)
assert_equal 'So I was thinking', p.title
-
+
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
assert_equal 'So I was thinking!!!', p.title
assert_equal 'SpecialLockedPage', p.versions.first.version_type
@@ -115,11 +116,11 @@ class VersionedTest < Test::Unit::TestCase
def test_lock_version_works_with_versioning
p = locked_pages(:thinking)
p2 = LockedPage.find(p.id)
-
+
p.title = 'fresh title'
p.save
assert_equal 2, p.versions.size # limit!
-
+
assert_raises(ActiveRecord::StaleObjectError) do
p2.title = 'stale title'
p2.save
@@ -127,15 +128,15 @@ class VersionedTest < Test::Unit::TestCase
end
def test_version_if_condition
- p = Page.create :title => "title"
+ p = Page.create! :title => "title"
assert_equal 1, p.version
-
+
Page.feeling_good = false
p.save
assert_equal 1, p.version
Page.feeling_good = true
end
-
+
def test_version_if_condition2
# set new if condition
Page.class_eval do
@@ -143,46 +144,46 @@ class VersionedTest < Test::Unit::TestCase
alias_method :old_feeling_good, :feeling_good?
alias_method :feeling_good?, :new_feeling_good
end
-
- p = Page.create :title => "title"
+
+ p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
-
+
p.update_attributes(:title => 'new title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
-
+
p.update_attributes(:title => 'a title')
assert_equal 2, p.version
assert_equal 2, p.versions(true).size
-
+
# reset original if condition
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
end
-
+
def test_version_if_condition_with_block
# set new if condition
old_condition = Page.version_condition
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
-
- p = Page.create :title => "title"
+
+ p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
-
+
p.update_attributes(:title => 'a title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions(true).size
-
+
p.update_attributes(:title => 'b title')
assert_equal 2, p.version
assert_equal 2, p.versions(true).size
-
+
# reset original if condition
Page.version_condition = old_condition
end
def test_version_no_limit
- p = Page.create :title => "title", :body => 'first body'
+ p = Page.create! :title => "title", :body => 'first body'
p.save
p.save
5.times do |i|
@@ -191,7 +192,7 @@ class VersionedTest < Test::Unit::TestCase
end
def test_version_max_limit
- p = LockedPage.create :title => "title"
+ p = LockedPage.create! :title => "title"
p.update_attributes(:title => "title1")
p.update_attributes(:title => "title2")
5.times do |i|
@@ -199,31 +200,29 @@ class VersionedTest < Test::Unit::TestCase
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
end
end
-
- def test_track_changed_attributes_default_value
- assert !Page.track_changed_attributes
- assert LockedPage.track_changed_attributes
- assert SpecialLockedPage.track_changed_attributes
+
+ def test_track_altered_attributes_default_value
+ assert !Page.track_altered_attributes
+ assert LockedPage.track_altered_attributes
+ assert SpecialLockedPage.track_altered_attributes
end
-
+
def test_version_order
assert_equal 23, pages(:welcome).versions.first.version
assert_equal 24, pages(:welcome).versions.last.version
- assert_equal 23, pages(:welcome).find_versions.first.version
- assert_equal 24, pages(:welcome).find_versions.last.version
end
-
- def test_track_changed_attributes
- p = LockedPage.create :title => "title"
+
+ def test_track_altered_attributes
+ p = LockedPage.create! :title => "title"
assert_equal 1, p.lock_version
assert_equal 1, p.versions(true).size
-
+
p.title = 'title'
assert !p.save_version?
p.save
assert_equal 2, p.lock_version # still increments version because of optimistic locking
assert_equal 1, p.versions(true).size
-
+
p.title = 'updated title'
assert p.save_version?
p.save
@@ -236,27 +235,38 @@ class VersionedTest < Test::Unit::TestCase
assert_equal 4, p.lock_version
assert_equal 2, p.versions(true).size # version 1 deleted
end
-
+
def assert_page_title(p, i, version_field = :version)
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+4), p.send(version_field)
end
-
+
def test_find_versions
assert_equal 2, locked_pages(:welcome).versions.size
- assert_equal 1, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%weblog%']).length
- assert_equal 2, locked_pages(:welcome).find_versions(:conditions => ['title LIKE ?', '%web%']).length
- assert_equal 0, locked_pages(:thinking).find_versions(:conditions => ['title LIKE ?', '%web%']).length
- assert_equal 2, locked_pages(:welcome).find_versions.length
+ assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length
+ assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
+ assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
+ assert_equal 2, locked_pages(:welcome).versions.length
+ end
+
+ def test_find_version
+ assert_equal page_versions(:welcome_1), Page.find_version(pages(:welcome).id, 23)
+ assert_equal page_versions(:welcome_2), Page.find_version(pages(:welcome).id, 24)
+ assert_equal pages(:welcome), Page.find_version(pages(:welcome).id)
+
+ assert_equal page_versions(:welcome_1), pages(:welcome).find_version(23)
+ assert_equal page_versions(:welcome_2), pages(:welcome).find_version(24)
+ assert_equal pages(:welcome), pages(:welcome).find_version
+
+ assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(pages(:welcome).id, 1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(0, 23) }
end
-
+
def test_with_sequence
assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
- Widget.create :name => 'new widget'
- Widget.create :name => 'new widget'
- Widget.create :name => 'new widget'
+ 3.times { Widget.create! :name => 'new widget' }
assert_equal 3, Widget.count
assert_equal 3, Widget.versioned_class.count
end
@@ -268,26 +278,26 @@ class VersionedTest < Test::Unit::TestCase
def test_has_many_through_with_custom_association
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
end
-
+
def test_referential_integrity
pages(:welcome).destroy
assert_equal 0, Page.count
assert_equal 0, Page::Version.count
end
-
+
def test_association_options
association = Page.reflect_on_association(:versions)
options = association.options
assert_equal :delete_all, options[:dependent]
assert_equal 'version', options[:order]
-
+
association = Widget.reflect_on_association(:versions)
options = association.options
- assert_nil options[:dependent]
+ assert_equal :nullify, options[:dependent]
assert_equal 'version desc', options[:order]
assert_equal 'widget_id', options[:foreign_key]
-
- widget = Widget.create :name => 'new widget'
+
+ widget = Widget.create! :name => 'new widget'
assert_equal 1, Widget.count
assert_equal 1, Widget.versioned_class.count
widget.destroy
@@ -300,14 +310,38 @@ class VersionedTest < Test::Unit::TestCase
page_version = page.versions.last
assert_equal page, page_version.page
end
-
- def test_unchanged_attributes
- landmarks(:washington).attributes = landmarks(:washington).attributes
+
+ def test_unaltered_attributes
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
assert !landmarks(:washington).changed?
end
-
+
def test_unchanged_string_attributes
- landmarks(:washington).attributes = landmarks(:washington).attributes.inject({}) { |params, (key, value)| params.update key => value.to_s }
+ landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
assert !landmarks(:washington).changed?
end
-end
+
+ def test_should_find_earliest_version
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
+ end
+
+ def test_should_find_latest_version
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
+ end
+
+ def test_should_find_previous_version
+ assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
+ assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
+ end
+
+ def test_should_find_next_version
+ assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
+ assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
+ end
+
+ def test_should_find_version_count
+ assert_equal 24, pages(:welcome).versions_count
+ assert_equal 24, page_versions(:welcome_1).versions_count
+ assert_equal 24, page_versions(:welcome_2).versions_count
+ end
+end \ No newline at end of file
diff --git a/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb
index 53e4455cf..2cb122795 100644
--- a/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb
+++ b/groups/vendor/plugins/acts_as_watchable/lib/acts_as_watchable.rb
@@ -13,6 +13,7 @@ module Redmine
class_eval do
has_many :watchers, :as => :watchable, :dependent => :delete_all
+ has_many :watcher_users, :through => :watchers, :source => :user
end
end
end
@@ -22,25 +23,40 @@ module Redmine
base.extend ClassMethods
end
+ # Returns an array of users that are proposed as watchers
+ def addable_watcher_users
+ self.project.users.sort - self.watcher_users
+ end
+
+ # Adds user as a watcher
def add_watcher(user)
self.watchers << Watcher.new(:user => user)
end
+ # Removes user from the watchers list
def remove_watcher(user)
return nil unless user && user.is_a?(User)
Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
end
+ # Adds/removes watcher
+ def set_watcher(user, watching=true)
+ watching ? add_watcher(user) : remove_watcher(user)
+ end
+
+ # Returns if object is watched by user
def watched_by?(user)
!self.watchers.find(:first,
:conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
end
+ # Returns an array of watchers' email addresses
def watcher_recipients
self.watchers.collect { |w| w.user.mail if w.user.active? }.compact
end
module ClassMethods
+ # Returns the objects that are watched by user
def watched_by(user)
find(:all,
:include => :watchers,
@@ -50,4 +66,4 @@ module Redmine
end
end
end
-end \ No newline at end of file
+end
diff --git a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb
index f6d71ade2..e5d87b233 100644
--- a/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb
+++ b/groups/vendor/plugins/coderay-0.7.6.227/lib/coderay/scanners/c.rb
@@ -122,7 +122,7 @@ module Scanners
end
when :include_expected
- if scan(/<[^>\n]+>?|"[^"\n\\]*(?:\\.[^"\n\\]*)*"?/)
+ if scan(/[^\n]+/)
kind = :include
state = :initial
diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb
index f437410dc..51c024838 100644
--- a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb
+++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails-text.rb
@@ -138,8 +138,8 @@ module ActionView #:nodoc:
def add_options(option_tags, options, value = nil)
option_tags = "<option value=\"\"></option>\n" + option_tags if options[:include_blank]
- if value.blank? && options[:prompt]
- ("<option value=\"\">#{options[:prompt].kind_of?(String) ? options[:prompt] : l(:actionview_instancetag_blank_option)}</option>\n") + option_tags
+ if options[:prompt]
+ ("<option value=\"\">--- #{options[:prompt].kind_of?(String) ? options[:prompt] : l(:actionview_instancetag_blank_option)} ---</option>\n") + option_tags
else
option_tags
end
diff --git a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb
index aa65991b0..f05c4cddb 100644
--- a/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb
+++ b/groups/vendor/plugins/gloc-1.1.0/lib/gloc-rails.rb
@@ -168,7 +168,7 @@ module ActiveRecord #:nodoc:
if attr == "base"
full_messages << (msg.is_a?(Symbol) ? l(msg) : msg)
else
- full_messages << @base.class.human_attribute_name(attr) + " " + (msg.is_a?(Symbol) ? l(msg) : msg)
+ full_messages << @base.class.human_attribute_name(attr) + " " + (msg.is_a?(Symbol) ? l(msg) : msg.to_s)
end
end
end
diff --git a/groups/vendor/plugins/rfpdf/init.rb b/groups/vendor/plugins/rfpdf/init.rb
index 7e51d9eba..339bacfdb 100644
--- a/groups/vendor/plugins/rfpdf/init.rb
+++ b/groups/vendor/plugins/rfpdf/init.rb
@@ -1,3 +1,9 @@
require 'rfpdf'
-ActionView::Base::register_template_handler 'rfpdf', RFPDF::View \ No newline at end of file
+begin
+ ActionView::Template::register_template_handler 'rfpdf', RFPDF::View
+rescue NameError
+ # Rails < 2.1
+ RFPDF::View.backward_compatibility_mode = true
+ ActionView::Base::register_template_handler 'rfpdf', RFPDF::View
+end
diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb
index 6fe3eee8a..5684c702d 100644
--- a/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb
+++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/chinese.rb
@@ -1,473 +1,473 @@
-# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-# 1.12 contributed by Ed Moss.
-#
-# The MIT License
-#
-# 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.
-#
-# This is direct port of chinese.php
-#
-# Chinese PDF support.
-#
-# Usage is as follows:
-#
-# require 'fpdf'
-# require 'chinese'
-# pdf = FPDF.new
-# pdf.extend(PDF_Chinese)
-#
-# This allows it to be combined with other extensions, such as the bookmark
-# module.
-
-module PDF_Chinese
-
- Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250,
- '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500,
- '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250,
- '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625,
- 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823,
- 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677,
- 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427,
- 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802,
- 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677,
- 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667}
-
- GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239,
- '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462,
- '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238,
- '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563,
- 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772,
- 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620,
- 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427,
- 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793,
- 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652,
- 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605}
-
- def AddCIDFont(family,style,name,cw,cMap,registry)
-#ActionController::Base::logger.debug registry.to_a.join(":").to_s
- fontkey=family.downcase+style.upcase
- unless @fonts[fontkey].nil?
- Error("Font already added: family style")
- end
- i=@fonts.length+1
- name=name.gsub(' ','')
- @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry}
- end
-
- def AddCIDFonts(family,name,cw,cMap,registry)
- AddCIDFont(family,'',name,cw,cMap,registry)
- AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
- AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
- AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
- end
-
- def AddBig5Font(family='Big5',name='MSungStd-Light-Acro')
- #Add Big5 font with proportional Latin
- cw=Big5_widths
- cMap='ETenms-B5-H'
- registry={'ordering'=>'CNS1','supplement'=>0}
-#ActionController::Base::logger.debug registry.to_a.join(":").to_s
- AddCIDFonts(family,name,cw,cMap,registry)
- end
-
- def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro')
- #Add Big5 font with half-witdh Latin
- cw = {}
- 32.upto(126) do |i|
- cw[i.chr]=500
- end
- cMap='ETen-B5-H'
- registry={'ordering'=>'CNS1','supplement'=>0}
- AddCIDFonts(family,name,cw,cMap,registry)
- end
-
- def AddGBFont(family='GB',name='STSongStd-Light-Acro')
- #Add GB font with proportional Latin
- cw=GB_widths
- cMap='GBKp-EUC-H'
- registry={'ordering'=>'GB1','supplement'=>2}
- AddCIDFonts(family,name,cw,cMap,registry)
- end
-
- def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro')
- #Add GB font with half-width Latin
- 32.upto(126) do |i|
- cw[i.chr]=500
- end
- cMap='GBK-EUC-H'
- registry={'ordering'=>'GB1','supplement'=>2}
- AddCIDFonts(family,name,cw,cMap,registry)
- end
-
- def GetStringWidth(s)
- if(@CurrentFont['type']=='Type0')
- return GetMBStringWidth(s)
- else
- return super(s)
- end
- end
-
- def GetMBStringWidth(s)
- #Multi-byte version of GetStringWidth()
- l=0
- cw=@CurrentFont['cw']
- nb=s.length
- i=0
- while(i<nb)
- c=s[i]
- if(c<128)
- l+=cw[c.chr] if cw[c.chr]
- i+=1
- else
- l+=1000
- i+=2
- end
- end
- return l*@FontSize/1000
- end
-
- def MultiCell(w,h,txt,border=0,align='L',fill=0)
- if(@CurrentFont['type']=='Type0')
- MBMultiCell(w,h,txt,border,align,fill)
- else
- super(w,h,txt,border,align,fill)
- end
- end
-
- def MBMultiCell(w,h,txt,border=0,align='L',fill=0)
- #Multi-byte version of MultiCell()
- cw=@CurrentFont['cw']
- if(w==0)
- w=@w-@rMargin-@x
- end
- wmax=(w-2*@cMargin)*1000/@FontSize
- s=txt.gsub("\r",'')
- nb=s.length
- if(nb>0 and s[nb-1]=="\n")
- nb-=1
- end
- b=0
- if(border)
- if(border==1)
- border='LTRB'
- b='LRT'
- b2='LR'
- else
- b2=''
- if(border.to_s.index('L'))
- b2+='L'
- end
- if(border.to_s.index('R'))
- b2+='R'
- end
- b=border.to_s.index('T') ? b2+'T' : b2
- end
- end
- sep=-1
- i=0
- j=0
- l=0
- nl=1
- while(i<nb)
- #Get next character
- c=s[i]
- #Check if ASCII or MB
- ascii=(c<128)
- if(c=="\n")
- #Explicit line break
- Cell(w,h,s[j,i-j],b,2,align,fill)
- i+=1
- sep=-1
- j=i
- l=0
- nl+=1
- if(border and nl==2)
- b=b2
- end
- next
- end
- if(!ascii)
- sep=i
- ls=l
- elsif(c==' ')
- sep=i
- ls=l
- end
- l+=ascii ? (cw[c.chr] || 0) : 1000
- if(l>wmax)
- #Automatic line break
- if(sep==-1 or i==j)
- if(i==j)
- i+=ascii ? 1 : 2
- end
- Cell(w,h,s[j,i-j],b,2,align,fill)
- else
- Cell(w,h,s[j,sep-j],b,2,align,fill)
- i=(s[sep]==' ') ? sep+1 : sep
- end
- sep=-1
- j=i
- l=0
-# nl+=1
- if(border and nl==2)
- b=b2
- end
- else
- i+=ascii ? 1 : 2
- end
- end
- #Last chunk
- if(border and not border.to_s.index('B').nil?)
- b+='B'
- end
- Cell(w,h,s[j,i-j],b,2,align,fill)
- @x=@lMargin
- end
-
- def Write(h,txt,link='')
- if(@CurrentFont['type']=='Type0')
- MBWrite(h,txt,link)
- else
- super(h,txt,link)
- end
- end
-
- def MBWrite(h,txt,link)
- #Multi-byte version of Write()
- cw=@CurrentFont['cw']
- w=@w-@rMargin-@x
- wmax=(w-2*@cMargin)*1000/@FontSize
- s=txt.gsub("\r",'')
- nb=s.length
- sep=-1
- i=0
- j=0
- l=0
- nl=1
- while(i<nb)
- #Get next character
- c=s[i]
- #Check if ASCII or MB
- ascii=(c<128)
- if(c=="\n")
- #Explicit line break
- Cell(w,h,s[j,i-j],0,2,'',0,link)
- i+=1
- sep=-1
- j=i
- l=0
- if(nl==1)
- @x=@lMargin
- w=@w-@rMargin-@x
- wmax=(w-2*@cMargin)*1000/@FontSize
- end
- nl+=1
- next
- end
- if(!ascii or c==' ')
- sep=i
- end
- l+=ascii ? cw[c.chr] : 1000
- if(l>wmax)
- #Automatic line break
- if(sep==-1 or i==j)
- if(@x>@lMargin)
- #Move to next line
- @x=@lMargin
- @y+=h
- w=@w-@rMargin-@x
- wmax=(w-2*@cMargin)*1000/@FontSize
- i+=1
- nl+=1
- next
- end
- if(i==j)
- i+=ascii ? 1 : 2
- end
- Cell(w,h,s[j,i-j],0,2,'',0,link)
- else
- Cell(w,h,s[j,sep-j],0,2,'',0,link)
- i=(s[sep]==' ') ? sep+1 : sep
- end
- sep=-1
- j=i
- l=0
- if(nl==1)
- @x=@lMargin
- w=@w-@rMargin-@x
- wmax=(w-2*@cMargin)*1000/@FontSize
- end
- nl+=1
- else
- i+=ascii ? 1 : 2
- end
- end
- #Last chunk
- if(i!=j)
- Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)
- end
- end
-
-private
-
- def putfonts()
- nf=@n
- @diffs.each do |diff|
- #Encodings
- newobj()
- out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
- out('endobj')
- end
- # mqr=get_magic_quotes_runtime()
- # set_magic_quotes_runtime(0)
- @FontFiles.each_pair do |file, info|
- #Font file embedding
- newobj()
- @FontFiles[file]['n']=@n
- if(defined('FPDF_FONTPATH'))
- file=FPDF_FONTPATH+file
- end
- size=filesize(file)
- if(!size)
- Error('Font file not found')
- end
- out('<</Length '+size)
- if(file[-2]=='.z')
- out('/Filter /FlateDecode')
- end
- out('/Length1 '+info['length1'])
- unless info['length2'].nil?
- out('/Length2 '+info['length2']+' /Length3 0')
- end
- out('>>')
- f=fopen(file,'rb')
- putstream(fread(f,size))
- fclose(f)
- out('endobj')
- end
-#
- # set_magic_quotes_runtime(mqr)
-#
- @fonts.each_pair do |k, font|
- #Font objects
- newobj()
- @fonts[k]['n']=@n
- out('<</Type /Font')
- if(font['type']=='Type0')
- putType0(font)
- else
- name=font['name']
- out('/BaseFont /'+name)
- if(font['type']=='core')
- #Standard font
- out('/Subtype /Type1')
- if(name!='Symbol' and name!='ZapfDingbats')
- out('/Encoding /WinAnsiEncoding')
- end
- else
- #Additional font
- out('/Subtype /'+font['type'])
- out('/FirstChar 32')
- out('/LastChar 255')
- out('/Widths '+(@n+1)+' 0 R')
- out('/FontDescriptor '+(@n+2)+' 0 R')
- if(font['enc'])
- if !font['diff'].nil?
- out('/Encoding '+(nf+font['diff'])+' 0 R')
- else
- out('/Encoding /WinAnsiEncoding')
- end
- end
- end
- out('>>')
- out('endobj')
- if(font['type']!='core')
- #Widths
- newobj()
- cw=font['cw']
- s='['
- 32.upto(255) do |i|
- s+=cw[i.chr]+' '
- end
- out(s+']')
- out('endobj')
- #Descriptor
- newobj()
- s='<</Type /FontDescriptor /FontName /'+name
- font['desc'].each_pair do |k, v|
- s+=' /'+k+' '+v
- end
- file=font['file']
- if(file)
- s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'
- end
- out(s+'>>')
- out('endobj')
- end
- end
- end
- end
-
- def putType0(font)
- #Type0
- out('/Subtype /Type0')
- out('/BaseFont /'+font['name']+'-'+font['CMap'])
- out('/Encoding /'+font['CMap'])
- out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
- out('>>')
- out('endobj')
- #CIDFont
- newobj()
- out('<</Type /Font')
- out('/Subtype /CIDFontType0')
- out('/BaseFont /'+font['name'])
- out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')
- out('/FontDescriptor '+(@n+1).to_s+' 0 R')
- if(font['CMap']=='ETen-B5-H')
- w='13648 13742 500'
- elsif(font['CMap']=='GBK-EUC-H')
- w='814 907 500 7716 [500]'
- else
- # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s
- # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s
- w='1 ['
- font['cw'].keys.sort.each {|key|
- w+=font['cw'][key].to_s + " "
-# ActionController::Base::logger.debug key.to_s
-# ActionController::Base::logger.debug font['cw'][key].to_s
- }
- w +=']'
- end
- out('/W ['+w+']>>')
- out('endobj')
- #Font descriptor
- newobj()
- out('<</Type /FontDescriptor')
- out('/FontName /'+font['name'])
- out('/Flags 6')
- out('/FontBBox [0 -200 1000 900]')
- out('/ItalicAngle 0')
- out('/Ascent 800')
- out('/Descent -200')
- out('/CapHeight 800')
- out('/StemV 50')
- out('>>')
- out('endobj')
- end
-end
+# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
+# 1.12 contributed by Ed Moss.
+#
+# The MIT License
+#
+# 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.
+#
+# This is direct port of chinese.php
+#
+# Chinese PDF support.
+#
+# Usage is as follows:
+#
+# require 'fpdf'
+# require 'chinese'
+# pdf = FPDF.new
+# pdf.extend(PDF_Chinese)
+#
+# This allows it to be combined with other extensions, such as the bookmark
+# module.
+
+module PDF_Chinese
+
+ Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,''=>490,'%'=>875,'&'=>698,'\''=>250,
+ '('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500,
+ '2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,''=>250,
+ '<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625,
+ 'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823,
+ 'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677,
+ 'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427,
+ 'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802,
+ 'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677,
+ 'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'end'=>480,'~'=>667}
+
+ GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,''=>462,'%'=>797,'&'=>710,'\''=>239,
+ '('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462,
+ '2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,''=>238,
+ '<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563,
+ 'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772,
+ 'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620,
+ 'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427,
+ 'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793,
+ 'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652,
+ 'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'end'=>370,'~'=>605}
+
+ def AddCIDFont(family,style,name,cw,cMap,registry)
+#ActionController::Base::logger.debug registry.to_a.join(":").to_s
+ fontkey=family.downcase+style.upcase
+ unless @fonts[fontkey].nil?
+ Error("Font already added: family style")
+ end
+ i=@fonts.length+1
+ name=name.gsub(' ','')
+ @fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry}
+ end
+
+ def AddCIDFonts(family,name,cw,cMap,registry)
+ AddCIDFont(family,'',name,cw,cMap,registry)
+ AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
+ AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
+ AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
+ end
+
+ def AddBig5Font(family='Big5',name='MSungStd-Light-Acro')
+ #Add Big5 font with proportional Latin
+ cw=Big5_widths
+ cMap='ETenms-B5-H'
+ registry={'ordering'=>'CNS1','supplement'=>0}
+#ActionController::Base::logger.debug registry.to_a.join(":").to_s
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro')
+ #Add Big5 font with half-witdh Latin
+ cw = {}
+ 32.upto(126) do |i|
+ cw[i.chr]=500
+ end
+ cMap='ETen-B5-H'
+ registry={'ordering'=>'CNS1','supplement'=>0}
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def AddGBFont(family='GB',name='STSongStd-Light-Acro')
+ #Add GB font with proportional Latin
+ cw=GB_widths
+ cMap='GBKp-EUC-H'
+ registry={'ordering'=>'GB1','supplement'=>2}
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro')
+ #Add GB font with half-width Latin
+ 32.upto(126) do |i|
+ cw[i.chr]=500
+ end
+ cMap='GBK-EUC-H'
+ registry={'ordering'=>'GB1','supplement'=>2}
+ AddCIDFonts(family,name,cw,cMap,registry)
+ end
+
+ def GetStringWidth(s)
+ if(@CurrentFont['type']=='Type0')
+ return GetMBStringWidth(s)
+ else
+ return super(s)
+ end
+ end
+
+ def GetMBStringWidth(s)
+ #Multi-byte version of GetStringWidth()
+ l=0
+ cw=@CurrentFont['cw']
+ nb=s.length
+ i=0
+ while(i<nb)
+ c=s[i]
+ if(c<128)
+ l+=cw[c.chr] if cw[c.chr]
+ i+=1
+ else
+ l+=1000
+ i+=2
+ end
+ end
+ return l*@FontSize/1000
+ end
+
+ def MultiCell(w,h,txt,border=0,align='L',fill=0)
+ if(@CurrentFont['type']=='Type0')
+ MBMultiCell(w,h,txt,border,align,fill)
+ else
+ super(w,h,txt,border,align,fill)
+ end
+ end
+
+ def MBMultiCell(w,h,txt,border=0,align='L',fill=0)
+ #Multi-byte version of MultiCell()
+ cw=@CurrentFont['cw']
+ if(w==0)
+ w=@w-@rMargin-@x
+ end
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ if(nb>0 and s[nb-1]=="\n")
+ nb-=1
+ end
+ b=0
+ if(border)
+ if(border==1)
+ border='LTRB'
+ b='LRT'
+ b2='LR'
+ else
+ b2=''
+ if(border.to_s.index('L'))
+ b2+='L'
+ end
+ if(border.to_s.index('R'))
+ b2+='R'
+ end
+ b=border.to_s.index('T') ? b2+'T' : b2
+ end
+ end
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ #Check if ASCII or MB
+ ascii=(c<128)
+ if(c.chr=="\n")
+ #Explicit line break
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ nl+=1
+ if(border and nl==2)
+ b=b2
+ end
+ next
+ end
+ if(!ascii)
+ sep=i
+ ls=l
+ elsif(c==' ')
+ sep=i
+ ls=l
+ end
+ l+=ascii ? (cw[c.chr] || 0) : 1100
+ if(l>wmax)
+ #Automatic line break
+ if(sep==-1 or i==j)
+ if(i==j)
+ i+=ascii ? 1 : 3
+ end
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ else
+ Cell(w,h,s[j,sep-j],b,2,align,fill)
+ i=(s[sep]==' ') ? sep+1 : sep
+ end
+ sep=-1
+ j=i
+ l=0
+# nl+=1
+ if(border and nl==2)
+ b=b2
+ end
+ else
+ i+=ascii ? 1 : 3
+ end
+ end
+ #Last chunk
+ if(border and not border.to_s.index('B').nil?)
+ b+='B'
+ end
+ Cell(w,h,s[j,i-j],b,2,align,fill)
+ @x=@lMargin
+ end
+
+ def Write(h,txt,link='')
+ if(@CurrentFont['type']=='Type0')
+ MBWrite(h,txt,link)
+ else
+ super(h,txt,link)
+ end
+ end
+
+ def MBWrite(h,txt,link)
+ #Multi-byte version of Write()
+ cw=@CurrentFont['cw']
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ s=txt.gsub("\r",'')
+ nb=s.length
+ sep=-1
+ i=0
+ j=0
+ l=0
+ nl=1
+ while(i<nb)
+ #Get next character
+ c=s[i]
+ #Check if ASCII or MB
+ ascii=(c<128)
+ if(c.chr=="\n")
+ #Explicit line break
+ Cell(w,h,s[j,i-j],0,2,'',0,link)
+ i+=1
+ sep=-1
+ j=i
+ l=0
+ if(nl==1)
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl+=1
+ next
+ end
+ if(!ascii or c==' ')
+ sep=i
+ end
+ l+=ascii ? cw[c.chr] : 1100
+ if(l>wmax)
+ #Automatic line break
+ if(sep==-1 or i==j)
+ if(@x>@lMargin)
+ #Move to next line
+ @x=@lMargin
+ @y+=h
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ i+=1
+ nl+=1
+ next
+ end
+ if(i==j)
+ i+=ascii ? 1 : 3
+ end
+ Cell(w,h,s[j,i-j],0,2,'',0,link)
+ else
+ Cell(w,h,s[j,sep-j],0,2,'',0,link)
+ i=(s[sep]==' ') ? sep+1 : sep
+ end
+ sep=-1
+ j=i
+ l=0
+ if(nl==1)
+ @x=@lMargin
+ w=@w-@rMargin-@x
+ wmax=(w-2*@cMargin)*1000/@FontSize
+ end
+ nl+=1
+ else
+ i+=ascii ? 1 : 3
+ end
+ end
+ #Last chunk
+ if(i!=j)
+ Cell(l/1000*@FontSize,h,s[j,i-j],0,0,'',0,link)
+ end
+ end
+
+private
+
+ def putfonts()
+ nf=@n
+ @diffs.each do |diff|
+ #Encodings
+ newobj()
+ out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
+ out('endobj')
+ end
+ # mqr=get_magic_quotes_runtime()
+ # set_magic_quotes_runtime(0)
+ @FontFiles.each_pair do |file, info|
+ #Font file embedding
+ newobj()
+ @FontFiles[file]['n']=@n
+ if(defined('FPDF_FONTPATH'))
+ file=FPDF_FONTPATH+file
+ end
+ size=filesize(file)
+ if(!size)
+ Error('Font file not found')
+ end
+ out('<</Length '+size)
+ if(file[-2]=='.z')
+ out('/Filter /FlateDecode')
+ end
+ out('/Length1 '+info['length1'])
+ unless info['length2'].nil?
+ out('/Length2 '+info['length2']+' /Length3 0')
+ end
+ out('>>')
+ f=fopen(file,'rb')
+ putstream(fread(f,size))
+ fclose(f)
+ out('endobj')
+ end
+#
+ # set_magic_quotes_runtime(mqr)
+#
+ @fonts.each_pair do |k, font|
+ #Font objects
+ newobj()
+ @fonts[k]['n']=@n
+ out('<</Type /Font')
+ if(font['type']=='Type0')
+ putType0(font)
+ else
+ name=font['name']
+ out('/BaseFont /'+name)
+ if(font['type']=='core')
+ #Standard font
+ out('/Subtype /Type1')
+ if(name!='Symbol' and name!='ZapfDingbats')
+ out('/Encoding /WinAnsiEncoding')
+ end
+ else
+ #Additional font
+ out('/Subtype /'+font['type'])
+ out('/FirstChar 32')
+ out('/LastChar 255')
+ out('/Widths '+(@n+1)+' 0 R')
+ out('/FontDescriptor '+(@n+2)+' 0 R')
+ if(font['enc'])
+ if !font['diff'].nil?
+ out('/Encoding '+(nf+font['diff'])+' 0 R')
+ else
+ out('/Encoding /WinAnsiEncoding')
+ end
+ end
+ end
+ out('>>')
+ out('endobj')
+ if(font['type']!='core')
+ #Widths
+ newobj()
+ cw=font['cw']
+ s='['
+ 32.upto(255) do |i|
+ s+=cw[i.chr]+' '
+ end
+ out(s+']')
+ out('endobj')
+ #Descriptor
+ newobj()
+ s='<</Type /FontDescriptor /FontName /'+name
+ font['desc'].each_pair do |k, v|
+ s+=' /'+k+' '+v
+ end
+ file=font['file']
+ if(file)
+ s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@FontFiles[file]['n']+' 0 R'
+ end
+ out(s+'>>')
+ out('endobj')
+ end
+ end
+ end
+ end
+
+ def putType0(font)
+ #Type0
+ out('/Subtype /Type0')
+ out('/BaseFont /'+font['name']+'-'+font['CMap'])
+ out('/Encoding /'+font['CMap'])
+ out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
+ out('>>')
+ out('endobj')
+ #CIDFont
+ newobj()
+ out('<</Type /Font')
+ out('/Subtype /CIDFontType0')
+ out('/BaseFont /'+font['name'])
+ out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')
+ out('/FontDescriptor '+(@n+1).to_s+' 0 R')
+ if(font['CMap']=='ETen-B5-H')
+ w='13648 13742 500'
+ elsif(font['CMap']=='GBK-EUC-H')
+ w='814 907 500 7716 [500]'
+ else
+ # ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s
+ # ActionController::Base::logger.debug font['cw'].values.join(' ').to_s
+ w='1 ['
+ font['cw'].keys.sort.each {|key|
+ w+=font['cw'][key].to_s + " "
+# ActionController::Base::logger.debug key.to_s
+# ActionController::Base::logger.debug font['cw'][key].to_s
+ }
+ w +=']'
+ end
+ out('/W ['+w+']>>')
+ out('endobj')
+ #Font descriptor
+ newobj()
+ out('<</Type /FontDescriptor')
+ out('/FontName /'+font['name'])
+ out('/Flags 6')
+ out('/FontBBox [0 -200 1000 900]')
+ out('/ItalicAngle 0')
+ out('/Ascent 800')
+ out('/Descent -200')
+ out('/CapHeight 800')
+ out('/StemV 50')
+ out('>>')
+ out('endobj')
+ end
+end
diff --git a/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb
index 185811202..6b6267331 100644
--- a/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb
+++ b/groups/vendor/plugins/rfpdf/lib/rfpdf/view.rb
@@ -30,6 +30,8 @@
module RFPDF
class View
+ @@backward_compatibility_mode = false
+ cattr_accessor :backward_compatibility_mode
def initialize(action_view)
@action_view = action_view
@@ -45,6 +47,14 @@ module RFPDF
:temp_dir => "#{File.expand_path(RAILS_ROOT)}/tmp"
}.merge(@action_view.controller.instance_eval{ @options_for_rfpdf } || {}).with_indifferent_access
end
+
+ def self.compilable?
+ false
+ end
+
+ def compilable?
+ self.class.compilable?
+ end
def render(template, local_assigns = {})
@pdf_name = "Default.pdf" if @pdf_name.nil?
@@ -66,7 +76,7 @@ module RFPDF
local_assigns.each do |key,val|
class << self; self; end.send(:define_method,key){ val }
end
- ERB.new(template).result(binding)
+ ERB.new(@@backward_compatibility_mode == true ? template : template.source).result(binding)
end
end