Mercurial > hg > soundsoftware-site
changeset 1294:3e4c3460b6ca redmine-2.2
Update to Redmine SVN revision 11972 on 2.2-stable branch
line wrap: on
line diff
--- a/.hgignore Mon Jan 07 12:01:42 2013 +0000 +++ b/.hgignore Fri Jun 14 09:01:12 2013 +0100 @@ -29,10 +29,7 @@ vendor/cache vendor/rails *.rbc - -.svn/ .git/ - .bundle Gemfile.lock Gemfile.local
--- a/Gemfile Mon Jan 07 12:01:42 2013 +0000 +++ b/Gemfile Fri Jun 14 09:01:12 2013 +0100 @@ -1,6 +1,6 @@ source 'http://rubygems.org' -gem 'rails', '3.2.10' +gem "rails", "3.2.13" gem "jquery-rails", "~> 2.0.2" gem "i18n", "~> 0.6.0" gem "coderay", "~> 1.0.6" @@ -79,7 +79,7 @@ platforms = [:mri_19] platforms << :jruby if defined?(JRUBY_VERSION) && JRUBY_VERSION >= "1.7" gem "test-unit", :platforms => platforms - gem "mocha", "0.12.3" + gem "mocha", "~> 0.13.3" end local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
--- a/app/controllers/boards_controller.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/controllers/boards_controller.rb Fri Jun 14 09:01:12 2013 +0100 @@ -39,14 +39,18 @@ sort_init 'updated_on', 'desc' sort_update 'created_on' => "#{Message.table_name}.created_on", 'replies' => "#{Message.table_name}.replies_count", - 'updated_on' => "#{Message.table_name}.updated_on" + 'updated_on' => "COALESCE(last_replies_messages.created_on, #{Message.table_name}.created_on)" @topic_count = @board.topics.count @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page'] - @topics = @board.topics.reorder("#{Message.table_name}.sticky DESC").order(sort_clause).all( - :include => [:author, {:last_reply => :author}], - :limit => @topic_pages.items_per_page, - :offset => @topic_pages.current.offset) + @topics = @board.topics. + reorder("#{Message.table_name}.sticky DESC"). + includes(:last_reply). + limit(@topic_pages.items_per_page). + offset(@topic_pages.current.offset). + order(sort_clause). + preload(:author, {:last_reply => :author}). + all @message = Message.new(:board => @board) render :action => 'show', :layout => !request.xhr? }
--- a/app/controllers/messages_controller.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/controllers/messages_controller.rb Fri Jun 14 09:01:12 2013 +0100 @@ -123,7 +123,7 @@ private def find_message - find_board + return unless find_board @message = @board.messages.find(params[:id], :include => :parent) @topic = @message.root rescue ActiveRecord::RecordNotFound @@ -135,5 +135,6 @@ @project = @board.project rescue ActiveRecord::RecordNotFound render_404 + nil end end
--- a/app/controllers/my_controller.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/controllers/my_controller.rb Fri Jun 14 09:01:12 2013 +0100 @@ -147,15 +147,16 @@ # params[:block] : id of the block to add def add_block block = params[:block].to_s.underscore - (render :nothing => true; return) unless block && (BLOCKS.keys.include? block) - @user = User.current - layout = @user.pref[:my_page_layout] || {} - # remove if already present in a group - %w(top left right).each {|f| (layout[f] ||= []).delete block } - # add it on top - layout['top'].unshift block - @user.pref[:my_page_layout] = layout - @user.pref.save + if block.present? && BLOCKS.key?(block) + @user = User.current + layout = @user.pref[:my_page_layout] || {} + # remove if already present in a group + %w(top left right).each {|f| (layout[f] ||= []).delete block } + # add it on top + layout['top'].unshift block + @user.pref[:my_page_layout] = layout + @user.pref.save + end redirect_to :action => 'page_layout' end
--- a/app/helpers/application_helper.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/helpers/application_helper.rb Fri Jun 14 09:01:12 2013 +0100 @@ -597,8 +597,9 @@ def parse_inline_attachments(text, project, obj, attr, only_path, options) # when using an image link, try to use an attachment, if possible - if options[:attachments] || (obj && obj.respond_to?(:attachments)) - attachments = options[:attachments] || obj.attachments + attachments = options[:attachments] || [] + attachments += obj.attachments if obj.respond_to?(:attachments) + if attachments.present? text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| filename, ext, alt, alttext = $1.downcase, $2, $3, $4 # search for the picture in attachments @@ -703,10 +704,11 @@ # identifier:document:"Some document" # identifier:version:1.0.0 # identifier:source:some/file - def parse_redmine_links(text, project, obj, attr, only_path, options) - text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| + def parse_redmine_links(text, default_project, obj, attr, only_path, options) + text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m| leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 link = nil + project = default_project if project_identifier project = Project.visible.find_by_identifier(project_identifier) end @@ -792,7 +794,7 @@ when 'commit', 'source', 'export' if project repository = nil - if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$} + if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$} repo_prefix, repo_identifier, name = $1, $2, $3 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} else @@ -819,7 +821,7 @@ end when 'attachment' attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) - if attachments && attachment = attachments.detect {|a| a.filename == name } + if attachments && attachment = Attachment.latest_attach(attachments, name) link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, :class => 'attachment' end
--- a/app/helpers/issues_helper.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/helpers/issues_helper.rb Fri Jun 14 09:01:12 2013 +0100 @@ -350,7 +350,10 @@ association = Issue.reflect_on_association(field.to_sym) if association record = association.class_name.constantize.find_by_id(id) - return record.name if record + if record + record.name.force_encoding('UTF-8') if record.name.respond_to?(:force_encoding) + return record.name + end end end
--- a/app/helpers/queries_helper.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/helpers/queries_helper.rb Fri Jun 14 09:01:12 2013 +0100 @@ -87,14 +87,14 @@ format_time(value) when 'Date' format_date(value) - when 'Fixnum', 'Float' + when 'Fixnum' if column.name == :done_ratio progress_bar(value, :width => '80px') - elsif column.name == :spent_hours - sprintf "%.2f", value else - h(value.to_s) + value.to_s end + when 'Float' + sprintf "%.2f", value when 'User' link_to_user value when 'Project'
--- a/app/models/issue.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/models/issue.rb Fri Jun 14 09:01:12 2013 +0100 @@ -418,7 +418,7 @@ if attrs['parent_issue_id'].present? s = attrs['parent_issue_id'].to_s - unless (m = s.match(%r{\A#?(\d+)\z})) && Issue.visible(user).exists?(m[1]) + unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1])) @invalid_parent_issue_id = attrs.delete('parent_issue_id') end end
--- a/app/models/mail_handler.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/models/mail_handler.rb Fri Jun 14 09:01:12 2013 +0100 @@ -249,26 +249,9 @@ def add_attachments(obj) if email.attachments && email.attachments.any? email.attachments.each do |attachment| - filename = attachment.filename - unless filename.respond_to?(:encoding) - # try to reencode to utf8 manually with ruby1.8 - h = attachment.header['Content-Disposition'] - unless h.nil? - begin - if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/) - filename = Redmine::CodesetUtil.to_utf8(filename, m[1]) - elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/) - # http://tools.ietf.org/html/rfc2047#section-4 - filename = Redmine::CodesetUtil.to_utf8(filename, m[1]) - end - rescue - # nop - end - end - end obj.attachments << Attachment.create(:container => obj, :file => attachment.decoded, - :filename => filename, + :filename => attachment.filename, :author => user, :content_type => attachment.mime_type) end @@ -391,19 +374,6 @@ def cleaned_up_subject subject = email.subject.to_s - unless subject.respond_to?(:encoding) - # try to reencode to utf8 manually with ruby1.8 - begin - if h = email.header[:subject] - # http://tools.ietf.org/html/rfc2047#section-4 - if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/) - subject = Redmine::CodesetUtil.to_utf8(subject, m[1]) - end - end - rescue - # nop - end - end subject.strip[0,255] end
--- a/app/models/project.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/models/project.rb Fri Jun 14 09:01:12 2013 +0100 @@ -731,7 +731,7 @@ def copy_wiki(project) # Check that the source project has a wiki first unless project.wiki.nil? - self.wiki ||= Wiki.new + wiki = self.wiki || Wiki.new wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") wiki_pages_map = {} project.wiki.pages.each do |page| @@ -743,6 +743,8 @@ wiki.pages << new_wiki_page wiki_pages_map[page.id] = new_wiki_page end + + self.wiki = wiki wiki.save # Reproduce page hierarchy project.wiki.pages.each do |page|
--- a/app/views/context_menus/issues.html.erb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/views/context_menus/issues.html.erb Fri Jun 14 09:01:12 2013 +0100 @@ -111,7 +111,7 @@ <li><%= bulk_update_custom_field_context_menu_link(field, text, value || text) %></li> <% end %> <% unless field.is_required? %> - <li><%= bulk_update_custom_field_context_menu_link(field, l(:label_none), '') %></li> + <li><%= bulk_update_custom_field_context_menu_link(field, l(:label_none), '__none__') %></li> <% end %> </ul> </li>
--- a/app/views/issues/_form.html.erb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/views/issues/_form.html.erb Fri Jun 14 09:01:12 2013 +0100 @@ -18,7 +18,7 @@ <% end %> <% if @issue.safe_attribute? 'subject' %> -<p><%= f.text_field :subject, :size => 80, :required => true %></p> +<p><%= f.text_field :subject, :size => 80, :maxlength => 255, :required => true %></p> <% end %> <% if @issue.safe_attribute? 'description' %>
--- a/app/views/timelog/_form.html.erb Mon Jan 07 12:01:42 2013 +0000 +++ b/app/views/timelog/_form.html.erb Fri Jun 14 09:01:12 2013 +0100 @@ -12,7 +12,7 @@ <p><%= f.text_field :issue_id, :size => 6 %> <em><%= h("#{@time_entry.issue.tracker.name} ##{@time_entry.issue.id}: #{@time_entry.issue.subject}") if @time_entry.issue %></em></p> <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.text_field :comments, :size => 100, :maxlength => 255 %></p> <p><%= f.select :activity_id, activity_collection_for_select_options(@time_entry), :required => true %></p> <% @time_entry.custom_field_values.each do |value| %> <p><%= custom_field_tag_with_label :time_entry, value %></p>
--- a/config/locales/bg.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/bg.yml Fri Jun 14 09:01:12 2013 +0100 @@ -833,7 +833,7 @@ label_generate_key: Генериране на ключ label_issue_watchers: Наблюдатели label_example: Пример - label_display: Display + label_display: Показване label_sort: Сортиране label_ascending: Нарастващ label_descending: Намаляващ
--- a/config/locales/cs.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/cs.yml Fri Jun 14 09:01:12 2013 +0100 @@ -1,3 +1,4 @@ +# Update to 2.2 by Karel Picman <karel.picman@kontron.com> # Update to 1.1 by Michal Gebauer <mishak@mishak.net> # Updated by Josef Liška <jl@chl.cz> # CZ translation by Maxim Krušina | Massimo Filippi, s.r.o. | maxim@mxm.cz @@ -941,146 +942,146 @@ enumeration_activities: Aktivity (sledování času) enumeration_system_activity: Systémová aktivita - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: Priority updated - label_issues_visibility_own: Issues created by or assigned to the user - field_issues_visibility: Issues visibility - label_issues_visibility_all: All issues - permission_set_own_issues_private: Set own issues public or private - field_is_private: Private - permission_set_issues_private: Set issues public or private - label_issues_visibility_public: All non private issues - text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + field_warn_on_leaving_unsaved: Varuj mě před opuštěním stránky s neuloženým textem + text_warn_on_leaving_unsaved: Aktuální stránka obsahuje neuložený text, který bude ztracen, když opustíte stránku. + label_my_queries: Moje vlastní dotazy + text_journal_changed_no_detail: "%{label} aktualizován" + label_news_comment_added: K novince byl přidán komentář + button_expand_all: Rozbal vše + button_collapse_all: Sbal vše + label_additional_workflow_transitions_for_assignee: Další změna stavu povolena, jestliže je uživatel přiřazen + label_additional_workflow_transitions_for_author: Další změna stavu povolena, jestliže je uživatel autorem + label_bulk_edit_selected_time_entries: Hromadná změna záznamů času + text_time_entries_destroy_confirmation: Jste si jistí, že chcete smazat vybraný záznam(y) času? + label_role_anonymous: Anonymní + label_role_non_member: Není členem + label_issue_note_added: Přidána poznámka + label_issue_status_updated: Aktualizován stav + label_issue_priority_updated: Aktualizována priorita + label_issues_visibility_own: Úkol vytvořen nebo přiřazen uživatel(i/em) + field_issues_visibility: Viditelnost úkolů + label_issues_visibility_all: Všechny úkoly + permission_set_own_issues_private: Nastavit vlastní úkoly jako veřejné nebo soukromé + field_is_private: Soukromý + permission_set_issues_private: Nastavit úkoly jako veřejné nebo soukromé + label_issues_visibility_public: Všechny úkoly, které nejsou soukromé + text_issues_destroy_descendants_confirmation: "%{count} podúkol(ů) bude rovněž smazán(o)." field_commit_logs_encoding: Kódování zpráv při commitu - field_scm_path_encoding: Path encoding - text_scm_path_encoding_note: "Default: UTF-8" - field_path_to_repository: Path to repository - field_root_directory: Root directory - field_cvs_module: Module + field_scm_path_encoding: Kódování cesty SCM + text_scm_path_encoding_note: "Výchozí: UTF-8" + field_path_to_repository: Cesta k repositáři + field_root_directory: Kořenový adresář + field_cvs_module: Modul field_cvsroot: CVSROOT - text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) - text_scm_command: Command - text_scm_command_version: Version - label_git_report_last_commit: Report last commit for files and directories - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - notice_issue_successful_create: Issue %{id} created. - label_between: between - setting_issue_group_assignment: Allow issue assignment to groups - label_diff: diff - text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) - description_query_sort_criteria_direction: Sort direction - description_project_scope: Search scope - description_filter: Filter - description_user_mail_notification: Mail notification settings - description_date_from: Enter start date - description_message_content: Message content - description_available_columns: Available Columns - description_date_range_interval: Choose range by selecting start and end date - description_issue_category_reassign: Choose issue category - description_search: Searchfield - description_notes: Notes - description_date_range_list: Choose range from list - description_choose_project: Projects - description_date_to: Enter end date - description_query_sort_criteria_attribute: Sort attribute - description_wiki_subpages_reassign: Choose new parent page - description_selected_columns: Selected Columns - label_parent_revision: Parent - label_child_revision: Child - error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. - setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues - button_edit_section: Edit this section - setting_repositories_encodings: Attachments and repositories encodings - description_all_columns: All Columns + text_mercurial_repository_note: Lokální repositář (např. /hgrepo, c:\hgrepo) + text_scm_command: Příkaz + text_scm_command_version: Verze + label_git_report_last_commit: Reportovat poslední commit pro soubory a adresáře + text_scm_config: Můžete si nastavit vaše SCM příkazy v config/configuration.yml. Restartujte, prosím, aplikaci po jejich úpravě. + text_scm_command_not_available: SCM příkaz není k dispozici. Zkontrolujte, prosím, nastavení v panelu Administrace. + notice_issue_successful_create: Úkol %{id} vytvořen. + label_between: mezi + setting_issue_group_assignment: Povolit přiřazení úkolu skupině + label_diff: rozdíl + text_git_repository_note: Repositář je "bare and local" (např. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Směr třídění + description_project_scope: Rozsah vyhledávání + description_filter: Filtr + description_user_mail_notification: Nastavení emailových notifikací + description_date_from: Zadejte počáteční datum + description_message_content: Obsah zprávy + description_available_columns: Dostupné sloupce + description_date_range_interval: Zvolte rozsah výběrem počátečního a koncového data + description_issue_category_reassign: Zvolte kategorii úkolu + description_search: Vyhledávací pole + description_notes: Poznámky + description_date_range_list: Zvolte rozsah ze seznamu + description_choose_project: Projekty + description_date_to: Zadejte datum + description_query_sort_criteria_attribute: Třídící atribut + description_wiki_subpages_reassign: Zvolte novou rodičovskou stránku + description_selected_columns: Vybraný sloupec + label_parent_revision: Rodič + label_child_revision: Potomek + error_scm_annotate_big_text_file: Vstup nemůže být anotován, protože překračuje povolenou velikost textového souboru + setting_default_issue_start_date_to_creation_date: Použij aktuální datum jako počáteční datum pro nové úkoly + button_edit_section: Uprav tuto sekci + setting_repositories_encodings: Kódování příloh a repositářů + description_all_columns: Všechny sloupce button_export: Export - label_export_options: "%{export_format} export options" - error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) - notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_export_options: "nastavení exportu %{export_format}" + error_attachment_too_big: Soubor nemůže být nahrán, protože jeho velikost je větší než maximum (%{max_size}) + notice_failed_to_save_time_entries: "Chyba při ukládání %{count} časov(ých/ého) záznam(ů) z %{total} vybraného: %{ids}." label_x_issues: zero: 0 Úkol one: 1 Úkol other: "%{count} Úkoly" - label_repository_new: New repository - field_repository_is_default: Main repository - label_copy_attachments: Copy attachments + label_repository_new: Nový repositář + field_repository_is_default: Hlavní repositář + label_copy_attachments: Kopírovat přílohy label_item_position: "%{position}/%{count}" - label_completed_versions: Completed versions - text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account + label_completed_versions: Dokončené verze + text_project_identifier_info: Jsou povolena pouze malá písmena (a-z), číslice, pomlčky a podtržítka.<br />Po uložení již nelze identifikátor měnit. + field_multiple: Více hodnot + setting_commit_cross_project_ref: Povolit reference a opravy úklů ze všech ostatních projektů + text_issue_conflict_resolution_add_notes: Přidat moje poznámky a zahodit ostatní změny + text_issue_conflict_resolution_overwrite: Přesto přijmout moje úpravy (předchozí poznámky budou zachovány, ale některé změny mohou být přepsány) + notice_issue_update_conflict: Během vašich úprav byl úkol aktualizován jiným uživatelem. + text_issue_conflict_resolution_cancel: Zahoď všechny moje změny a znovu zobraz %{link} + permission_manage_related_issues: Spravuj související úkoly + field_auth_source_ldap_filter: LDAP filtr + label_search_for_watchers: Hledej sledující pro přidání + notice_account_deleted: Váš účet byl trvale smazán. + setting_unsubscribe: Povolit uživatelům smazání jejich vlastního účtu + button_delete_my_account: Smazat můj účet text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project + Skutečně chcete pokračovat? + Váš účet bude nenávratně smazán. + error_session_expired: Vaše sezení vypršelo. Znovu se přihlaste, prosím. + text_session_expiration_settings: "Varování: změnou tohoto nastavení mohou vypršet aktuální sezení včetně toho vašeho." + setting_session_lifetime: Maximální čas sezení + setting_session_timeout: Vypršení sezení bez aktivity + label_session_expiration: Vypršení sezení + permission_close_project: Zavřít / Otevřít projekt + label_show_closed_projects: Zobrazit zavřené projekty + button_close: Zavřít + button_reopen: Znovu otevřít + project_status_active: aktivní + project_status_closed: zavřený + project_status_archived: archivovaný + text_project_closed: Tento projekt je uzevřený a je pouze pro čtení. + notice_user_successful_create: Uživatel %{id} vytvořen. + field_core_fields: Standardní pole + field_timeout: Vypršení (v sekundách) + setting_thumbnails_enabled: Zobrazit náhled přílohy + setting_thumbnails_size: Velikost náhledu (v pixelech) + label_status_transitions: Změna stavu + label_fields_permissions: Práva k polím + label_readonly: Pouze pro čtení + label_required: Vyžadováno + text_repository_identifier_info: Jou povoleny pouze malá písmena (a-z), číslice, pomlčky a podtržítka.<br />Po uložení již nelze identifikátor změnit. + field_board_parent: Rodičovské fórum + label_attribute_of_project: Projektové %{name} + label_attribute_of_author: Autorovo %{name} + label_attribute_of_assigned_to: "%{name} přiřazené(ho)" + label_attribute_of_fixed_version: Cílová verze %{name} + label_copy_subtasks: Kopírovat podúkoly + label_copied_to: zkopírováno do + label_copied_from: zkopírováno z + label_any_issues_in_project: jakékoli úkoly v projektu + label_any_issues_not_in_project: jakékoli úkoly mimo projektu + field_private_notes: Soukromé poznámky + permission_view_private_notes: Zobrazit soukromé poznámky + permission_set_notes_private: Nastavit poznámky jako soukromé + label_no_issues_in_project: žádné úkoly v projektu label_any: vše - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks + label_last_n_weeks: poslední %{count} týdny + setting_cross_project_subtasks: Povolit podúkoly napříč projekty label_cross_project_descendants: S podprojekty label_cross_project_tree: Se stromem projektu label_cross_project_hierarchy: S hierarchií projektu label_cross_project_system: Se všemi projekty - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past + button_hide: Skrýt + setting_non_working_week_days: Dny pracovního volna/klidu + label_in_the_next_days: v přístích + label_in_the_past_days: v minulých
--- a/config/locales/es.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/es.yml Fri Jun 14 09:01:12 2013 +0100 @@ -1037,84 +1037,84 @@ description_selected_columns: Columnas seleccionadas label_parent_revision: Padre label_child_revision: Hijo - setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues - button_edit_section: Edit this section - setting_repositories_encodings: Attachments and repositories encodings + setting_default_issue_start_date_to_creation_date: Utilizar la fecha actual como fecha de inicio para nuevas peticiones + button_edit_section: Editar esta sección + setting_repositories_encodings: Codificación de adjuntos y repositorios description_all_columns: Todas las columnas button_export: Exportar label_export_options: "%{export_format} opciones de exportación" error_attachment_too_big: Este fichero no se puede adjuntar porque excede el tamaño máximo de fichero (%{max_size}) - notice_failed_to_save_time_entries: "Error al guarda %{count} entradas de tiempo de las %{total} seleccionadas: %{ids}." + notice_failed_to_save_time_entries: "Error al guardar %{count} entradas de tiempo de las %{total} seleccionadas: %{ids}." label_x_issues: zero: 0 petición one: 1 petición other: "%{count} peticiones" - label_repository_new: New repository - field_repository_is_default: Main repository - label_copy_attachments: Copy attachments + label_repository_new: Nuevo repositorio + field_repository_is_default: Repositorio principal + label_copy_attachments: Copiar adjuntos label_item_position: "%{position}/%{count}" - label_completed_versions: Completed versions - text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account + label_completed_versions: Versiones completadas + text_project_identifier_info: Solo se permiten letras en minúscula (a-z), números, guiones y barras bajas.<br />Una vez guardado, el identificador no se puede cambiar. + field_multiple: Valores múltiples + setting_commit_cross_project_ref: Permitir referenciar y resolver peticiones de todos los demás proyectos + text_issue_conflict_resolution_add_notes: Añadir mis notas y descartar mis otros cambios + text_issue_conflict_resolution_overwrite: Aplicar mis campos de todas formas (las notas anteriores se mantendrán pero algunos cambios podrían ser sobreescritos) + notice_issue_update_conflict: La petición ha sido actualizada por otro usuario mientras la editaba. + text_issue_conflict_resolution_cancel: Descartar todos mis cambios y mostrar de nuevo %{link} + permission_manage_related_issues: Gestionar peticiones relacionadas + field_auth_source_ldap_filter: Filtro LDAP + label_search_for_watchers: Buscar seguidores para añadirlos + notice_account_deleted: Su cuenta ha sido eliminada + setting_unsubscribe: Permitir a los usuarios borrar sus propias cuentas + button_delete_my_account: Borrar mi cuenta text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project + ¿Está seguro de querer proceder? + Su cuenta quedará borrada permanentemente, sin la posibilidad de reactivarla. + error_session_expired: Su sesión ha expirado. Por favor, vuelva a identificarse. + text_session_expiration_settings: "Advertencia: el cambio de estas opciones podría hacer expirar las sesiones activas, incluyendo la suya." + setting_session_lifetime: Tiempo de vida máximo de las sesiones + setting_session_timeout: Tiempo máximo de inactividad de las sesiones + label_session_expiration: Expiración de sesiones + permission_close_project: Cerrar / reabrir el proyecto + label_show_closed_projects: Ver proyectos cerrados + button_close: Cerrar + button_reopen: Reabrir + project_status_active: activo + project_status_closed: cerrado + project_status_archived: archivado + text_project_closed: Este proyecto está cerrado y es de sólo lectura + notice_user_successful_create: Usuario %{id} creado. + field_core_fields: Campos básicos + field_timeout: Tiempo de inactividad (en segundos) + setting_thumbnails_enabled: Mostrar miniaturas de los adjuntos + setting_thumbnails_size: Tamaño de las miniaturas (en píxeles) + label_status_transitions: Transiciones de estado + label_fields_permissions: Permisos sobre los campos + label_readonly: Sólo lectura + label_required: Requerido + text_repository_identifier_info: Solo se permiten letras en minúscula (a-z), números, guiones y barras bajas.<br />Una vez guardado, el identificador no se puede cambiar. + field_board_parent: Foro padre + label_attribute_of_project: "%{name} del proyecto" + label_attribute_of_author: "%{name} del autor" + label_attribute_of_assigned_to: "%{name} de la persona asignada" + label_attribute_of_fixed_version: "%{name} de la versión indicada" + label_copy_subtasks: Copiar subtareas + label_copied_to: copiada a + label_copied_from: copiada desde + label_any_issues_in_project: cualquier petición del proyecto + label_any_issues_not_in_project: cualquier petición que no sea del proyecto + field_private_notes: Notas privadas + permission_view_private_notes: Ver notas privadas + permission_set_notes_private: Poner notas como privadas + label_no_issues_in_project: no hay peticiones en el proyecto label_any: todos - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks + label_last_n_weeks: en las últimas %{count} semanas + setting_cross_project_subtasks: Permitir subtareas cruzadas entre proyectos label_cross_project_descendants: Con proyectos hijo label_cross_project_tree: Con el árbol del proyecto label_cross_project_hierarchy: Con la jerarquía del proyecto label_cross_project_system: Con todos los proyectos - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past + button_hide: Ocultar + setting_non_working_week_days: Días no laborables + label_in_the_next_days: en los próximos + label_in_the_past_days: en los anteriores
--- a/config/locales/pt-BR.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/pt-BR.yml Fri Jun 14 09:01:12 2013 +0100 @@ -53,8 +53,8 @@ one: 'aproximadamente 1 hora' other: 'aproximadamente %{count} horas' x_hours: - one: "1 hour" - other: "%{count} hours" + one: "1 hora" + other: "%{count} horas" x_days: one: '1 dia' @@ -76,8 +76,8 @@ one: 'mais de 1 ano' other: 'mais de %{count} anos' almost_x_years: - one: "almost 1 year" - other: "almost %{count} years" + one: "quase 1 ano" + other: "quase %{count} anos" # numeros number: @@ -888,7 +888,7 @@ setting_issue_done_ratio_issue_status: Usar a situação da tarefa error_issue_done_ratios_not_updated: O pecentual de conclusão das tarefas não foi atualizado. error_workflow_copy_target: Por favor, selecione os tipos de tarefa e os papéis alvo - setting_issue_done_ratio_issue_field: Use the issue field + setting_issue_done_ratio_issue_field: Use o campo da tarefa label_copy_same_as_target: Mesmo alvo label_copy_target: Alvo notice_issue_done_ratios_updated: Percentual de conslusão atualizados. @@ -994,13 +994,13 @@ label_git_report_last_commit: Relatar última alteração para arquivos e diretórios text_scm_config: Você pode configurar seus comandos de versionamento em config/configurations.yml. Por favor reinicie a aplicação após alterá-lo. text_scm_command_not_available: Comando de versionamento não disponível. Por favor verifique as configurações no painel de administração. - notice_issue_successful_create: Issue %{id} created. - label_between: between - setting_issue_group_assignment: Allow issue assignment to groups + notice_issue_successful_create: Tarefa %{id} criada. + label_between: entre + setting_issue_group_assignment: Permitir atribuições de tarefas a grupos label_diff: diff - text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) + text_git_repository_note: "Repositório esta vazio e é local (ex: /gitrepo, c:\\gitrepo)" - description_query_sort_criteria_direction: Sort direction + description_query_sort_criteria_direction: Direção da ordenação description_project_scope: Escopo da pesquisa description_filter: Filtro description_user_mail_notification: Configuração de notificações por e-mail @@ -1014,91 +1014,91 @@ description_date_range_list: Escolha um período a partira da lista description_choose_project: Projetos description_date_to: Digite a data final - description_query_sort_criteria_attribute: Sort attribute + description_query_sort_criteria_attribute: Atributo de ordenação description_wiki_subpages_reassign: Escolha uma nova página pai description_selected_columns: Colunas selecionadas - label_parent_revision: Parent - label_child_revision: Child - error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. + label_parent_revision: Pais + label_child_revision: Filhos + error_scm_annotate_big_text_file: A entrada não pode ser anotada, pois excede o tamanho máximo do arquivo de texto. setting_default_issue_start_date_to_creation_date: Usar data corrente como data inicial para novas tarefas - button_edit_section: Edit this section - setting_repositories_encodings: Attachments and repositories encodings - description_all_columns: All Columns - button_export: Export - label_export_options: "%{export_format} export options" - error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) - notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + button_edit_section: Editar esta seção + setting_repositories_encodings: Encoding dos repositórios e anexos + description_all_columns: Todas as colunas + button_export: Exportar + label_export_options: "Opções de exportação %{export_format}" + error_attachment_too_big: Este arquivo não pode ser enviado porque excede o tamanho máximo permitido (%{max_size}) + notice_failed_to_save_time_entries: "Falha ao salvar %{count} de %{total} horas trabalhadas: %{ids}." label_x_issues: zero: 0 tarefa one: 1 tarefa other: "%{count} tarefas" - label_repository_new: New repository - field_repository_is_default: Main repository - label_copy_attachments: Copy attachments + label_repository_new: Novo repositório + field_repository_is_default: Repositório principal + label_copy_attachments: Copiar anexos label_item_position: "%{position}/%{count}" - label_completed_versions: Completed versions - text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account + label_completed_versions: Versões completadas + text_project_identifier_info: Somente letras minúsculas (az), números, traços e sublinhados são permitidos. <br /> Uma vez salvo, o identificador não pode ser alterado. + field_multiple: Multiplos valores + setting_commit_cross_project_ref: Permitir que tarefas de todos os outros projetos sejam refenciadas e resolvidas + text_issue_conflict_resolution_add_notes: Adicione minhas anotações e descartar minhas outras mudanças + text_issue_conflict_resolution_overwrite: Aplicar as minhas alterações de qualquer maneira (notas anteriores serão mantidos, mas algumas mudanças podem ser substituídos) + notice_issue_update_conflict: A tarefa foi atualizada por um outro usuário, enquanto você estava editando. + text_issue_conflict_resolution_cancel: Descartar todas as minhas mudanças e re-exibir %{link} + permission_manage_related_issues: Gerenciar tarefas relacionadas + field_auth_source_ldap_filter: Filtro LDAP + label_search_for_watchers: Procurar por outros observadores para adiconar + notice_account_deleted: Sua conta foi excluída permanentemente. + setting_unsubscribe: Permitir aos usuários excluir sua conta própria + button_delete_my_account: Excluir minha conta text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project + Tem certeza de que quer continuar? + Sua conta será excluída permanentemente, sem qualquer forma de reativá-lo. + error_session_expired: A sua sessão expirou. Por favor, faça login novamente. + text_session_expiration_settings: "Aviso: a alteração dessas configurações pode expirar as sessões atuais, incluindo a sua." + setting_session_lifetime: duração máxima da sessão + setting_session_timeout: tempo limite de inatividade da sessão + label_session_expiration: "Expiração da sessão" + permission_close_project: Fechar / reabrir o projeto + label_show_closed_projects: Visualização de projetos fechados + button_close: Fechar + button_reopen: Reabrir + project_status_active: ativo + project_status_closed: fechado + project_status_archived: arquivado + text_project_closed: Este projeto é fechado e somente leitura. + notice_user_successful_create: Usuário %{id} criado. + field_core_fields: campos padrão + field_timeout: Tempo de espera (em segundos) + setting_thumbnails_enabled: exibir miniaturas de anexos + setting_thumbnails_size: Tamanho das miniaturas (em pixels) + label_status_transitions: Estados das transições + label_fields_permissions: Permissões de campos + label_readonly: somente leitura + label_required: Obrigatório + text_repository_identifier_info: Somente letras minúsculas (az), números, traços e sublinhados são permitidos <br/> Uma vez salvo, o identificador não pode ser alterado. + field_board_parent: Fórum Pai + label_attribute_of_project: "Projeto %{name}" + label_attribute_of_author: "autor %{name}" + label_attribute_of_assigned_to: "atribuído %{name}" + label_attribute_of_fixed_version: "versão alvo %{name}" + label_copy_subtasks: Copiar sub-tarefas + label_copied_to: copiada + label_copied_from: copiado + label_any_issues_in_project: quaisquer problemas em projeto + label_any_issues_not_in_project: todas as questões que não estão em projeto + field_private_notes: notas privadas + permission_view_private_notes: Ver notas privadas + permission_set_notes_private: Defina notas como privada + label_no_issues_in_project: sem problemas em projeto label_any: todos - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks + label_last_n_weeks: "últimas %{count} semanas" + setting_cross_project_subtasks: Permitir cruzamento de sub-tarefas entre projetos label_cross_project_descendants: Com sub-projetos label_cross_project_tree: Com a árvore do projeto label_cross_project_hierarchy: Com a hierarquia do projeto label_cross_project_system: Com todos os projetos - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past + button_hide: Esconder + setting_non_working_week_days: dias não úteis + label_in_the_next_days: na próxima + label_in_the_past_days: no passado
--- a/config/locales/pt.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/pt.yml Fri Jun 14 09:01:12 2013 +0100 @@ -1,6 +1,7 @@ # Portuguese localization for Ruby on Rails # by Ricardo Otero <oterosantos@gmail.com> # by Alberto Ferreira <toraxic@gmail.com> +# by Rui Rebelo <rmrebelo@ua.pt> pt: support: array: @@ -51,8 +52,8 @@ one: "aproximadamente 1 hora" other: "aproximadamente %{count} horas" x_hours: - one: "1 hour" - other: "%{count} hours" + one: "1 hora" + other: "%{count} horas" x_days: one: "1 dia" other: "%{count} dias" @@ -69,8 +70,8 @@ one: "mais de 1 ano" other: "mais de %{count} anos" almost_x_years: - one: "almost 1 year" - other: "almost %{count} years" + one: "quase 1 ano" + other: "quase %{count} anos" number: format: @@ -440,17 +441,17 @@ label_closed_issues: fechado label_closed_issues_plural: fechados label_x_open_issues_abbr_on_total: - zero: 0 open / %{total} - one: 1 open / %{total} - other: "%{count} open / %{total}" + zero: 0 abertas / %{total} + one: 1 aberta / %{total} + other: "%{count} abertas / %{total}" label_x_open_issues_abbr: - zero: 0 open - one: 1 open - other: "%{count} open" + zero: 0 abertas + one: 1 aberta + other: "%{count} abertas" label_x_closed_issues_abbr: - zero: 0 closed - one: 1 closed - other: "%{count} closed" + zero: 0 fechadas + one: 1 fechada + other: "%{count} fechadas" label_total: Total label_permissions: Permissões label_current_status: Estado actual @@ -474,9 +475,9 @@ label_comment: Comentário label_comment_plural: Comentários label_x_comments: - zero: no comments - one: 1 comment - other: "%{count} comments" + zero: sem comentários + one: 1 comentário + other: "%{count} comentários" label_comment_add: Adicionar comentário label_comment_added: Comentário adicionado label_comment_delete: Apagar comentários @@ -943,146 +944,146 @@ setting_commit_logtime_enabled: Activar registo de tempo notice_gantt_chart_truncated: O gráfico foi truncado porque excede o número máximo de itens visível (%{máx.}) setting_gantt_items_limit: Número máximo de itens exibidos no gráfico Gantt - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: Priority updated - label_issues_visibility_own: Issues created by or assigned to the user - field_issues_visibility: Issues visibility - label_issues_visibility_all: All issues - permission_set_own_issues_private: Set own issues public or private - field_is_private: Private - permission_set_issues_private: Set issues public or private - label_issues_visibility_public: All non private issues - text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). - field_commit_logs_encoding: Encoding das mensagens de commit - field_scm_path_encoding: Path encoding - text_scm_path_encoding_note: "Default: UTF-8" - field_path_to_repository: Path to repository - field_root_directory: Root directory - field_cvs_module: Module + field_warn_on_leaving_unsaved: Avisar-me quando deixar uma página com texto por salvar + text_warn_on_leaving_unsaved: A página actual contém texto por salvar que será perdido caso saia desta página. + label_my_queries: As minhas consultas + text_journal_changed_no_detail: "%{label} actualizada" + label_news_comment_added: Comentário adicionado a uma notícia + button_expand_all: Expandir todos + button_collapse_all: Minimizar todos + label_additional_workflow_transitions_for_assignee: Transições adicionais permitidas quando a tarefa está atribuida ao utilizador + label_additional_workflow_transitions_for_author: Transições adicionais permitidas quando o utilizador é o autor da tarefa + label_bulk_edit_selected_time_entries: Edição em massa de registos de tempo + text_time_entries_destroy_confirmation: Têm a certeza que pretende apagar o(s) registo(s) de tempo selecionado(s)? + label_role_anonymous: Anónimo + label_role_non_member: Não membro + label_issue_note_added: Nota adicionada + label_issue_status_updated: Estado actualizado + label_issue_priority_updated: Prioridade adicionada + label_issues_visibility_own: Tarefas criadas ou atribuídas ao utilizador + field_issues_visibility: Visibilidade das tarefas + label_issues_visibility_all: Todas as tarefas + permission_set_own_issues_private: Configurar as suas tarefas como públicas ou privadas + field_is_private: Privado + permission_set_issues_private: Configurar tarefas como públicas ou privadas + label_issues_visibility_public: Todas as tarefas públicas + text_issues_destroy_descendants_confirmation: Irá apagar também %{count} subtarefa(s). + field_commit_logs_encoding: Codificação das mensagens de commit + field_scm_path_encoding: Codificação do caminho + text_scm_path_encoding_note: "Por omissão: UTF-8" + field_path_to_repository: Caminho para o repositório + field_root_directory: Raíz do directório + field_cvs_module: Módulo field_cvsroot: CVSROOT - text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) - text_scm_command: Command - text_scm_command_version: Version - label_git_report_last_commit: Report last commit for files and directories - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - notice_issue_successful_create: Issue %{id} created. - label_between: between - setting_issue_group_assignment: Allow issue assignment to groups - label_diff: diff - text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) - description_query_sort_criteria_direction: Sort direction - description_project_scope: Search scope - description_filter: Filter - description_user_mail_notification: Mail notification settings - description_date_from: Enter start date - description_message_content: Message content - description_available_columns: Available Columns - description_date_range_interval: Choose range by selecting start and end date - description_issue_category_reassign: Choose issue category - description_search: Searchfield - description_notes: Notes - description_date_range_list: Choose range from list - description_choose_project: Projects - description_date_to: Enter end date - description_query_sort_criteria_attribute: Sort attribute - description_wiki_subpages_reassign: Choose new parent page - description_selected_columns: Selected Columns - label_parent_revision: Parent - label_child_revision: Child - error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. - setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues - button_edit_section: Edit this section - setting_repositories_encodings: Attachments and repositories encodings - description_all_columns: All Columns - button_export: Export - label_export_options: "%{export_format} export options" - error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) - notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + text_mercurial_repository_note: "Repositório local (ex: /hgrepo, c:\\hgrepo)" + text_scm_command: Comando + text_scm_command_version: Versão + label_git_report_last_commit: Analisar último commit por ficheiros e pastas + text_scm_config: Pode configurar os comando SCM em config/configuration.yml. Por favor reinicie a aplicação depois de alterar o ficheiro. + text_scm_command_not_available: O comando SCM não está disponível. Por favor verifique as configurações no painel de administração. + notice_issue_successful_create: Tarefa %{id} criada. + label_between: entre + setting_issue_group_assignment: Permitir atribuir tarefas a grupos + label_diff: diferença + text_git_repository_note: O repositório é local (e.g. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Direcção da ordenação + description_project_scope: Âmbito da pesquisa + description_filter: Filtro + description_user_mail_notification: Configurações das notificações por email + description_date_from: Introduza data de início + description_message_content: Conteúdo da mensagem + description_available_columns: Colunas disponíveis + description_date_range_interval: Escolha o intervalo seleccionando a data de início e de fim + description_issue_category_reassign: Escolha a categoria da tarefa + description_search: Campo de pesquisa + description_notes: Notas + description_date_range_list: Escolha o intervalo da lista + description_choose_project: Projecto + description_date_to: Introduza data de fim + description_query_sort_criteria_attribute: Ordenar atributos + description_wiki_subpages_reassign: Escolha nova página pai + description_selected_columns: Colunas seleccionadas + label_parent_revision: Pai + label_child_revision: Filha + error_scm_annotate_big_text_file: Esta entrada não pode ser anotada, excede o tamanha máximo. + setting_default_issue_start_date_to_creation_date: Utilizar a data actual como data de início para novas tarefas + button_edit_section: Editar esta secção + setting_repositories_encodings: Codificação dos anexos e repositórios + description_all_columns: Todas as colunas + button_export: Exportar + label_export_options: "%{export_format} opções de exportação" + error_attachment_too_big: Este ficheiro não pode ser carregado pois excede o tamanho máximo permitido por ficheiro (%{max_size}) + notice_failed_to_save_time_entries: "Falha ao guardar %{count} registo(s) de tempo dos %{total} seleccionados: %{ids}." label_x_issues: zero: 0 tarefa one: 1 tarefa other: "%{count} tarefas" - label_repository_new: New repository - field_repository_is_default: Main repository - label_copy_attachments: Copy attachments + label_repository_new: Novo repositório + field_repository_is_default: Repositório principal + label_copy_attachments: Copiar anexos label_item_position: "%{position}/%{count}" - label_completed_versions: Completed versions - text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account + label_completed_versions: Versões completas + text_project_identifier_info: Apenas letras minúsculas (a-z), números, traços e sublinhados são permitidos.<br />Depois de guardar não é possível alterar. + field_multiple: Múltiplos valores + setting_commit_cross_project_ref: Permitir que tarefas dos restantes projectos sejam referenciadas e resolvidas + text_issue_conflict_resolution_add_notes: Adicionar as minhas notas e descartar as minhas restantes alterações + text_issue_conflict_resolution_overwrite: Aplicar as minhas alterações (notas antigas serão mantidas mas algumas alterações podem se perder) + notice_issue_update_conflict: Esta tarefa foi actualizada por outro utilizador enquanto estava a edita-la. + text_issue_conflict_resolution_cancel: Descartar todas as minhas alterações e actualizar %{link} + permission_manage_related_issues: Gerir tarefas relacionadas + field_auth_source_ldap_filter: Filtro LDAP + label_search_for_watchers: Pesquisar por observadores para adicionar + notice_account_deleted: A sua conta foi apagada permanentemente. + setting_unsubscribe: Permitir aos utilizadores apagarem a sua própria conta + button_delete_my_account: Apagar a minha conta text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project + Têm a certeza que pretende avançar? + A sua conta vai ser permanentemente apagada, não será possível recupera-la. + error_session_expired: A sua sessão expirou. Por-favor autentique-se novamente. + text_session_expiration_settings: "Atenção: alterar estas configurações pode fazer expirar as sessões em curso, incluíndo a sua." + setting_session_lifetime: Duração máxima da sessão + setting_session_timeout: Tempo limite de inactividade da sessão + label_session_expiration: Expiração da sessão + permission_close_project: Fechar / re-abrir o projecto + label_show_closed_projects: Ver os projectos fechados + button_close: Fechar + button_reopen: Re-abrir + project_status_active: activo + project_status_closed: fechado + project_status_archived: arquivado + text_project_closed: Este projecto está fechado e é apenas de leitura. + notice_user_successful_create: Utilizador %{id} criado. + field_core_fields: Campos padrão + field_timeout: Tempo limite (em segundos) + setting_thumbnails_enabled: Apresentar miniaturas dos anexos + setting_thumbnails_size: Tamanho das miniaturas (em pixeis) + label_status_transitions: Estado das transições + label_fields_permissions: Permissões do campo + label_readonly: Apenas de leitura + label_required: Obrigatório + text_repository_identifier_info: Apenas letras minúsculas (a-z), números, traços e sublinhados são permitidos.<br />Depois de guardar não é possível alterar. + field_board_parent: Fórum pai + label_attribute_of_project: "%{name} do Projecto" + label_attribute_of_author: "%{name} do Autor" + label_attribute_of_assigned_to: "%{name} do atribuído" + label_attribute_of_fixed_version: "%{name} da Versão" + label_copy_subtasks: Copiar sub-tarefas + label_copied_to: copiado para + label_copied_from: copiado de + label_any_issues_in_project: tarefas do projecto + label_any_issues_not_in_project: tarefas sem projecto + field_private_notes: Notas privadas + permission_view_private_notes: Ver notas privadas + permission_set_notes_private: Configurar notas como privadas + label_no_issues_in_project: sem tarefas no projecto label_any: todos - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks + label_last_n_weeks: últimas %{count} semanas + setting_cross_project_subtasks: Permitir sub-tarefas entre projectos label_cross_project_descendants: Com os sub-projectos label_cross_project_tree: Com árvore do projecto label_cross_project_hierarchy: Com hierarquia do projecto label_cross_project_system: Com todos os projectos - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past + button_hide: Esconder + setting_non_working_week_days: Dias não úteis + label_in_the_next_days: no futuro + label_in_the_past_days: no passado
--- a/config/locales/ru.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/ru.yml Fri Jun 14 09:01:12 2013 +0100 @@ -1147,7 +1147,7 @@ button_delete_my_account: "Удалить мою учетную запись" text_account_destroy_confirmation: "Ваша учетная запись будет полностью удалена без возможности восстановления.\nВы уверены, что хотите продолжить?" error_session_expired: Срок вашей сессии истек. Пожалуйста войдите еще раз - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." + text_session_expiration_settings: "Внимание! Изменение этих настроек может привести к завершению текущих сессий, включая вашу." setting_session_lifetime: Максимальная продолжительность сессии setting_session_timeout: Таймут сессии label_session_expiration: Срок истечения сессии
--- a/config/locales/sr-YU.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/sr-YU.yml Fri Jun 14 09:01:12 2013 +0100 @@ -923,47 +923,47 @@ project_module_calendar: Kalendar button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" field_text: Text field - label_user_mail_option_only_owner: Only for things I am the owner of - setting_default_notification_option: Default notification option - label_user_mail_option_only_my_events: Only for things I watch or I'm involved in - label_user_mail_option_only_assigned: Only for things I am assigned to - label_user_mail_option_none: No events + label_user_mail_option_only_owner: Samo za stvari koje posedujem + setting_default_notification_option: Podrazumevana opcija za notifikaciju + label_user_mail_option_only_my_events: Za dogadjaje koje pratim ili sam u njih uključen + label_user_mail_option_only_assigned: Za dogadjaje koji su mi dodeljeni lično + label_user_mail_option_none: Bez obaveštenja field_member_of_group: Assignee's group field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" - label_user_search: "Search for user:" - field_visible: Visible - setting_emails_header: Emails header + notice_not_authorized_archived_project: Projekat kome pokušavate da pristupite je arhiviran + label_principal_search: "Traži korisnike ili grupe:" + label_user_search: "Traži korisnike:" + field_visible: Vidljivo + setting_emails_header: Email zaglavlje setting_commit_logtime_activity_id: Activity for logged time text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Enable time logging + setting_commit_logtime_enabled: Omogući praćenje vremena notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maximum number of items displayed on the gantt chart - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. + setting_gantt_items_limit: Maksimalan broj stavki na gant grafiku + field_warn_on_leaving_unsaved: Upozori me ako napuštam stranu sa tekstom koji nije snimljen + text_warn_on_leaving_unsaved: Strana sadrži tekst koji nije snimljen i biće izgubljen ako je napustite. label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all + text_journal_changed_no_detail: "%{label} ažuriran" + label_news_comment_added: Komentar dodat u novosti + button_expand_all: Proširi sve + button_collapse_all: Zatvori sve label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: Priority updated - label_issues_visibility_own: Issues created by or assigned to the user - field_issues_visibility: Issues visibility - label_issues_visibility_all: All issues - permission_set_own_issues_private: Set own issues public or private - field_is_private: Private - permission_set_issues_private: Set issues public or private - label_issues_visibility_public: All non private issues - text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). + text_time_entries_destroy_confirmation: Da li ste sigurni da želite da obrišete selektovane stavke ? + label_role_anonymous: Anonimus + label_role_non_member: Nije član + label_issue_note_added: Nota dodana + label_issue_status_updated: Status ažuriran + label_issue_priority_updated: Prioritet ažuriran + label_issues_visibility_own: Problem kreiran od strane ili je dodeljen korisniku + field_issues_visibility: Vidljivost problema + label_issues_visibility_all: Svi problemi + permission_set_own_issues_private: Podesi sopstveni problem kao privatan ili javan + field_is_private: Privatno + permission_set_issues_private: Podesi problem kao privatan ili javan + label_issues_visibility_public: Svi javni problemi + text_issues_destroy_descendants_confirmation: Ova operacija će takođe obrisati %{count} podzadataka. field_commit_logs_encoding: Kodiranje izvršnih poruka field_scm_path_encoding: Path encoding text_scm_path_encoding_note: "Default: UTF-8"
--- a/config/locales/sv.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/sv.yml Fri Jun 14 09:01:12 2013 +0100 @@ -79,8 +79,8 @@ one: "ungefär en timme" other: "ungefär %{count} timmar" x_hours: - one: "1 hour" - other: "%{count} hours" + one: "1 timme" + other: "%{count} timmar" x_days: one: "en dag" other: "%{count} dagar" @@ -219,6 +219,7 @@ notice_issue_successful_create: Ärende %{id} skapades. notice_issue_update_conflict: Detta ärende har uppdaterats av en annan användare samtidigt som du redigerade det. notice_account_deleted: Ditt konto har avslutats permanent. + notice_user_successful_create: "Användare %{id} skapad." error_can_t_load_default_data: "Standardkonfiguration gick inte att läsa in: %{value}" error_scm_not_found: "Inlägg och/eller revision finns inte i detta versionsarkiv." @@ -239,6 +240,7 @@ error_unable_delete_issue_status: 'Ärendestatus kunde inte tas bort' error_unable_to_connect: "Kan inte ansluta (%{value})" error_attachment_too_big: Denna fil kan inte laddas upp eftersom den överstiger maximalt tillåten filstorlek (%{max_size}) + error_session_expired: "Din session har gått ut. Vänligen logga in på nytt." warning_attachments_not_saved: "%{count} fil(er) kunde inte sparas." mail_subject_lost_password: "Ditt %{value} lösenord" @@ -368,6 +370,10 @@ field_repository_is_default: Huvudarkiv field_multiple: Flera värden field_auth_source_ldap_filter: LDAP-filter + field_core_fields: Standardfält + field_timeout: "Timeout (i sekunder)" + field_board_parent: Förälderforum + field_private_notes: Privata anteckningar setting_app_title: Applikationsrubrik setting_app_subtitle: Applikationsunderrubrik @@ -392,6 +398,7 @@ setting_autologin: Automatisk inloggning setting_date_format: Datumformat setting_time_format: Tidsformat + setting_cross_project_subtasks: Tillåt underaktiviteter mellan projekt setting_cross_project_issue_relations: Tillåt ärenderelationer mellan projekt setting_issue_list_default_columns: Standardkolumner i ärendelistan setting_repositories_encodings: Encoding för bilagor och versionsarkiv @@ -430,10 +437,16 @@ setting_default_issue_start_date_to_creation_date: Använd dagens datum som startdatum för nya ärenden setting_commit_cross_project_ref: Tillåt ärende i alla de andra projekten att bli refererade och fixade setting_unsubscribe: Tillåt användare att avsluta prenumereration + setting_session_lifetime: Maximal sessionslivslängd + setting_session_timeout: Tidsgräns för sessionsinaktivitet + setting_thumbnails_enabled: Visa miniatyrbilder av bilagor + setting_thumbnails_size: Storlek på miniatyrbilder (i pixlar) + setting_non_working_week_days: Lediga dagar permission_add_project: Skapa projekt permission_add_subprojects: Skapa underprojekt permission_edit_project: Ändra projekt + permission_close_project: Stänga / återöppna projektet permission_select_project_modules: Välja projektmoduler permission_manage_members: Hantera medlemmar permission_manage_project_activities: Hantera projektaktiviteter @@ -448,6 +461,8 @@ permission_add_issue_notes: Lägga till ärendenotering permission_edit_issue_notes: Ändra ärendenoteringar permission_edit_own_issue_notes: Ändra egna ärendenoteringar + permission_view_private_notes: Visa privata anteckningar + permission_set_notes_private: Ställa in anteckningar som privata permission_move_issues: Flytta ärenden permission_delete_issues: Ta bort ärenden permission_manage_public_queries: Hantera publika frågor @@ -682,6 +697,8 @@ label_not_equals: är inte label_in_less_than: om mindre än label_in_more_than: om mer än + label_in_the_next_days: under kommande + label_in_the_past_days: under föregående label_greater_or_equal: '>=' label_less_or_equal: '<=' label_between: mellan @@ -691,6 +708,7 @@ label_yesterday: igår label_this_week: denna vecka label_last_week: senaste veckan + label_last_n_weeks: "senaste %{count} veckorna" label_last_n_days: "senaste %{count} dagarna" label_this_month: denna månad label_last_month: senaste månaden @@ -701,6 +719,9 @@ label_ago: dagar sedan label_contains: innehåller label_not_contains: innehåller inte + label_any_issues_in_project: några ärenden i projektet + label_any_issues_not_in_project: några ärenden utanför projektet + label_no_issues_in_project: inga ärenden i projektet label_day_plural: dagar label_repository: Versionsarkiv label_repository_new: Nytt versionsarkiv @@ -776,6 +797,8 @@ label_blocked_by: blockerad av label_precedes: kommer före label_follows: följer + label_copied_to: Kopierad till + label_copied_from: Kopierad från label_end_to_start: slut till start label_end_to_end: slut till slut label_start_to_start: start till start @@ -889,9 +912,20 @@ label_child_revision: Barn label_export_options: "%{export_format} exportalternativ" label_copy_attachments: Kopiera bilagor + label_copy_subtasks: Kopiera underaktiviteter label_item_position: "%{position}/%{count}" label_completed_versions: Klara versioner label_search_for_watchers: Sök efter bevakare att lägga till + label_session_expiration: Sessionsutgång + label_show_closed_projects: Visa stängda projekt + label_status_transitions: Statusövergångar + label_fields_permissions: Fältbehörigheter + label_readonly: Skrivskyddad + label_required: Nödvändig + label_attribute_of_project: Projektets %{name} + label_attribute_of_author: Författarens %{name} + label_attribute_of_assigned_to: Tilldelads %{name} + label_attribute_of_fixed_version: Målversionens %{name} button_login: Logga in button_submit: Skicka @@ -939,14 +973,21 @@ button_quote: Citera button_duplicate: Duplicera button_show: Visa + button_hide: Göm button_edit_section: Redigera denna sektion button_export: Exportera button_delete_my_account: Ta bort mitt konto + button_close: Stäng + button_reopen: Återöppna status_active: aktiv status_registered: registrerad status_locked: låst + project_status_active: aktiv + project_status_closed: stängd + project_status_archived: arkiverad + version_status_open: öppen version_status_locked: låst version_status_closed: stängd @@ -1026,6 +1067,8 @@ text_issue_conflict_resolution_add_notes: Lägg till mina anteckningar och kasta mina andra ändringar text_issue_conflict_resolution_cancel: Kasta alla mina ändringar och visa igen %{link} text_account_destroy_confirmation: "Är du säker på att du vill fortsätta?\nDitt konto kommer tas bort permanent, utan möjlighet att återaktivera det." + text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att gå ut." + text_project_closed: Detta projekt är stängt och skrivskyddat. default_role_manager: Projektledare default_role_developer: Utvecklare @@ -1071,51 +1114,9 @@ description_date_range_interval: Ange intervall genom att välja start- och slutdatum description_date_from: Ange startdatum description_date_to: Ange slutdatum - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required text_repository_identifier_info: Ändast gemener (a-z), siffror, streck och understreck är tillåtna.<br />När identifieraren sparats kan den inte ändras. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project label_any: alla - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks label_cross_project_descendants: Med underprojekt label_cross_project_tree: Med projektträd label_cross_project_hierarchy: Med projekthierarki label_cross_project_system: Med alla projekt - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past
--- a/config/locales/vi.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/config/locales/vi.yml Fri Jun 14 09:01:12 2013 +0100 @@ -2,6 +2,7 @@ # by # Do Hai Bac (dohaibac@gmail.com) # Dao Thanh Ngoc (ngocdaothanh@gmail.com, http://github.com/ngocdaothanh/rails-i18n/tree/master) +# Nguyen Minh Thien (thiencdcn@gmail.com, http://www.eDesignLab.org) vi: number: @@ -80,8 +81,8 @@ one: "khoảng 1 giờ" other: "khoảng %{count} giờ" x_hours: - one: "1 hour" - other: "%{count} hours" + one: "1 giờ" + other: "%{count} giờ" x_days: one: "1 ngày" other: "%{count} ngày" @@ -98,8 +99,8 @@ one: "hơn 1 năm" other: "hơn %{count} năm" almost_x_years: - one: "almost 1 year" - other: "almost %{count} years" + one: "gần 1 năm" + other: "gần %{count} năm" prompts: year: "Năm" month: "Tháng" @@ -142,7 +143,7 @@ greater_than_start_date: "phải đi sau ngày bắt đầu" not_same_project: "không thuộc cùng dự án" circular_dependency: "quan hệ có thể gây ra lặp vô tận" - cant_link_an_issue_with_a_descendant: "An issue can not be linked to one of its subtasks" + cant_link_an_issue_with_a_descendant: "Một vấn đề không thể liên kết tới một trong số những tác vụ con của nó" direction: ltr date: @@ -214,16 +215,16 @@ notice_email_sent: "Email đã được gửi tới %{value}" notice_email_error: "Lỗi xảy ra khi gửi email (%{value})" notice_feeds_access_key_reseted: Mã số chứng thực RSS đã được tạo lại. - notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}." - notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." + notice_failed_to_save_issues: "Thất bại khi lưu %{count} vấn đề trong %{total} lựa chọn: %{ids}." + notice_no_issue_selected: "Không có vấn đề được chọn! Vui lòng kiểm tra các vấn đề bạn cần chỉnh sửa." notice_account_pending: "Thông tin tài khoản đã được tạo ra và đang chờ chứng thực từ ban quản trị." notice_default_data_loaded: Đã nạp cấu hình mặc định. notice_unable_delete_version: Không thể xóa phiên bản. error_can_t_load_default_data: "Không thể nạp cấu hình mặc định: %{value}" - error_scm_not_found: "The entry or revision was not found in the repository." + error_scm_not_found: "Không tìm thấy dữ liệu trong kho chứa." error_scm_command_failed: "Lỗi xảy ra khi truy cập vào kho lưu trữ: %{value}" - error_scm_annotate: "The entry does not exist or can not be annotated." + error_scm_annotate: "Đầu vào không tồn tại hoặc không thể chú thích." error_issue_not_found_in_project: 'Vấn đề không tồn tại hoặc không thuộc dự án' mail_subject_lost_password: "%{value}: mật mã của bạn" @@ -292,17 +293,17 @@ field_version: Phiên bản field_type: Kiểu field_host: Host - field_port: Port + field_port: Cổng field_account: Tài khoản field_base_dn: Base DN - field_attr_login: Login attribute - field_attr_firstname: Firstname attribute - field_attr_lastname: Lastname attribute - field_attr_mail: Email attribute - field_onthefly: On-the-fly user creation + field_attr_login: Thuộc tính đăng nhập + field_attr_firstname: Thuộc tính tên đệm và Tên + field_attr_lastname: Thuộc tính Họ + field_attr_mail: Thuộc tính Email + field_onthefly: Tạo người dùng tức thì field_start_date: Bắt đầu field_done_ratio: Tiến độ - field_auth_source: Authentication mode + field_auth_source: Chế độ xác thực field_hide_mail: Không làm lộ email của bạn field_comments: Bình luận field_url: URL @@ -332,31 +333,31 @@ setting_login_required: Cần đăng nhập setting_self_registration: Tự chứng thực setting_attachment_max_size: Cỡ tối đa của tập tin đính kèm - setting_issues_export_limit: Issues export limit - setting_mail_from: Emission email address + setting_issues_export_limit: Giới hạn Export vấn đề + setting_mail_from: Địa chỉ email gửi thông báo setting_bcc_recipients: Tạo bản CC bí mật (bcc) setting_host_name: Tên miền và đường dẫn setting_text_formatting: Định dạng bài viết - setting_wiki_compression: Wiki history compression + setting_wiki_compression: Nén lịch sử Wiki setting_feeds_limit: Giới hạn nội dung của feed setting_default_projects_public: Dự án mặc định là công cộng - setting_autofetch_changesets: Autofetch commits - setting_sys_api_enabled: Enable WS for repository management + setting_autofetch_changesets: Tự động tìm nạp commits + setting_sys_api_enabled: Cho phép WS quản lý kho chứa setting_commit_ref_keywords: Từ khóa tham khảo setting_commit_fix_keywords: Từ khóa chỉ vấn đề đã giải quyết setting_autologin: Tự động đăng nhập setting_date_format: Định dạng ngày setting_time_format: Định dạng giờ setting_cross_project_issue_relations: Cho phép quan hệ chéo giữa các dự án - setting_issue_list_default_columns: Default columns displayed on the issue list + setting_issue_list_default_columns: Các cột mặc định hiển thị trong danh sách vấn đề setting_emails_footer: Chữ ký cuối thư setting_protocol: Giao thức - setting_per_page_options: Objects per page options + setting_per_page_options: Tùy chọn đối tượng mỗi trang setting_user_format: Định dạng hiển thị người dùng - 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_activity_days_default: Ngày hiển thị hoạt động của dự án + setting_display_subprojects_issues: Hiển thị mặc định vấn đề của dự án con ở dự án chính + setting_enabled_scm: Cho phép SCM + setting_mail_handler_api_enabled: Cho phép WS cho các email tới setting_mail_handler_api_key: Mã số API setting_sequential_project_identifiers: Tự sinh chuỗi ID dự án @@ -376,9 +377,9 @@ label_project_new: Dự án mới label_project_plural: Dự án label_x_projects: - zero: no projects - one: 1 project - other: "%{count} projects" + zero: không có dự án + one: một dự án + other: "%{count} dự án" label_project_all: Mọi dự án label_project_latest: Dự án mới nhất label_issue: Vấn đề @@ -402,18 +403,18 @@ label_tracker: Dòng vấn đề label_tracker_plural: Dòng vấn đề label_tracker_new: Tạo dòng vấn đề mới - label_workflow: Workflow - label_issue_status: Issue status - label_issue_status_plural: Issue statuses - label_issue_status_new: New status + label_workflow: Quy trình làm việc + label_issue_status: Trạng thái vấn đề + label_issue_status_plural: Trạng thái vấn đề + label_issue_status_new: Thêm trạng thái label_issue_category: Chủ đề label_issue_category_plural: Chủ đề label_issue_category_new: Chủ đề mới - label_custom_field: Custom field - label_custom_field_plural: Custom fields - label_custom_field_new: New custom field - label_enumerations: Enumerations - label_enumeration_new: New value + label_custom_field: Trường tùy biến + label_custom_field_plural: Trường tùy biến + label_custom_field_new: Thêm Trường tùy biến + label_enumerations: Liệt kê + label_enumeration_new: Thêm giá trị label_information: Thông tin label_information_plural: Thông tin label_please_login: Vui lòng đăng nhập @@ -435,23 +436,23 @@ label_overall_activity: Tất cả hoạt động label_new: Mới label_logged_as: Tài khoản » - label_environment: Environment - label_authentication: Authentication - label_auth_source: Authentication mode - label_auth_source_new: New authentication mode - label_auth_source_plural: Authentication modes + label_environment: Môi trường + label_authentication: Xác thực + label_auth_source: Chế độ xác thực + label_auth_source_new: Chế độ xác thực mới + label_auth_source_plural: Chế độ xác thực label_subproject_plural: Dự án con label_and_its_subprojects: "%{value} và dự án con" - label_min_max_length: Min - Max length - label_list: List + label_min_max_length: Độ dài nhỏ nhất - lớn nhất + label_list: Danh sách label_date: Ngày - label_integer: Integer - label_float: Float + label_integer: Số nguyên + label_float: Số thực label_boolean: Boolean - label_string: Text - label_text: Long text - label_attribute: Attribute - label_attribute_plural: Attributes + label_string: Văn bản + label_text: Văn bản dài + label_attribute: Thuộc tính + label_attribute_plural: Các thuộc tính label_download: "%{count} lần tải" label_download_plural: "%{count} lần tải" label_no_data: Chưa có thông tin gì @@ -477,24 +478,24 @@ label_version_plural: Phiên bản label_confirmation: Khẳng định label_export_to: 'Định dạng khác của trang này:' - label_read: Read... + label_read: Đọc... label_public_projects: Các dự án công cộng label_open_issues: mở label_open_issues_plural: mở label_closed_issues: đóng label_closed_issues_plural: đóng label_x_open_issues_abbr_on_total: - zero: 0 open / %{total} - one: 1 open / %{total} - other: "%{count} open / %{total}" + zero: "0 mở / %{total}" + one: "1 mở / %{total}" + other: "%{count} mở / %{total}" label_x_open_issues_abbr: - zero: 0 open - one: 1 open - other: "%{count} open" + zero: 0 mở + one: 1 mở + other: "%{count} mở" label_x_closed_issues_abbr: - zero: 0 closed - one: 1 closed - other: "%{count} closed" + zero: 0 đóng + one: 1 đóng + other: "%{count} đóng" label_total: Tổng cộng label_permissions: Quyền label_current_status: Trạng thái hiện tại @@ -504,7 +505,7 @@ label_nobody: Chẳng ai label_next: Sau label_previous: Trước - label_used_by: Used by + label_used_by: Được dùng bởi label_details: Chi tiết label_add_note: Thêm ghi chú label_per_page: Mỗi trang @@ -518,9 +519,9 @@ label_comment: Bình luận label_comment_plural: Bình luận label_x_comments: - zero: no comments - one: 1 comment - other: "%{count} comments" + zero: không có bình luận + one: 1 bình luận + other: "%{count} bình luận" label_comment_add: Thêm bình luận label_comment_added: Đã thêm bình luận label_comment_delete: Xóa bình luận @@ -557,7 +558,7 @@ label_modification_plural: "%{count} thay đổi" label_revision: Bản điều chỉnh label_revision_plural: Bản điều chỉnh - label_associated_revisions: Associated revisions + label_associated_revisions: Các bản điều chỉnh được ghép label_added: thêm label_modified: đổi label_copied: chép @@ -579,7 +580,7 @@ label_result_plural: Kết quả label_all_words: Mọi từ label_wiki: Wiki - label_wiki_edit: Wiki edit + label_wiki_edit: Sửa Wiki label_wiki_edit_plural: Thay đổi wiki label_wiki_page: Trang wiki label_wiki_page_plural: Trang wiki @@ -587,7 +588,7 @@ label_index_by_date: Danh sách theo ngày label_current_version: Bản hiện tại label_preview: Xem trước - label_feed_plural: Feeds + label_feed_plural: Nguồn cấp tin label_changes_details: Chi tiết của mọi thay đổi label_issue_tracking: Vấn đề label_spent_time: Thời gian @@ -596,13 +597,13 @@ label_time_tracking: Theo dõi thời gian label_change_plural: Thay đổi label_statistics: Thống kê - label_commits_per_month: Commits per month - label_commits_per_author: Commits per author + label_commits_per_month: Commits mỗi tháng + label_commits_per_author: Commits mỗi tác giả label_view_diff: So sánh label_diff_inline: inline - label_diff_side_by_side: side by side + label_diff_side_by_side: bên cạnh nhau label_options: Tùy chọn - label_copy_workflow_from: Copy workflow from + label_copy_workflow_from: Sao chép quy trình từ label_permissions_report: Thống kê các quyền label_watched_issues: Chủ đề đang theo dõi label_related_issues: Liên quan @@ -642,7 +643,7 @@ label_date_to: Đến label_language_based: Theo ngôn ngữ người dùng label_sort_by: "Sắp xếp theo %{value}" - label_send_test_email: Send a test email + label_send_test_email: Gửi một email kiểm tra label_feeds_access_key_created_on: "Mã chứng thực RSS được tạo ra cách đây %{value}" label_module_plural: Mô-đun label_added_time_by: "thêm bởi %{author} cách đây %{age}" @@ -659,11 +660,11 @@ label_user_mail_option_all: "Mọi sự kiện trên mọi dự án của bạn" label_user_mail_option_selected: "Mọi sự kiện trên các dự án được chọn..." label_user_mail_no_self_notified: "Đừng gửi email về các thay đổi do chính bạn thực hiện" - label_registration_activation_by_email: account activation by email - label_registration_manual_activation: manual account activation - label_registration_automatic_activation: automatic account activation + label_registration_activation_by_email: kích hoạt tài khoản qua email + label_registration_manual_activation: kích hoạt tài khoản thủ công + label_registration_automatic_activation: kích hoạt tài khoản tự động label_display_per_page: "mỗi trang: %{value}" - label_age: Age + label_age: Thời gian label_change_properties: Thay đổi thuộc tính label_general: Tổng quan label_more: Chi tiết @@ -727,43 +728,43 @@ text_select_mail_notifications: Chọn hành động đối với mỗi email thông báo sẽ gửi. text_regexp_info: eg. ^[A-Z0-9]+$ text_min_max_length_info: 0 để chỉ không hạn chế - text_project_destroy_confirmation: Are you sure you want to delete this project and related data ? - text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted." - text_workflow_edit: Select a role and a tracker to edit the workflow + text_project_destroy_confirmation: Bạn có chắc chắn muốn xóa dự án này và các dữ liệu liên quan ? + text_subprojects_destroy_warning: "Dự án con của : %{value} cũng sẽ bị xóa." + text_workflow_edit: Chọn một vai trò và một vấn đề để sửa quy trình text_are_you_sure: Bạn chắc chứ? text_tip_issue_begin_day: ngày bắt đầu text_tip_issue_end_day: ngày kết thúc text_tip_issue_begin_end_day: bắt đầu và kết thúc cùng ngày text_caracters_maximum: "Tối đa %{count} ký tự." text_caracters_minimum: "Phải gồm ít nhất %{count} ký tự." - text_length_between: "Length between %{min} and %{max} characters." - text_tracker_no_workflow: No workflow defined for this tracker + text_length_between: "Chiều dài giữa %{min} và %{max} ký tự." + text_tracker_no_workflow: Không có quy trình được định nghĩa cho theo dõi này text_unallowed_characters: Ký tự không hợp lệ - text_comma_separated: Multiple values allowed (comma separated). - text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages - text_issue_added: "Issue %{id} has been reported by %{author}." - text_issue_updated: "Issue %{id} has been updated by %{author}." - text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content ? - text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do ?" - text_issue_category_destroy_assignments: Remove category assignments - text_issue_category_reassign_to: Reassign issues to this category + text_comma_separated: Nhiều giá trị được phép (cách nhau bởi dấu phẩy). + text_issues_ref_in_commit_messages: Vấn đề tham khảo và cố định trong ghi chú commit + text_issue_added: "Vấn đề %{id} đã được báo cáo bởi %{author}." + text_issue_updated: "Vấn đề %{id} đã được cập nhật bởi %{author}." + text_wiki_destroy_confirmation: Bạn có chắc chắn muốn xóa trang wiki này và tất cả nội dung của nó ? + text_issue_category_destroy_question: "Một số vấn đề (%{count}) được gán cho danh mục này. Bạn muốn làm gì ?" + text_issue_category_destroy_assignments: Gỡ bỏ danh mục được phân công + text_issue_category_reassign_to: Gán lại vấn đề cho danh mục này text_user_mail_option: "Với các dự án không được chọn, bạn chỉ có thể nhận được thông báo về các vấn đề bạn đăng ký theo dõi hoặc có liên quan đến bạn (chẳng hạn, vấn đề được gán cho bạn)." - 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_load_default_configuration: Load the default configuration - text_status_changed_by_changeset: "Applied in changeset %{value}." - text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s) ?' + text_no_configuration_data: "Quyền, theo dõi, tình trạng vấn đề và quy trình chưa được cấu hình.\nBắt buộc phải nạp cấu hình mặc định. Bạn sẽ thay đổi nó được sau khi đã nạp." + text_load_default_configuration: Nạp lại cấu hình mặc định + text_status_changed_by_changeset: "Áp dụng trong changeset : %{value}." + text_issues_destroy_confirmation: 'Bạn có chắc chắn muốn xóa các vấn đề đã chọn ?' text_select_project_modules: 'Chọn các mô-đun cho dự án:' - text_default_administrator_account_changed: Default administrator account changed - text_file_repository_writable: File repository writable - text_rmagick_available: RMagick available (optional) - text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do ?" - 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: "%{value} wrote:" - text_enumeration_destroy_question: "%{count} 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/configuration.yml and restart the application to enable them." + text_default_administrator_account_changed: Thay đổi tài khoản quản trị mặc định + text_file_repository_writable: Cho phép ghi thư mục đính kèm + text_rmagick_available: Trạng thái RMagick + text_destroy_time_entries_question: "Thời gian %{hours} giờ đã báo cáo trong vấn đề bạn định xóa. Bạn muốn làm gì tiếp ?" + text_destroy_time_entries: Xóa thời gian báo cáo + text_assign_time_entries_to_project: Gán thời gian báo cáo cho dự án + text_reassign_time_entries: 'Gán lại thời gian báo cáo cho Vấn đề này:' + text_user_wrote: "%{value} đã viết:" + text_enumeration_destroy_question: "%{count} đối tượng được gán giá trị này." + text_enumeration_category_reassign_to: 'Gán lại giá trị này:' + text_email_delivery_not_configured: "Cấu hình gửi Email chưa được đặt, và chức năng thông báo bị loại bỏ.\nCấu hình máy chủ SMTP của bạn ở file config/configuration.yml và khởi động lại để kích hoạt chúng." default_role_manager: Điều hành default_role_developer: Phát triển @@ -772,7 +773,7 @@ default_tracker_feature: Tính năng default_tracker_support: Hỗ trợ default_issue_status_new: Mới - default_issue_status_in_progress: In Progress + default_issue_status_in_progress: Đang tiến hành default_issue_status_resolved: Quyết tâm default_issue_status_feedback: Phản hồi default_issue_status_closed: Đóng @@ -843,292 +844,295 @@ permission_delete_own_messages: Xóa bài viết cá nhân label_example: Ví dụ text_repository_usernames_mapping: "Chọn hoặc cập nhật ánh xạ người dùng hệ thống với người dùng trong kho lưu trữ.\nNhững trường hợp trùng hợp về tên và email sẽ được tự động ánh xạ." - permission_delete_own_messages: Delete own messages - label_user_activity: "%{value}'s activity" - label_updated_time_by: "Updated by %{author} %{age} ago" - text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' - setting_diff_max_lines_displayed: Max number of diff lines displayed - text_plugin_assets_writable: Plugin assets directory writable - warning_attachments_not_saved: "%{count} file(s) could not be saved." - button_create_and_continue: Create and continue - text_custom_field_possible_values_info: 'One line for each value' - label_display: Display - field_editable: Editable - setting_repository_log_display_limit: Maximum number of revisions displayed on file log - setting_file_max_size_displayed: Max size of text files displayed inline - field_watcher: Watcher - setting_openid: Allow OpenID login and registration + permission_delete_own_messages: Xóa thông điệp + label_user_activity: "%{value} hoạt động" + label_updated_time_by: "Cập nhật bởi %{author} cách đây %{age}" + text_diff_truncated: '... Thay đổi này đã được cắt bớt do nó vượt qua giới hạn kích thước có thể hiển thị.' + setting_diff_max_lines_displayed: Số dòng thay đổi tối đa được hiển thị + text_plugin_assets_writable: Cho phép ghi thư mục Plugin + warning_attachments_not_saved: "%{count} file không được lưu." + button_create_and_continue: Tạo và tiếp tục + text_custom_field_possible_values_info: 'Một dòng cho mỗi giá trị' + label_display: Hiển thị + field_editable: Có thể sửa được + setting_repository_log_display_limit: Số lượng tối đa các bản điều chỉnh hiển thị trong file log + setting_file_max_size_displayed: Kích thước tối đa của tệp tin văn bản + field_watcher: Người quan sát + setting_openid: Cho phép đăng nhập và đăng ký dùng OpenID field_identity_url: OpenID URL - label_login_with_open_id_option: or login with OpenID - field_content: Content - label_descending: Descending - label_sort: Sort - label_ascending: Ascending - label_date_from_to: From %{start} to %{end} + label_login_with_open_id_option: hoặc đăng nhập với OpenID + field_content: Nội dung + label_descending: Giảm dần + label_sort: Sắp xếp + label_ascending: Tăng dần + label_date_from_to: "Từ %{start} tới %{end}" label_greater_or_equal: ">=" - label_less_or_equal: <= - text_wiki_page_destroy_question: This page has %{descendants} child page(s) and descendant(s). What do you want to do? - text_wiki_page_reassign_children: Reassign child pages to this parent page - text_wiki_page_nullify_children: Keep child pages as root pages - text_wiki_page_destroy_children: Delete child pages and all their descendants - setting_password_min_length: Minimum password length - field_group_by: Group results by - mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" - label_wiki_content_added: Wiki page added - mail_subject_wiki_content_added: "'%{id}' wiki page has been added" - mail_body_wiki_content_added: The '%{id}' wiki page has been added by %{author}. - label_wiki_content_updated: Wiki page updated - mail_body_wiki_content_updated: The '%{id}' wiki page has been updated by %{author}. - permission_add_project: Create project - setting_new_project_user_role_id: Role given to a non-admin user who creates a project - label_view_all_revisions: View all revisions - label_tag: Tag - label_branch: Branch - error_no_tracker_in_project: No tracker is associated to this project. Please check the Project settings. - error_no_default_issue_status: No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses"). - text_journal_changed: "%{label} changed from %{old} to %{new}" - text_journal_set_to: "%{label} set to %{value}" - text_journal_deleted: "%{label} deleted (%{old})" - label_group_plural: Groups - label_group: Group - label_group_new: New group - label_time_entry_plural: Spent time - text_journal_added: "%{label} %{value} added" - field_active: Active - enumeration_system_activity: System Activity - permission_delete_issue_watchers: Delete watchers - version_status_closed: closed - version_status_locked: locked - version_status_open: open - error_can_not_reopen_issue_on_closed_version: An issue assigned to a closed version can not be reopened - label_user_anonymous: Anonymous - button_move_and_follow: Move and follow - setting_default_projects_modules: Default enabled modules for new projects - setting_gravatar_default: Default Gravatar image - field_sharing: Sharing - label_version_sharing_hierarchy: With project hierarchy - label_version_sharing_system: With all projects - label_version_sharing_descendants: With subprojects - label_version_sharing_tree: With project tree - label_version_sharing_none: Not shared - error_can_not_archive_project: This project can not be archived - button_duplicate: Duplicate - button_copy_and_follow: Copy and follow - label_copy_source: Source - setting_issue_done_ratio: Calculate the issue done ratio with - setting_issue_done_ratio_issue_status: Use the issue status - error_issue_done_ratios_not_updated: Issue done ratios not updated. - error_workflow_copy_target: Please select target tracker(s) and role(s) - setting_issue_done_ratio_issue_field: Use the issue field - label_copy_same_as_target: Same as target - label_copy_target: Target - notice_issue_done_ratios_updated: Issue done ratios updated. - error_workflow_copy_source: Please select a source tracker or role - label_update_issue_done_ratios: Update issue done ratios - setting_start_of_week: Start calendars on - permission_view_issues: View Issues - label_display_used_statuses_only: Only display statuses that are used by this tracker - label_revision_id: Revision %{value} - label_api_access_key: API access key - label_api_access_key_created_on: API access key created %{value} ago - label_feeds_access_key: RSS access key - notice_api_access_key_reseted: Your API access key was reset. - setting_rest_api_enabled: Enable REST web service - label_missing_api_access_key: Missing an API access key - label_missing_feeds_access_key: Missing a RSS access key - button_show: Show - text_line_separated: Multiple values allowed (one line for each value). - setting_mail_handler_body_delimiters: Truncate emails after one of these lines - permission_add_subprojects: Create subprojects - label_subproject_new: New subproject + label_less_or_equal: "<=" + text_wiki_page_destroy_question: "Trang này có %{descendants} trang con và trang cháu. Bạn muốn làm gì tiếp?" + text_wiki_page_reassign_children: Gán lại trang con vào trang mẹ này + text_wiki_page_nullify_children: Giữ trang con như trang gốc + text_wiki_page_destroy_children: Xóa trang con và tất cả trang con cháu của nó + setting_password_min_length: Chiều dài tối thiểu của mật khẩu + field_group_by: Nhóm kết quả bởi + mail_subject_wiki_content_updated: "%{id} trang wiki đã được cập nhật" + label_wiki_content_added: Đã thêm trang Wiki + mail_subject_wiki_content_added: "%{id} trang wiki đã được thêm vào" + mail_body_wiki_content_added: "Có %{id} trang wiki đã được thêm vào bởi %{author}." + label_wiki_content_updated: Trang Wiki đã được cập nhật + mail_body_wiki_content_updated: "Có %{id} trang wiki đã được cập nhật bởi %{author}." + permission_add_project: Tạo dự án + setting_new_project_user_role_id: Quyền được gán cho người dùng không phải quản trị viên khi tạo dự án mới + label_view_all_revisions: Xem tất cả bản điều chỉnh + label_tag: Thẻ + label_branch: Nhánh + error_no_tracker_in_project: Không có ai theo dõi dự án này. Hãy kiểm tra lại phần thiết lập cho dự án. + error_no_default_issue_status: Không có vấn đề mặc định được định nghĩa. Vui lòng kiểm tra cấu hình của bạn (Vào "Quản trị -> Trạng thái vấn đề"). + text_journal_changed: "%{label} thay đổi từ %{old} tới %{new}" + text_journal_set_to: "%{label} gán cho %{value}" + text_journal_deleted: "%{label} xóa (%{old})" + label_group_plural: Các nhóm + label_group: Nhóm + label_group_new: Thêm nhóm + label_time_entry_plural: Thời gian đã sử dụng + text_journal_added: "%{label} %{value} được thêm" + field_active: Tích cực + enumeration_system_activity: Hoạt động hệ thống + permission_delete_issue_watchers: Xóa người quan sát + version_status_closed: đóng + version_status_locked: khóa + version_status_open: mở + error_can_not_reopen_issue_on_closed_version: Một vấn đề được gán cho phiên bản đã đóng không thể mở lại được + label_user_anonymous: Ẩn danh + button_move_and_follow: Di chuyển và theo + setting_default_projects_modules: Các Module được kích hoạt mặc định cho dự án mới + setting_gravatar_default: Ảnh Gravatar mặc định + field_sharing: Chia sẻ + label_version_sharing_hierarchy: Với thứ bậc dự án + label_version_sharing_system: Với tất cả dự án + label_version_sharing_descendants: Với dự án con + label_version_sharing_tree: Với cây dự án + label_version_sharing_none: Không chia sẻ + error_can_not_archive_project: Dựa án này không thể lưu trữ được + button_duplicate: Nhân đôi + button_copy_and_follow: Sao chép và theo + label_copy_source: Nguồn + setting_issue_done_ratio: Tính toán tỷ lệ hoàn thành vấn đề với + setting_issue_done_ratio_issue_status: Sử dụng trạng thái của vấn đề + error_issue_done_ratios_not_updated: Tỷ lệ hoàn thành vấn đề không được cập nhật. + error_workflow_copy_target: Vui lòng lựa chọn đích của theo dấu và quyền + setting_issue_done_ratio_issue_field: Dùng trường vấn đề + label_copy_same_as_target: Tương tự như đích + label_copy_target: Đích + notice_issue_done_ratios_updated: Tỷ lệ hoàn thành vấn đề được cập nhật. + error_workflow_copy_source: Vui lòng lựa chọn nguồn của theo dấu hoặc quyền + label_update_issue_done_ratios: Cập nhật tỷ lệ hoàn thành vấn đề + setting_start_of_week: Định dạng lịch + permission_view_issues: Xem Vấn đề + label_display_used_statuses_only: Chỉ hiển thị trạng thái đã được dùng bởi theo dõi này + label_revision_id: "Bản điều chỉnh %{value}" + label_api_access_key: Khoá truy cập API + label_api_access_key_created_on: "Khoá truy cập API đựơc tạo cách đây %{value}. Khóa này được dùng cho eDesignLab Client." + label_feeds_access_key: Khoá truy cập RSS + notice_api_access_key_reseted: Khoá truy cập API của bạn đã được đặt lại. + setting_rest_api_enabled: Cho phép dịch vụ web REST + label_missing_api_access_key: Mất Khoá truy cập API + label_missing_feeds_access_key: Mất Khoá truy cập RSS + button_show: Hiện + text_line_separated: Nhiều giá trị được phép(mỗi dòng một giá trị). + setting_mail_handler_body_delimiters: "Cắt bớt email sau những dòng :" + permission_add_subprojects: Tạo Dự án con + label_subproject_new: Thêm dự án con text_own_membership_delete_confirmation: |- - You are about to remove some or all of your permissions and may no longer be able to edit this project after that. - Are you sure you want to continue? - label_close_versions: Close completed versions - label_board_sticky: Sticky - label_board_locked: Locked - permission_export_wiki_pages: Export wiki pages - setting_cache_formatted_text: Cache formatted text - permission_manage_project_activities: Manage project activities - error_unable_delete_issue_status: Unable to delete issue status - label_profile: Profile - permission_manage_subtasks: Manage subtasks - field_parent_issue: Parent task - label_subtask_plural: Subtasks - label_project_copy_notifications: Send email notifications during the project copy - error_can_not_delete_custom_field: Unable to delete custom field - error_unable_to_connect: Unable to connect (%{value}) - error_can_not_remove_role: This role is in use and can not be deleted. - error_can_not_delete_tracker: This tracker contains issues and can't be deleted. - field_principal: Principal - label_my_page_block: My page block - notice_failed_to_save_members: "Failed to save member(s): %{errors}." - text_zoom_out: Zoom out - text_zoom_in: Zoom in - notice_unable_delete_time_entry: Unable to delete time log entry. - label_overall_spent_time: Overall spent time + Bạn đang cố gỡ bỏ một số hoặc tất cả quyền của bạn với dự án này và có thể sẽ mất quyền thay đổi nó sau đó. + Bạn có muốn tiếp tục? + label_close_versions: Đóng phiên bản đã hoàn thành + label_board_sticky: Chú ý + label_board_locked: Đã khóa + permission_export_wiki_pages: Xuất trang wiki + setting_cache_formatted_text: Cache định dạng các ký tự + permission_manage_project_activities: Quản lý hoạt động của dự án + error_unable_delete_issue_status: Không thể xóa trạng thái vấn đề + label_profile: Hồ sơ + permission_manage_subtasks: Quản lý tác vụ con + field_parent_issue: Tác vụ cha + label_subtask_plural: Tác vụ con + label_project_copy_notifications: Gửi email thông báo trong khi dự án được sao chép + error_can_not_delete_custom_field: Không thể xóa trường tùy biến + error_unable_to_connect: "Không thể kết nối (%{value})" + error_can_not_remove_role: Quyền này đang được dùng và không thể xóa được. + error_can_not_delete_tracker: Theo dõi này chứa vấn đề và không thể xóa được. + field_principal: Chủ yếu + label_my_page_block: Block trang của tôi + notice_failed_to_save_members: "Thất bại khi lưu thành viên : %{errors}." + text_zoom_out: Thu nhỏ + text_zoom_in: Phóng to + notice_unable_delete_time_entry: Không thể xóa mục time log. + label_overall_spent_time: Tổng thời gian sử dụng field_time_entries: Log time - project_module_gantt: Gantt - project_module_calendar: Calendar - button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" - field_text: Text field - label_user_mail_option_only_owner: Only for things I am the owner of - setting_default_notification_option: Default notification option - label_user_mail_option_only_my_events: Only for things I watch or I'm involved in - label_user_mail_option_only_assigned: Only for things I am assigned to - label_user_mail_option_none: No events - field_member_of_group: Assignee's group - field_assigned_to_role: Assignee's role - notice_not_authorized_archived_project: The project you're trying to access has been archived. - label_principal_search: "Search for user or group:" - label_user_search: "Search for user:" - field_visible: Visible - setting_emails_header: Emails header - setting_commit_logtime_activity_id: Activity for logged time - text_time_logged_by_changeset: Applied in changeset %{value}. - setting_commit_logtime_enabled: Enable time logging - notice_gantt_chart_truncated: The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max}) - setting_gantt_items_limit: Maximum number of items displayed on the gantt chart - field_warn_on_leaving_unsaved: Warn me when leaving a page with unsaved text - text_warn_on_leaving_unsaved: The current page contains unsaved text that will be lost if you leave this page. - label_my_queries: My custom queries - text_journal_changed_no_detail: "%{label} updated" - label_news_comment_added: Comment added to a news - button_expand_all: Expand all - button_collapse_all: Collapse all - label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee - label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author - label_bulk_edit_selected_time_entries: Bulk edit selected time entries - text_time_entries_destroy_confirmation: Are you sure you want to delete the selected time entr(y/ies)? - label_role_anonymous: Anonymous - label_role_non_member: Non member - label_issue_note_added: Note added - label_issue_status_updated: Status updated - label_issue_priority_updated: Priority updated - label_issues_visibility_own: Issues created by or assigned to the user - field_issues_visibility: Issues visibility - label_issues_visibility_all: All issues - permission_set_own_issues_private: Set own issues public or private - field_is_private: Private - permission_set_issues_private: Set issues public or private - label_issues_visibility_public: All non private issues - text_issues_destroy_descendants_confirmation: This will also delete %{count} subtask(s). - field_commit_logs_encoding: Commit messages encoding - field_scm_path_encoding: Path encoding - text_scm_path_encoding_note: "Default: UTF-8" - field_path_to_repository: Path to repository - field_root_directory: Root directory + project_module_gantt: Biểu đồ Gantt + project_module_calendar: Lịch + button_edit_associated_wikipage: "Chỉnh sửa trang Wiki liên quan: %{page_title}" + text_are_you_sure_with_children: Xóa vấn đề và tất cả vấn đề con? + field_text: Trường văn bản + label_user_mail_option_only_owner: Chỉ những thứ tôi sở hữu + setting_default_notification_option: Tuỳ chọn thông báo mặc định + label_user_mail_option_only_my_events: Chỉ những thứ tôi theo dõi hoặc liên quan + label_user_mail_option_only_assigned: Chỉ những thứ tôi được phân công + label_user_mail_option_none: Không có sự kiện + field_member_of_group: Nhóm thụ hưởng + field_assigned_to_role: Quyền thụ hưởng + notice_not_authorized_archived_project: Dự án bạn đang có truy cập đã được lưu trữ. + label_principal_search: "Tìm kiếm người dùng hoặc nhóm:" + label_user_search: "Tìm kiếm người dùng:" + field_visible: Nhìn thấy + setting_emails_header: Tiêu đề Email + setting_commit_logtime_activity_id: Cho phép ghi lại thời gian + text_time_logged_by_changeset: "Áp dụng trong changeset : %{value}." + setting_commit_logtime_enabled: Cho phép time logging + notice_gantt_chart_truncated: "Đồ thị đã được cắt bớt bởi vì nó đã vượt qua lượng thông tin tối đa có thể hiển thị :(%{max})" + setting_gantt_items_limit: Lượng thông tin tối đa trên đồ thị gantt + description_selected_columns: Các cột được lựa chọn + field_warn_on_leaving_unsaved: Cảnh báo tôi khi rời một trang có các nội dung chưa lưu + text_warn_on_leaving_unsaved: Trang hiện tại chứa nội dung chưa lưu và sẽ bị mất nếu bạn rời trang này. + label_my_queries: Các truy vấn tùy biến + text_journal_changed_no_detail: "%{label} cập nhật" + label_news_comment_added: Bình luận đã được thêm cho một tin tức + button_expand_all: Mở rộng tất cả + button_collapse_all: Thu gọn tất cả + label_additional_workflow_transitions_for_assignee: Chuyển đổi bổ sung cho phép khi người sử dụng là người nhận chuyển nhượng + label_additional_workflow_transitions_for_author: Các chuyển đổi bổ xung được phép khi người dùng là tác giả + label_bulk_edit_selected_time_entries: Sửa nhiều mục đã chọn + text_time_entries_destroy_confirmation: Bạn có chắc chắn muốn xóa bỏ các mục đã chọn? + label_role_anonymous: Ẩn danh + label_role_non_member: Không là thành viên + label_issue_note_added: Ghi chú được thêm + label_issue_status_updated: Trạng thái cập nhật + label_issue_priority_updated: Cập nhật ưu tiên + label_issues_visibility_own: Vấn đề tạo bởi hoặc gán cho người dùng + field_issues_visibility: Vấn đề được nhìn thấy + label_issues_visibility_all: Tất cả vấn đề + permission_set_own_issues_private: Đặt vấn đề sở hữu là riêng tư hoặc công cộng + field_is_private: Riêng tư + permission_set_issues_private: Gán vấn đề là riêng tư hoặc công cộng + label_issues_visibility_public: Tất cả vấn đề không riêng tư + text_issues_destroy_descendants_confirmation: "Hành động này sẽ xóa %{count} tác vụ con." + field_commit_logs_encoding: Mã hóa ghi chú Commit + field_scm_path_encoding: Mã hóa đường dẫn + text_scm_path_encoding_note: "Mặc định: UTF-8" + field_path_to_repository: Đường dẫn tới kho chứa + field_root_directory: Thư mục gốc field_cvs_module: Module field_cvsroot: CVSROOT - text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) - text_scm_command: Command - text_scm_command_version: Version - label_git_report_last_commit: Report last commit for files and directories - text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. - text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. - notice_issue_successful_create: Issue %{id} created. - label_between: between - setting_issue_group_assignment: Allow issue assignment to groups - label_diff: diff - text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) - description_query_sort_criteria_direction: Sort direction - description_project_scope: Search scope - description_filter: Filter - description_user_mail_notification: Mail notification settings - description_date_from: Enter start date - description_message_content: Message content - description_available_columns: Available Columns - description_date_range_interval: Choose range by selecting start and end date - description_issue_category_reassign: Choose issue category - description_search: Searchfield - description_notes: Notes - description_date_range_list: Choose range from list - description_choose_project: Projects - description_date_to: Enter end date - description_query_sort_criteria_attribute: Sort attribute - description_wiki_subpages_reassign: Choose new parent page - description_selected_columns: Selected Columns - label_parent_revision: Parent - label_child_revision: Child - error_scm_annotate_big_text_file: The entry cannot be annotated, as it exceeds the maximum text file size. - setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues - button_edit_section: Edit this section - setting_repositories_encodings: Attachments and repositories encodings - description_all_columns: All Columns + text_mercurial_repository_note: Kho chứa cục bộ (vd. /hgrepo, c:\hgrepo) + text_scm_command: Lệnh + text_scm_command_version: Phiên bản + label_git_report_last_commit: Báo cáo lần Commit cuối cùng cho file và thư mục + text_scm_config: Bạn có thể cấu hình lệnh Scm trong file config/configuration.yml. Vui lòng khởi động lại ứng dụng sau khi chỉnh sửa nó. + text_scm_command_not_available: Lệnh Scm không có sẵn. Vui lòng kiểm tra lại thiết đặt trong phần Quản trị. + notice_issue_successful_create: "Vấn đề %{id} đã được tạo." + label_between: Ở giữa + setting_issue_group_assignment: Cho phép gán vấn đề đến các nhóm + label_diff: Sự khác nhau + text_git_repository_note: Kho chứa cục bộ và công cộng (vd. /gitrepo, c:\gitrepo) + description_query_sort_criteria_direction: Chiều sắp xếp + description_project_scope: Phạm vi tìm kiếm + description_filter: Lọc + description_user_mail_notification: Thiết lập email thông báo + description_date_from: Nhập ngày bắt đầu + description_message_content: Nội dung thông điệp + description_available_columns: Các cột có sẵn + description_date_range_interval: Chọn khoảng thời gian giữa ngày bắt đầu và kết thúc + description_issue_category_reassign: Chọn danh mục vấn đề + description_search: Trường tìm kiếm + description_notes: Các chú ý + description_date_range_list: Chọn khoảng từ danh sách + description_choose_project: Các dự án + description_date_to: Nhập ngày kết thúc + description_query_sort_criteria_attribute: Sắp xếp thuộc tính + description_wiki_subpages_reassign: Chọn một trang cấp trên + label_parent_revision: Cha + label_child_revision: Con + error_scm_annotate_big_text_file: Các mục không được chú thích, vì nó vượt quá kích thước tập tin văn bản tối đa. + setting_default_issue_start_date_to_creation_date: Sử dụng thời gian hiện tại khi tạo vấn đề mới + button_edit_section: Soạn thảo sự lựa chọn này + setting_repositories_encodings: Mã hóa kho chứa + description_all_columns: Các cột button_export: Export - label_export_options: "%{export_format} export options" - error_attachment_too_big: This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size}) - notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." + label_export_options: "%{export_format} tùy chọn Export" + error_attachment_too_big: "File này không thể tải lên vì nó vượt quá kích thước cho phép : (%{max_size})" + notice_failed_to_save_time_entries: "Lỗi khi lưu %{count} lần trên %{total} sự lựa chọn : %{ids}." label_x_issues: zero: 0 vấn đề one: 1 vấn đề other: "%{count} vấn đề" - label_repository_new: New repository - field_repository_is_default: Main repository - label_copy_attachments: Copy attachments + label_repository_new: Kho lưu trữ mới + field_repository_is_default: Kho lưu trữ chính + label_copy_attachments: Copy các file đính kèm label_item_position: "%{position}/%{count}" - label_completed_versions: Completed versions - text_project_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_multiple: Multiple values - setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed - text_issue_conflict_resolution_add_notes: Add my notes and discard my other changes - text_issue_conflict_resolution_overwrite: Apply my changes anyway (previous notes will be kept but some changes may be overwritten) - notice_issue_update_conflict: The issue has been updated by an other user while you were editing it. - text_issue_conflict_resolution_cancel: Discard all my changes and redisplay %{link} - permission_manage_related_issues: Manage related issues - field_auth_source_ldap_filter: LDAP filter - label_search_for_watchers: Search for watchers to add - notice_account_deleted: Your account has been permanently deleted. - setting_unsubscribe: Allow users to delete their own account - button_delete_my_account: Delete my account + label_completed_versions: Các phiên bản hoàn thành + text_project_identifier_info: Chỉ cho phép chữ cái thường (a-z), con số và dấu gạch ngang.<br />Sau khi lưu, chỉ số ID không thể thay đổi. + field_multiple: Nhiều giá trị + setting_commit_cross_project_ref: Sử dụng thời gian hiện tại khi tạo vấn đề mới + text_issue_conflict_resolution_add_notes: Thêm ghi chú của tôi và loại bỏ các thay đổi khác + text_issue_conflict_resolution_overwrite: Áp dụng thay đổi bằng bất cứ giá nào, ghi chú trước đó có thể bị ghi đè + notice_issue_update_conflict: Vấn đề này đã được cập nhật bởi một người dùng khác trong khi bạn đang chỉnh sửa nó. + text_issue_conflict_resolution_cancel: "Loại bỏ tất cả các thay đổi và hiển thị lại %{link}" + permission_manage_related_issues: Quản lý các vấn đề liên quan + field_auth_source_ldap_filter: Bộ lọc LDAP + label_search_for_watchers: Tìm kiếm người theo dõi để thêm + notice_account_deleted: Tài khoản của bạn đã được xóa vĩnh viễn. + button_delete_my_account: Xóa tài khoản của tôi + setting_unsubscribe: Cho phép người dùng xóa Account text_account_destroy_confirmation: |- - Are you sure you want to proceed? - Your account will be permanently deleted, with no way to reactivate it. - error_session_expired: Your session has expired. Please login again. - text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." - setting_session_lifetime: Session maximum lifetime - setting_session_timeout: Session inactivity timeout - label_session_expiration: Session expiration - permission_close_project: Close / reopen the project - label_show_closed_projects: View closed projects - button_close: Close - button_reopen: Reopen - project_status_active: active - project_status_closed: closed - project_status_archived: archived - text_project_closed: This project is closed and read-only. - notice_user_successful_create: User %{id} created. - field_core_fields: Standard fields - field_timeout: Timeout (in seconds) - setting_thumbnails_enabled: Display attachment thumbnails - setting_thumbnails_size: Thumbnails size (in pixels) - label_status_transitions: Status transitions - label_fields_permissions: Fields permissions - label_readonly: Read-only - label_required: Required - text_repository_identifier_info: Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed. - field_board_parent: Parent forum - label_attribute_of_project: Project's %{name} - label_attribute_of_author: Author's %{name} - label_attribute_of_assigned_to: Assignee's %{name} - label_attribute_of_fixed_version: Target version's %{name} - label_copy_subtasks: Copy subtasks - label_copied_to: copied to - label_copied_from: copied from - label_any_issues_in_project: any issues in project - label_any_issues_not_in_project: any issues not in project - field_private_notes: Private notes - permission_view_private_notes: View private notes - permission_set_notes_private: Set notes as private - label_no_issues_in_project: no issues in project + Bạn đồng ý không ? + Tài khoản của bạn sẽ bị xóa vĩnh viễn, không thể khôi phục lại! + error_session_expired: Phiên làm việc của bạn bị quá hạn, hãy đăng nhập lại + text_session_expiration_settings: "Chú ý : Thay đổi các thiết lập này có thể gây vô hiệu hóa Session hiện tại" + setting_session_lifetime: Thời gian tồn tại lớn nhất của Session + setting_session_timeout: Thời gian vô hiệu hóa Session + label_session_expiration: Phiên làm việc bị quá hạn + permission_close_project: Đóng / Mở lại dự án + label_show_closed_projects: Xem các dự án đã đóng + button_close: Đóng + button_reopen: Mở lại + project_status_active: Kích hoạt + project_status_closed: Đã đóng + project_status_archived: Lưu trữ + text_project_closed: Dự án này đã đóng và chỉ đọc + notice_user_successful_create: "Người dùng %{id} đã được tạo." + field_core_fields: Các trường tiêu chuẩn + field_timeout: Quá hạn + setting_thumbnails_enabled: Hiển thị các thumbnail đính kèm + setting_thumbnails_size: Kích thước Thumbnails(pixel) + setting_session_lifetime: Thời gian tồn tại lớn nhất của Session + setting_session_timeout: Thời gian vô hiệu hóa Session + label_status_transitions: Trạng thái chuyển tiếp + label_fields_permissions: Cho phép các trường + label_readonly: Chỉ đọc + label_required: Yêu cầu + text_repository_identifier_info: Chỉ có các chữ thường (a-z), các số (0-9), dấu gạch ngang và gạch dưới là hợp lệ.<br />Khi đã lưu, tên định danh sẽ không thể thay đổi. + field_board_parent: Diễn đàn cha + label_attribute_of_project: "Của dự án : %{name}" + label_attribute_of_author: "Của tác giả : %{name}" + label_attribute_of_assigned_to: "Được phân công bởi %{name}" + label_attribute_of_fixed_version: "Phiên bản mục tiêu của %{name}" + label_copy_subtasks: Sao chép các nhiệm vụ con + label_copied_to: Sao chép đến + label_copied_from: Sao chép từ + label_any_issues_in_project: Bất kỳ vấn đề nào trong dự án + label_any_issues_not_in_project: Bất kỳ vấn đề nào không thuộc dự án + field_private_notes: Ghi chú riêng tư + permission_view_private_notes: Xem ghi chú riêng tư + permission_set_notes_private: Đặt ghi chú thành riêng tư + label_no_issues_in_project: Không có vấn đề nào trong dự án label_any: tất cả - label_last_n_weeks: last %{count} weeks - setting_cross_project_subtasks: Allow cross-project subtasks - label_cross_project_descendants: With subprojects - label_cross_project_tree: With project tree - label_cross_project_hierarchy: With project hierarchy - label_cross_project_system: With all projects - button_hide: Hide - setting_non_working_week_days: Non-working days - label_in_the_next_days: in the next - label_in_the_past_days: in the past + label_last_n_weeks: "%{count} tuần qua" + setting_cross_project_subtasks: Cho phép các nhiệm vụ con liên dự án + label_cross_project_descendants: Trong các dự án con + label_cross_project_tree: Trong cùng cây dự án + label_cross_project_hierarchy: Trong dự án cùng cấp bậc + label_cross_project_system: Trong tất cả các dự án + button_hide: Ẩn + setting_non_working_week_days: Các ngày không làm việc + label_in_the_next_days: Trong tương lai + label_in_the_past_days: Trong quá khứ
--- a/doc/CHANGELOG Mon Jan 07 12:01:42 2013 +0000 +++ b/doc/CHANGELOG Fri Jun 14 09:01:12 2013 +0100 @@ -4,6 +4,58 @@ Copyright (C) 2006-2012 Jean-Philippe Lang http://www.redmine.org/ +== 2013-03-19 v2.2.4 + +* Upgrade to Rails 3.2.13 +* Defect #12243: Ordering forum replies by last reply date is broken +* Defect #13127: h1 multiple lined titles breaks into main menu +* Defect #13138: Generating PDF of issue causes UndefinedConversionError with htmlentities gem +* Defect #13165: rdm-mailhandler.rb: initialize_http_header override basic auth +* Defect #13232: Link to topic in nonexistent forum causes error 500 +* Patch #13181: Bulgarian translation of jstoolbar-bg.js +* Patch #13207: Portuguese translation for 2.2-stable +* Patch #13310: pt-BR label_last_n_weeks translation +* Patch #13325: pt-BR translation for 2.2-stable +* Patch #13343: Vietnamese translation for 2.2-stable +* Patch #13398: Czech translation for 2.2-stable + +== 2013-02-12 v2.2.3 + +* Upgrade to Rails 3.2.12 +* Defect #11987: pdf: Broken new line in table +* Defect #12930: 404 Error when referencing different project source files in the wiki syntax +* Defect #12979: Wiki link syntax commit:repo_a:abcd doesn't work +* Defect #13075: Can't clear custom field value through context menu in the issue list +* Defect #13097: Project copy fails when wiki module is disabled +* Defect #13126: Issue view: estimated time vs. spent time +* Patch #12922: Update Spanish translation +* Patch #12928: Bulgarian translation for 2.2-stable +* Patch #12987: Russian translation for 2.2-stable + +== 2013-01-20 v2.2.2 + +* Defect #7510: Link to attachment should return latest attachment +* Defect #9842: {{toc}} is not replaced by table of content when exporting wiki page to pdf +* Defect #12749: Plugins cannot route wiki page sub-path +* Defect #12799: Cannot edit a wiki section which title starts with a tab +* Defect #12801: Viewing the history of a wiki page with attachments raises an error +* Defect #12833: Input fields restricted on length should have maxlength parameter set +* Defect #12838: Blank page when clicking Add with no block selected on my page layout +* Defect #12851: "Parent task is invalid" while editing child issues by Role with restricted Issues Visibility +* Patch #12800: Serbian Latin translation patch (sr-YU.yml) +* Patch #12809: Swedish Translation for r11162 +* Patch #12818: Minor swedish translation fix + +== 2013-01-09 v2.2.1 + +* Upgrade to Rails 3.2.11 +* Defect #12652: "Copy ticket" selects "new ticket" +* Defect #12691: Textile Homepage Dead? +* Defect #12711: incorrect fix of lib/SVG/Graph/TimeSeries.rb +* Defect #12744: Unable to call a macro with a name that contains uppercase letters +* Defect #12776: Security vulnerability in Rails 3.2.10 (CVE-2013-0156) +* Patch #12630: Russian "x_hours" translation + == 2012-12-18 v2.2.0 * Defect #4787: Gannt to PNG - CJK (Chinese, Japanese and Korean) characters appear as ?
--- a/extra/mail_handler/rdm-mailhandler.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/extra/mail_handler/rdm-mailhandler.rb Fri Jun 14 09:01:12 2013 +0100 @@ -10,8 +10,8 @@ def self.post_form(url, params, headers, options={}) request = Post.new(url.path) request.form_data = params + request.initialize_http_header(headers) request.basic_auth url.user, url.password if url.user - request.initialize_http_header(headers) http = new(url.host, url.port) http.use_ssl = (url.scheme == 'https') if options[:no_check_certificate] @@ -23,7 +23,7 @@ end class RedmineMailHandler - VERSION = '0.2' + VERSION = '0.2.1' attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :no_permission_check, :url, :key, :no_check_certificate
--- a/lib/SVG/Graph/Bar.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/Bar.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,148 +1,148 @@ -require 'rexml/document' -require 'SVG/Graph/Graph' -require 'SVG/Graph/BarBase' - -module SVG - module Graph - # === Create presentation quality SVG bar graphs easily - # - # = Synopsis - # - # require 'SVG/Graph/Bar' - # - # fields = %w(Jan Feb Mar); - # data_sales_02 = [12, 45, 21] - # - # graph = SVG::Graph::Bar.new( - # :height => 500, - # :width => 300, - # :fields => fields - # ) - # - # graph.add_data( - # :data => data_sales_02, - # :title => 'Sales 2002' - # ) - # - # print "Content-type: image/svg+xml\r\n\r\n" - # print graph.burn - # - # = Description - # - # This object aims to allow you to easily create high quality - # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default - # style sheet or supply your own. Either way there are many options which - # can be configured to give you control over how the graph is generated - - # with or without a key, data elements at each point, title, subtitle etc. - # - # = Notes - # - # The default stylesheet handles upto 12 data sets, if you - # use more you must create your own stylesheet and add the - # additional settings for the extra data sets. You will know - # if you go over 12 data sets as they will have no style and - # be in black. - # - # = Examples - # - # * http://germane-software.com/repositories/public/SVG/test/test.rb - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries - class Bar < BarBase - include REXML - - # See Graph::initialize and BarBase::set_defaults - def set_defaults - super - self.top_align = self.top_font = 1 - end - - protected - - def get_x_labels - @config[:fields] - end - - def get_y_labels - maxvalue = max_value - minvalue = min_value - range = maxvalue - minvalue - - top_pad = range == 0 ? 10 : range / 20.0 - scale_range = (maxvalue + top_pad) - minvalue - - scale_division = scale_divisions || (scale_range / 10.0) - - if scale_integers - scale_division = scale_division < 1 ? 1 : scale_division.round - end - - rv = [] - maxvalue = maxvalue%scale_division == 0 ? - maxvalue : maxvalue + scale_division - minvalue.step( maxvalue, scale_division ) {|v| rv << v} - return rv - end - - def x_label_offset( width ) - width / 2.0 - end - - def draw_data - minvalue = min_value - fieldwidth = field_width - - unit_size = (@graph_height.to_f - font_size*2*top_font) / - (get_y_labels.max - get_y_labels.min) - bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0 - - bar_width = fieldwidth - bargap - bar_width /= @data.length if stack == :side - x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0) - - bottom = @graph_height - - field_count = 0 - @config[:fields].each_index { |i| - dataset_count = 0 - for dataset in @data - - # cases (assume 0 = +ve): - # value min length - # +ve +ve value - min - # +ve -ve value - 0 - # -ve -ve value.abs - 0 - - value = dataset[:data][i] - - left = (fieldwidth * field_count) - - length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size - # top is 0 if value is negative - top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size) - left += bar_width * dataset_count if stack == :side - - @graph.add_element( "rect", { - "x" => left.to_s, - "y" => top.to_s, - "width" => bar_width.to_s, - "height" => length.to_s, - "class" => "fill#{dataset_count+1}" - }) - - make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s) - dataset_count += 1 - end - field_count += 1 - } - end - end - end -end +require 'rexml/document' +require 'SVG/Graph/Graph' +require 'SVG/Graph/BarBase' + +module SVG + module Graph + # === Create presentation quality SVG bar graphs easily + # + # = Synopsis + # + # require 'SVG/Graph/Bar' + # + # fields = %w(Jan Feb Mar); + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::Bar.new( + # :height => 500, + # :width => 300, + # :fields => fields + # ) + # + # graph.add_data( + # :data => data_sales_02, + # :title => 'Sales 2002' + # ) + # + # print "Content-type: image/svg+xml\r\n\r\n" + # print graph.burn + # + # = Description + # + # This object aims to allow you to easily create high quality + # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default + # style sheet or supply your own. Either way there are many options which + # can be configured to give you control over how the graph is generated - + # with or without a key, data elements at each point, title, subtitle etc. + # + # = Notes + # + # The default stylesheet handles upto 12 data sets, if you + # use more you must create your own stylesheet and add the + # additional settings for the extra data sets. You will know + # if you go over 12 data sets as they will have no style and + # be in black. + # + # = Examples + # + # * http://germane-software.com/repositories/public/SVG/test/test.rb + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + class Bar < BarBase + include REXML + + # See Graph::initialize and BarBase::set_defaults + def set_defaults + super + self.top_align = self.top_font = 1 + end + + protected + + def get_x_labels + @config[:fields] + end + + def get_y_labels + maxvalue = max_value + minvalue = min_value + range = maxvalue - minvalue + + top_pad = range == 0 ? 10 : range / 20.0 + scale_range = (maxvalue + top_pad) - minvalue + + scale_division = scale_divisions || (scale_range / 10.0) + + if scale_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + rv = [] + maxvalue = maxvalue%scale_division == 0 ? + maxvalue : maxvalue + scale_division + minvalue.step( maxvalue, scale_division ) {|v| rv << v} + return rv + end + + def x_label_offset( width ) + width / 2.0 + end + + def draw_data + minvalue = min_value + fieldwidth = field_width + + unit_size = (@graph_height.to_f - font_size*2*top_font) / + (get_y_labels.max - get_y_labels.min) + bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0 + + bar_width = fieldwidth - bargap + bar_width /= @data.length if stack == :side + x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0) + + bottom = @graph_height + + field_count = 0 + @config[:fields].each_index { |i| + dataset_count = 0 + for dataset in @data + + # cases (assume 0 = +ve): + # value min length + # +ve +ve value - min + # +ve -ve value - 0 + # -ve -ve value.abs - 0 + + value = dataset[:data][i] + + left = (fieldwidth * field_count) + + length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size + # top is 0 if value is negative + top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size) + left += bar_width * dataset_count if stack == :side + + @graph.add_element( "rect", { + "x" => left.to_s, + "y" => top.to_s, + "width" => bar_width.to_s, + "height" => length.to_s, + "class" => "fill#{dataset_count+1}" + }) + + make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s) + dataset_count += 1 + end + field_count += 1 + } + end + end + end +end
--- a/lib/SVG/Graph/BarBase.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/BarBase.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,139 +1,139 @@ -require 'rexml/document' -require 'SVG/Graph/Graph' - -module SVG - module Graph - # = Synopsis - # - # A superclass for bar-style graphs. Do not attempt to instantiate - # directly; use one of the subclasses instead. - # - # = Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class BarBase < SVG::Graph::Graph - # Ensures that :fields are provided in the configuration. - def initialize config - raise "fields was not supplied or is empty" unless config[:fields] && - config[:fields].kind_of?(Array) && - config[:fields].length > 0 - super - end - - # In addition to the defaults set in Graph::initialize, sets - # [bar_gap] true - # [stack] :overlap - def set_defaults - init_with( :bar_gap => true, :stack => :overlap ) - end - - # Whether to have a gap between the bars or not, default - # is true, set to false if you don't want gaps. - attr_accessor :bar_gap - # How to stack data sets. :overlap overlaps bars with - # transparent colors, :top stacks bars on top of one another, - # :side stacks the bars side-by-side. Defaults to :overlap. - attr_accessor :stack - - - protected - - def max_value - @data.collect{|x| x[:data].max}.max - end - - def min_value - min = 0 - if min_scale_value.nil? - min = @data.collect{|x| x[:data].min}.min - min = min > 0 ? 0 : min - else - min = min_scale_value - end - return min - end - - def get_css - return <<EOL -/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */ -.key1,.fill1{ - fill: #ff0000; - fill-opacity: 0.5; - stroke: none; - stroke-width: 0.5px; -} -.key2,.fill2{ - fill: #0000ff; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key3,.fill3{ - fill: #00ff00; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key4,.fill4{ - fill: #ffcc00; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key5,.fill5{ - fill: #00ccff; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key6,.fill6{ - fill: #ff00ff; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key7,.fill7{ - fill: #00ffff; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key8,.fill8{ - fill: #ffff00; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key9,.fill9{ - fill: #cc6666; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key10,.fill10{ - fill: #663399; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key11,.fill11{ - fill: #339900; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -.key12,.fill12{ - fill: #9966FF; - fill-opacity: 0.5; - stroke: none; - stroke-width: 1px; -} -EOL - end - end - end -end +require 'rexml/document' +require 'SVG/Graph/Graph' + +module SVG + module Graph + # = Synopsis + # + # A superclass for bar-style graphs. Do not attempt to instantiate + # directly; use one of the subclasses instead. + # + # = Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class BarBase < SVG::Graph::Graph + # Ensures that :fields are provided in the configuration. + def initialize config + raise "fields was not supplied or is empty" unless config[:fields] && + config[:fields].kind_of?(Array) && + config[:fields].length > 0 + super + end + + # In addition to the defaults set in Graph::initialize, sets + # [bar_gap] true + # [stack] :overlap + def set_defaults + init_with( :bar_gap => true, :stack => :overlap ) + end + + # Whether to have a gap between the bars or not, default + # is true, set to false if you don't want gaps. + attr_accessor :bar_gap + # How to stack data sets. :overlap overlaps bars with + # transparent colors, :top stacks bars on top of one another, + # :side stacks the bars side-by-side. Defaults to :overlap. + attr_accessor :stack + + + protected + + def max_value + @data.collect{|x| x[:data].max}.max + end + + def min_value + min = 0 + if min_scale_value.nil? + min = @data.collect{|x| x[:data].min}.min + min = min > 0 ? 0 : min + else + min = min_scale_value + end + return min + end + + def get_css + return <<EOL +/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */ +.key1,.fill1{ + fill: #ff0000; + fill-opacity: 0.5; + stroke: none; + stroke-width: 0.5px; +} +.key2,.fill2{ + fill: #0000ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key3,.fill3{ + fill: #00ff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key4,.fill4{ + fill: #ffcc00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key5,.fill5{ + fill: #00ccff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key6,.fill6{ + fill: #ff00ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key7,.fill7{ + fill: #00ffff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key8,.fill8{ + fill: #ffff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key9,.fill9{ + fill: #cc6666; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key10,.fill10{ + fill: #663399; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key11,.fill11{ + fill: #339900; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key12,.fill12{ + fill: #9966FF; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +EOL + end + end + end +end
--- a/lib/SVG/Graph/BarHorizontal.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/BarHorizontal.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,149 +1,149 @@ -require 'rexml/document' -require 'SVG/Graph/BarBase' - -module SVG - module Graph - # === Create presentation quality SVG horitonzal bar graphs easily - # - # = Synopsis - # - # require 'SVG/Graph/BarHorizontal' - # - # fields = %w(Jan Feb Mar) - # data_sales_02 = [12, 45, 21] - # - # graph = SVG::Graph::BarHorizontal.new({ - # :height => 500, - # :width => 300, - # :fields => fields, - # }) - # - # graph.add_data({ - # :data => data_sales_02, - # :title => 'Sales 2002', - # }) - # - # print "Content-type: image/svg+xml\r\n\r\n" - # print graph.burn - # - # = Description - # - # This object aims to allow you to easily create high quality - # SVG horitonzal bar graphs. You can either use the default style sheet - # or supply your own. Either way there are many options which can - # be configured to give you control over how the graph is - # generated - with or without a key, data elements at each point, - # title, subtitle etc. - # - # = Examples - # - # * http://germane-software.com/repositories/public/SVG/test/test.rb - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries - # - # == Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class BarHorizontal < BarBase - # In addition to the defaults set in BarBase::set_defaults, sets - # [rotate_y_labels] true - # [show_x_guidelines] true - # [show_y_guidelines] false - def set_defaults - super - init_with( - :rotate_y_labels => true, - :show_x_guidelines => true, - :show_y_guidelines => false - ) - self.right_align = self.right_font = 1 - end - - protected - - def get_x_labels - maxvalue = max_value - minvalue = min_value - range = maxvalue - minvalue - top_pad = range == 0 ? 10 : range / 20.0 - scale_range = (maxvalue + top_pad) - minvalue - - scale_division = scale_divisions || (scale_range / 10.0) - - if scale_integers - scale_division = scale_division < 1 ? 1 : scale_division.round - end - - rv = [] - maxvalue = maxvalue%scale_division == 0 ? - maxvalue : maxvalue + scale_division - minvalue.step( maxvalue, scale_division ) {|v| rv << v} - return rv - end - - def get_y_labels - @config[:fields] - end - - def y_label_offset( height ) - height / -2.0 - end - - def draw_data - minvalue = min_value - fieldheight = field_height - - unit_size = (@graph_width.to_f - font_size*2*right_font ) / - (get_x_labels.max - get_x_labels.min ) - bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0 - - bar_height = fieldheight - bargap - bar_height /= @data.length if stack == :side - y_mod = (bar_height / 2) + (font_size / 2) - - field_count = 1 - @config[:fields].each_index { |i| - dataset_count = 0 - for dataset in @data - value = dataset[:data][i] - - top = @graph_height - (fieldheight * field_count) - top += (bar_height * dataset_count) if stack == :side - # cases (assume 0 = +ve): - # value min length left - # +ve +ve value.abs - min minvalue.abs - # +ve -ve value.abs - 0 minvalue.abs - # -ve -ve value.abs - 0 minvalue.abs + value - length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size - left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size - - @graph.add_element( "rect", { - "x" => left.to_s, - "y" => top.to_s, - "width" => length.to_s, - "height" => bar_height.to_s, - "class" => "fill#{dataset_count+1}" - }) - - make_datapoint_text( - left+length+5, top+y_mod, value, "text-anchor: start; " - ) - dataset_count += 1 - end - field_count += 1 - } - end - end - end -end +require 'rexml/document' +require 'SVG/Graph/BarBase' + +module SVG + module Graph + # === Create presentation quality SVG horitonzal bar graphs easily + # + # = Synopsis + # + # require 'SVG/Graph/BarHorizontal' + # + # fields = %w(Jan Feb Mar) + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::BarHorizontal.new({ + # :height => 500, + # :width => 300, + # :fields => fields, + # }) + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002', + # }) + # + # print "Content-type: image/svg+xml\r\n\r\n" + # print graph.burn + # + # = Description + # + # This object aims to allow you to easily create high quality + # SVG horitonzal bar graphs. You can either use the default style sheet + # or supply your own. Either way there are many options which can + # be configured to give you control over how the graph is + # generated - with or without a key, data elements at each point, + # title, subtitle etc. + # + # = Examples + # + # * http://germane-software.com/repositories/public/SVG/test/test.rb + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class BarHorizontal < BarBase + # In addition to the defaults set in BarBase::set_defaults, sets + # [rotate_y_labels] true + # [show_x_guidelines] true + # [show_y_guidelines] false + def set_defaults + super + init_with( + :rotate_y_labels => true, + :show_x_guidelines => true, + :show_y_guidelines => false + ) + self.right_align = self.right_font = 1 + end + + protected + + def get_x_labels + maxvalue = max_value + minvalue = min_value + range = maxvalue - minvalue + top_pad = range == 0 ? 10 : range / 20.0 + scale_range = (maxvalue + top_pad) - minvalue + + scale_division = scale_divisions || (scale_range / 10.0) + + if scale_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + rv = [] + maxvalue = maxvalue%scale_division == 0 ? + maxvalue : maxvalue + scale_division + minvalue.step( maxvalue, scale_division ) {|v| rv << v} + return rv + end + + def get_y_labels + @config[:fields] + end + + def y_label_offset( height ) + height / -2.0 + end + + def draw_data + minvalue = min_value + fieldheight = field_height + + unit_size = (@graph_width.to_f - font_size*2*right_font ) / + (get_x_labels.max - get_x_labels.min ) + bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0 + + bar_height = fieldheight - bargap + bar_height /= @data.length if stack == :side + y_mod = (bar_height / 2) + (font_size / 2) + + field_count = 1 + @config[:fields].each_index { |i| + dataset_count = 0 + for dataset in @data + value = dataset[:data][i] + + top = @graph_height - (fieldheight * field_count) + top += (bar_height * dataset_count) if stack == :side + # cases (assume 0 = +ve): + # value min length left + # +ve +ve value.abs - min minvalue.abs + # +ve -ve value.abs - 0 minvalue.abs + # -ve -ve value.abs - 0 minvalue.abs + value + length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size + left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size + + @graph.add_element( "rect", { + "x" => left.to_s, + "y" => top.to_s, + "width" => length.to_s, + "height" => bar_height.to_s, + "class" => "fill#{dataset_count+1}" + }) + + make_datapoint_text( + left+length+5, top+y_mod, value, "text-anchor: start; " + ) + dataset_count += 1 + end + field_count += 1 + } + end + end + end +end
--- a/lib/SVG/Graph/Graph.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/Graph.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,978 +1,978 @@ -begin - require 'zlib' - @@__have_zlib = true -rescue - @@__have_zlib = false -end - -require 'rexml/document' - -module SVG - module Graph - VERSION = '@ANT_VERSION@' - - # === Base object for generating SVG Graphs - # - # == Synopsis - # - # This class is only used as a superclass of specialized charts. Do not - # attempt to use this class directly, unless creating a new chart type. - # - # For examples of how to subclass this class, see the existing specific - # subclasses, such as SVG::Graph::Pie. - # - # == Examples - # - # For examples of how to use this package, see either the test files, or - # the documentation for the specific class you want to use. - # - # * file:test/plot.rb - # * file:test/single.rb - # * file:test/test.rb - # * file:test/timeseries.rb - # - # == Description - # - # This package should be used as a base for creating SVG graphs. - # - # == Acknowledgements - # - # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby - # port is based on. - # - # Stephen Morgan for creating the TT template and SVG. - # - # == See - # - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries - # - # == Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class Graph - include REXML - - # Initialize the graph object with the graph settings. You won't - # instantiate this class directly; see the subclass for options. - # [width] 500 - # [height] 300 - # [show_x_guidelines] false - # [show_y_guidelines] true - # [show_data_values] true - # [min_scale_value] 0 - # [show_x_labels] true - # [stagger_x_labels] false - # [rotate_x_labels] false - # [step_x_labels] 1 - # [step_include_first_x_label] true - # [show_y_labels] true - # [rotate_y_labels] false - # [scale_integers] false - # [show_x_title] false - # [x_title] 'X Field names' - # [show_y_title] false - # [y_title_text_direction] :bt - # [y_title] 'Y Scale' - # [show_graph_title] false - # [graph_title] 'Graph Title' - # [show_graph_subtitle] false - # [graph_subtitle] 'Graph Sub Title' - # [key] true, - # [key_position] :right, # bottom or righ - # [font_size] 12 - # [title_font_size] 16 - # [subtitle_font_size] 14 - # [x_label_font_size] 12 - # [x_title_font_size] 14 - # [y_label_font_size] 12 - # [y_title_font_size] 14 - # [key_font_size] 10 - # [no_css] false - # [add_popups] false - def initialize( config ) - @config = config - - self.top_align = self.top_font = self.right_align = self.right_font = 0 - - init_with({ - :width => 500, - :height => 300, - :show_x_guidelines => false, - :show_y_guidelines => true, - :show_data_values => true, - -# :min_scale_value => 0, - - :show_x_labels => true, - :stagger_x_labels => false, - :rotate_x_labels => false, - :step_x_labels => 1, - :step_include_first_x_label => true, - - :show_y_labels => true, - :rotate_y_labels => false, - :stagger_y_labels => false, - :scale_integers => false, - - :show_x_title => false, - :x_title => 'X Field names', - - :show_y_title => false, - :y_title_text_direction => :bt, - :y_title => 'Y Scale', - - :show_graph_title => false, - :graph_title => 'Graph Title', - :show_graph_subtitle => false, - :graph_subtitle => 'Graph Sub Title', - :key => true, - :key_position => :right, # bottom or right - - :font_size =>12, - :title_font_size =>16, - :subtitle_font_size =>14, - :x_label_font_size =>12, - :x_title_font_size =>14, - :y_label_font_size =>12, - :y_title_font_size =>14, - :key_font_size =>10, - - :no_css =>false, - :add_popups =>false, - }) - - set_defaults if respond_to? :set_defaults - - init_with config - end - - - # This method allows you do add data to the graph object. - # It can be called several times to add more data sets in. - # - # data_sales_02 = [12, 45, 21]; - # - # graph.add_data({ - # :data => data_sales_02, - # :title => 'Sales 2002' - # }) - def add_data conf - @data = [] unless defined? @data - - if conf[:data] and conf[:data].kind_of? Array - @data << conf - else - raise "No data provided by #{conf.inspect}" - end - end - - - # This method removes all data from the object so that you can - # reuse it to create a new graph but with the same config options. - # - # graph.clear_data - def clear_data - @data = [] - end - - - # This method processes the template with the data and - # config which has been set and returns the resulting SVG. - # - # This method will croak unless at least one data set has - # been added to the graph object. - # - # print graph.burn - def burn - raise "No data available" unless @data.size > 0 - - calculations if respond_to? :calculations - - start_svg - calculate_graph_dimensions - @foreground = Element.new( "g" ) - draw_graph - draw_titles - draw_legend - draw_data - @graph.add_element( @foreground ) - style - - data = "" - @doc.write( data, 0 ) - - if @config[:compress] - if @@__have_zlib - inp, out = IO.pipe - gz = Zlib::GzipWriter.new( out ) - gz.write data - gz.close - data = inp.read - else - data << "<!-- Ruby Zlib not available for SVGZ -->"; - end - end - - return data - end - - - # Set the height of the graph box, this is the total height - # of the SVG box created - not the graph it self which auto - # scales to fix the space. - attr_accessor :height - # Set the width of the graph box, this is the total width - # of the SVG box created - not the graph it self which auto - # scales to fix the space. - attr_accessor :width - # Set the path to an external stylesheet, set to '' if - # you want to revert back to using the defaut internal version. - # - # To create an external stylesheet create a graph using the - # default internal version and copy the stylesheet section to - # an external file and edit from there. - attr_accessor :style_sheet - # (Bool) Show the value of each element of data on the graph - attr_accessor :show_data_values - # The point at which the Y axis starts, defaults to '0', - # if set to nil it will default to the minimum data value. - attr_accessor :min_scale_value - # Whether to show labels on the X axis or not, defaults - # to true, set to false if you want to turn them off. - attr_accessor :show_x_labels - # This puts the X labels at alternative levels so if they - # are long field names they will not overlap so easily. - # Default it false, to turn on set to true. - attr_accessor :stagger_x_labels - # This puts the Y labels at alternative levels so if they - # are long field names they will not overlap so easily. - # Default it false, to turn on set to true. - attr_accessor :stagger_y_labels - # This turns the X axis labels by 90 degrees. - # Default it false, to turn on set to true. - attr_accessor :rotate_x_labels - # This turns the Y axis labels by 90 degrees. - # Default it false, to turn on set to true. - attr_accessor :rotate_y_labels - # How many "steps" to use between displayed X axis labels, - # a step of one means display every label, a step of two results - # in every other label being displayed (label <gap> label <gap> label), - # a step of three results in every third label being displayed - # (label <gap> <gap> label <gap> <gap> label) and so on. - attr_accessor :step_x_labels - # Whether to (when taking "steps" between X axis labels) step from - # the first label (i.e. always include the first label) or step from - # the X axis origin (i.e. start with a gap if step_x_labels is greater - # than one). - attr_accessor :step_include_first_x_label - # Whether to show labels on the Y axis or not, defaults - # to true, set to false if you want to turn them off. - attr_accessor :show_y_labels - # Ensures only whole numbers are used as the scale divisions. - # Default it false, to turn on set to true. This has no effect if - # scale divisions are less than 1. - attr_accessor :scale_integers - # This defines the gap between markers on the Y axis, - # default is a 10th of the max_value, e.g. you will have - # 10 markers on the Y axis. NOTE: do not set this too - # low - you are limited to 999 markers, after that the - # graph won't generate. - attr_accessor :scale_divisions - # Whether to show the title under the X axis labels, - # default is false, set to true to show. - attr_accessor :show_x_title - # What the title under X axis should be, e.g. 'Months'. - attr_accessor :x_title - # Whether to show the title under the Y axis labels, - # default is false, set to true to show. - attr_accessor :show_y_title - # Aligns writing mode for Y axis label. - # Defaults to :bt (Bottom to Top). - # Change to :tb (Top to Bottom) to reverse. - attr_accessor :y_title_text_direction - # What the title under Y axis should be, e.g. 'Sales in thousands'. - attr_accessor :y_title - # Whether to show a title on the graph, defaults - # to false, set to true to show. - attr_accessor :show_graph_title - # What the title on the graph should be. - attr_accessor :graph_title - # Whether to show a subtitle on the graph, defaults - # to false, set to true to show. - attr_accessor :show_graph_subtitle - # What the subtitle on the graph should be. - attr_accessor :graph_subtitle - # Whether to show a key, defaults to false, set to - # true if you want to show it. - attr_accessor :key - # Where the key should be positioned, defaults to - # :right, set to :bottom if you want to move it. - attr_accessor :key_position - # Set the font size (in points) of the data point labels - attr_accessor :font_size - # Set the font size of the X axis labels - attr_accessor :x_label_font_size - # Set the font size of the X axis title - attr_accessor :x_title_font_size - # Set the font size of the Y axis labels - attr_accessor :y_label_font_size - # Set the font size of the Y axis title - attr_accessor :y_title_font_size - # Set the title font size - attr_accessor :title_font_size - # Set the subtitle font size - attr_accessor :subtitle_font_size - # Set the key font size - attr_accessor :key_font_size - # Show guidelines for the X axis - attr_accessor :show_x_guidelines - # Show guidelines for the Y axis - attr_accessor :show_y_guidelines - # Do not use CSS if set to true. Many SVG viewers do not support CSS, but - # not using CSS can result in larger SVGs as well as making it impossible to - # change colors after the chart is generated. Defaults to false. - attr_accessor :no_css - # Add popups for the data points on some graphs - attr_accessor :add_popups - - - protected - - def sort( *arrys ) - sort_multiple( arrys ) - end - - # Overwrite configuration options with supplied options. Used - # by subclasses. - def init_with config - config.each { |key, value| - self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym - } - end - - attr_accessor :top_align, :top_font, :right_align, :right_font - - KEY_BOX_SIZE = 12 - - # Override this (and call super) to change the margin to the left - # of the plot area. Results in @border_left being set. - def calculate_left_margin - @border_left = 7 - # Check for Y labels - max_y_label_height_px = rotate_y_labels ? - y_label_font_size : - get_y_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * y_label_font_size * 0.6 - @border_left += max_y_label_height_px if show_y_labels - @border_left += max_y_label_height_px + 10 if stagger_y_labels - @border_left += y_title_font_size + 5 if show_y_title - end - - - # Calculates the width of the widest Y label. This will be the - # character height if the Y labels are rotated - def max_y_label_width_px - return font_size if rotate_y_labels - end - - - # Override this (and call super) to change the margin to the right - # of the plot area. Results in @border_right being set. - def calculate_right_margin - @border_right = 7 - if key and key_position == :right - val = keys.max { |a,b| a.length <=> b.length } - @border_right += val.length * key_font_size * 0.6 - @border_right += KEY_BOX_SIZE - @border_right += 10 # Some padding around the box - end - end - - - # Override this (and call super) to change the margin to the top - # of the plot area. Results in @border_top being set. - def calculate_top_margin - @border_top = 5 - @border_top += title_font_size if show_graph_title - @border_top += 5 - @border_top += subtitle_font_size if show_graph_subtitle - end - - - # Adds pop-up point information to a graph. - def add_popup( x, y, label ) - txt_width = label.length * font_size * 0.6 + 10 - tx = (x+txt_width > width ? x-5 : x+5) - t = @foreground.add_element( "text", { - "x" => tx.to_s, - "y" => (y - font_size).to_s, - "visibility" => "hidden", - }) - t.attributes["style"] = "fill: #000; "+ - (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") - t.text = label.to_s - t.attributes["id"] = t.object_id.to_s - - @foreground.add_element( "circle", { - "cx" => x.to_s, - "cy" => y.to_s, - "r" => "10", - "style" => "opacity: 0", - "onmouseover" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", - "onmouseout" => - "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", - }) - - end - - - # Override this (and call super) to change the margin to the bottom - # of the plot area. Results in @border_bottom being set. - def calculate_bottom_margin - @border_bottom = 7 - if key and key_position == :bottom - @border_bottom += @data.size * (font_size + 5) - @border_bottom += 10 - end - if show_x_labels - max_x_label_height_px = (not rotate_x_labels) ? - x_label_font_size : - get_x_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * x_label_font_size * 0.6 - @border_bottom += max_x_label_height_px - @border_bottom += max_x_label_height_px + 10 if stagger_x_labels - end - @border_bottom += x_title_font_size + 5 if show_x_title - end - - - # Draws the background, axis, and labels. - def draw_graph - @graph = @root.add_element( "g", { - "transform" => "translate( #@border_left #@border_top )" - }) - - # Background - @graph.add_element( "rect", { - "x" => "0", - "y" => "0", - "width" => @graph_width.to_s, - "height" => @graph_height.to_s, - "class" => "graphBackground" - }) - - # Axis - @graph.add_element( "path", { - "d" => "M 0 0 v#@graph_height", - "class" => "axis", - "id" => "xAxis" - }) - @graph.add_element( "path", { - "d" => "M 0 #@graph_height h#@graph_width", - "class" => "axis", - "id" => "yAxis" - }) - - draw_x_labels - draw_y_labels - end - - - # Where in the X area the label is drawn - # Centered in the field, should be width/2. Start, 0. - def x_label_offset( width ) - 0 - end - - def make_datapoint_text( x, y, value, style="" ) - if show_data_values - @foreground.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "dataPointLabel", - "style" => "#{style} stroke: #fff; stroke-width: 2;" - }).text = value.to_s - text = @foreground.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "dataPointLabel" - }) - text.text = value.to_s - text.attributes["style"] = style if style.length > 0 - end - end - - - # Draws the X axis labels - def draw_x_labels - stagger = x_label_font_size + 5 - if show_x_labels - label_width = field_width - - count = 0 - for label in get_x_labels - if step_include_first_x_label == true then - step = count % step_x_labels - else - step = (count + 1) % step_x_labels - end - - if step == 0 then - text = @graph.add_element( "text" ) - text.attributes["class"] = "xAxisLabels" - text.text = label.to_s - - x = count * label_width + x_label_offset( label_width ) - y = @graph_height + x_label_font_size + 3 - t = 0 - (font_size / 2) - - if stagger_x_labels and count % 2 == 1 - y += stagger - @graph.add_element( "path", { - "d" => "M#{x} #@graph_height v#{stagger}", - "class" => "staggerGuideLine" - }) - end - - text.attributes["x"] = x.to_s - text.attributes["y"] = y.to_s - if rotate_x_labels - text.attributes["transform"] = - "rotate( 90 #{x} #{y-x_label_font_size} )"+ - " translate( 0 -#{x_label_font_size/4} )" - text.attributes["style"] = "text-anchor: start" - else - text.attributes["style"] = "text-anchor: middle" - end - end - - draw_x_guidelines( label_width, count ) if show_x_guidelines - count += 1 - end - end - end - - - # Where in the Y area the label is drawn - # Centered in the field, should be width/2. Start, 0. - def y_label_offset( height ) - 0 - end - - - def field_width - (@graph_width.to_f - font_size*2*right_font) / - (get_x_labels.length - right_align) - end - - - def field_height - (@graph_height.to_f - font_size*2*top_font) / - (get_y_labels.length - top_align) - end - - - # Draws the Y axis labels - def draw_y_labels - stagger = y_label_font_size + 5 - if show_y_labels - label_height = field_height - - count = 0 - y_offset = @graph_height + y_label_offset( label_height ) - y_offset += font_size/1.2 unless rotate_y_labels - for label in get_y_labels - y = y_offset - (label_height * count) - x = rotate_y_labels ? 0 : -3 - - if stagger_y_labels and count % 2 == 1 - x -= stagger - @graph.add_element( "path", { - "d" => "M#{x} #{y} h#{stagger}", - "class" => "staggerGuideLine" - }) - end - - text = @graph.add_element( "text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "yAxisLabels" - }) - text.text = label.to_s - if rotate_y_labels - text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ - "rotate( 90 #{x} #{y} ) " - text.attributes["style"] = "text-anchor: middle" - else - text.attributes["y"] = (y - (y_label_font_size/2)).to_s - text.attributes["style"] = "text-anchor: end" - end - draw_y_guidelines( label_height, count ) if show_y_guidelines - count += 1 - end - end - end - - - # Draws the X axis guidelines - def draw_x_guidelines( label_height, count ) - if count != 0 - @graph.add_element( "path", { - "d" => "M#{label_height*count} 0 v#@graph_height", - "class" => "guideLines" - }) - end - end - - - # Draws the Y axis guidelines - def draw_y_guidelines( label_height, count ) - if count != 0 - @graph.add_element( "path", { - "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", - "class" => "guideLines" - }) - end - end - - - # Draws the graph title and subtitle - def draw_titles - if show_graph_title - @root.add_element( "text", { - "x" => (width / 2).to_s, - "y" => (title_font_size).to_s, - "class" => "mainTitle" - }).text = graph_title.to_s - end - - if show_graph_subtitle - y_subtitle = show_graph_title ? - title_font_size + 10 : - subtitle_font_size - @root.add_element("text", { - "x" => (width / 2).to_s, - "y" => (y_subtitle).to_s, - "class" => "subTitle" - }).text = graph_subtitle.to_s - end - - if show_x_title - y = @graph_height + @border_top + x_title_font_size - if show_x_labels - y += x_label_font_size + 5 if stagger_x_labels - y += x_label_font_size + 5 - end - x = width / 2 - - @root.add_element("text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "xAxisTitle", - }).text = x_title.to_s - end - - if show_y_title - x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) - y = height / 2 - - text = @root.add_element("text", { - "x" => x.to_s, - "y" => y.to_s, - "class" => "yAxisTitle", - }) - text.text = y_title.to_s - if y_title_text_direction == :bt - text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" - else - text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" - end - end - end - - def keys - return @data.collect{ |d| d[:title] } - end - - # Draws the legend on the graph - def draw_legend - if key - group = @root.add_element( "g" ) - - key_count = 0 - for key_name in keys - y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) - group.add_element( "rect", { - "x" => 0.to_s, - "y" => y_offset.to_s, - "width" => KEY_BOX_SIZE.to_s, - "height" => KEY_BOX_SIZE.to_s, - "class" => "key#{key_count+1}" - }) - group.add_element( "text", { - "x" => (KEY_BOX_SIZE + 5).to_s, - "y" => (y_offset + KEY_BOX_SIZE).to_s, - "class" => "keyText" - }).text = key_name.to_s - key_count += 1 - end - - case key_position - when :right - x_offset = @graph_width + @border_left + 10 - y_offset = @border_top + 20 - when :bottom - x_offset = @border_left + 20 - y_offset = @border_top + @graph_height + 5 - if show_x_labels - max_x_label_height_px = (not rotate_x_labels) ? - x_label_font_size : - get_x_labels.max{|a,b| - a.to_s.length<=>b.to_s.length - }.to_s.length * x_label_font_size * 0.6 - x_label_font_size - y_offset += max_x_label_height_px - y_offset += max_x_label_height_px + 5 if stagger_x_labels - end - y_offset += x_title_font_size + 5 if show_x_title - end - group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" - end - end - - - private - - def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) - if lo < hi - p = partition(arrys,lo,hi) - sort_multiple(arrys, lo, p-1) - sort_multiple(arrys, p+1, hi) - end - arrys - end - - def partition( arrys, lo, hi ) - p = arrys[0][lo] - l = lo - z = lo+1 - while z <= hi - if arrys[0][z] < p - l += 1 - arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } - end - z += 1 - end - arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } - l - end - - def style - if no_css - styles = parse_css - @root.elements.each("//*[@class]") { |el| - cl = el.attributes["class"] - style = styles[cl] - style += el.attributes["style"] if el.attributes["style"] - el.attributes["style"] = style - } - end - end - - def parse_css - css = get_style - rv = {} - while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m - names_orig = names = $1 - css = $' - css =~ /([^}]+)\}/m - content = $1 - css = $' - - nms = [] - while names =~ /^\s*,?\s*\.(\w+)/ - nms << $1 - names = $' - end - - content = content.tr( "\n\t", " ") - for name in nms - current = rv[name] - current = current ? current+"; "+content : content - rv[name] = current.strip.squeeze(" ") - end - end - return rv - end - - - # Override and place code to add defs here - def add_defs defs - end - - - def start_svg - # Base document - @doc = Document.new - @doc << XMLDecl.new - @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 << Instruction.new( "xml-stylesheet", - %Q{href="#{style_sheet}" type="text/css"} ) - end - @root = @doc.add_element( "svg", { - "width" => width.to_s, - "height" => height.to_s, - "viewBox" => "0 0 #{width} #{height}", - "xmlns" => "http://www.w3.org/2000/svg", - "xmlns:xlink" => "http://www.w3.org/1999/xlink", - "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", - "a3:scriptImplementation" => "Adobe" - }) - @root << Comment.new( " "+"\\"*66 ) - @root << Comment.new( " Created with SVG::Graph " ) - @root << Comment.new( " SVG::Graph by Sean E. Russell " ) - @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ - " Leo Lapworth & Stephan Morgan " ) - @root << Comment.new( " "+"/"*66 ) - - defs = @root.add_element( "defs" ) - add_defs defs - if not(style_sheet && style_sheet != '') and !no_css - @root << Comment.new(" include default stylesheet if none specified ") - style = defs.add_element( "style", {"type"=>"text/css"} ) - style << CData.new( get_style ) - end - - @root << Comment.new( "SVG Background" ) - @root.add_element( "rect", { - "width" => width.to_s, - "height" => height.to_s, - "x" => "0", - "y" => "0", - "class" => "svgBackground" - }) - end - - - def calculate_graph_dimensions - calculate_left_margin - calculate_right_margin - calculate_bottom_margin - calculate_top_margin - @graph_width = width - @border_left - @border_right - @graph_height = height - @border_top - @border_bottom - end - - def get_style - return <<EOL -/* Copy from here for external style sheet */ -.svgBackground{ - fill:#ffffff; -} -.graphBackground{ - fill:#f0f0f0; -} - -/* graphs titles */ -.mainTitle{ - text-anchor: middle; - fill: #000000; - font-size: #{title_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} -.subTitle{ - text-anchor: middle; - fill: #999999; - font-size: #{subtitle_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.axis{ - stroke: #000000; - stroke-width: 1px; -} - -.guideLines{ - stroke: #666666; - stroke-width: 1px; - stroke-dasharray: 5 5; -} - -.xAxisLabels{ - text-anchor: middle; - fill: #000000; - font-size: #{x_label_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.yAxisLabels{ - text-anchor: end; - fill: #000000; - font-size: #{y_label_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.xAxisTitle{ - text-anchor: middle; - fill: #ff0000; - font-size: #{x_title_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.yAxisTitle{ - fill: #ff0000; - text-anchor: middle; - font-size: #{y_title_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.dataPointLabel{ - fill: #000000; - text-anchor:middle; - font-size: 10px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -.staggerGuideLine{ - fill: none; - stroke: #000000; - stroke-width: 0.5px; -} - -#{get_css} - -.keyText{ - fill: #000000; - text-anchor:start; - font-size: #{key_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} -/* End copy for external style sheet */ -EOL - end - - end - end -end +begin + require 'zlib' + @@__have_zlib = true +rescue + @@__have_zlib = false +end + +require 'rexml/document' + +module SVG + module Graph + VERSION = '@ANT_VERSION@' + + # === Base object for generating SVG Graphs + # + # == Synopsis + # + # This class is only used as a superclass of specialized charts. Do not + # attempt to use this class directly, unless creating a new chart type. + # + # For examples of how to subclass this class, see the existing specific + # subclasses, such as SVG::Graph::Pie. + # + # == Examples + # + # For examples of how to use this package, see either the test files, or + # the documentation for the specific class you want to use. + # + # * file:test/plot.rb + # * file:test/single.rb + # * file:test/test.rb + # * file:test/timeseries.rb + # + # == Description + # + # This package should be used as a base for creating SVG graphs. + # + # == Acknowledgements + # + # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby + # port is based on. + # + # Stephen Morgan for creating the TT template and SVG. + # + # == See + # + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Graph + include REXML + + # Initialize the graph object with the graph settings. You won't + # instantiate this class directly; see the subclass for options. + # [width] 500 + # [height] 300 + # [show_x_guidelines] false + # [show_y_guidelines] true + # [show_data_values] true + # [min_scale_value] 0 + # [show_x_labels] true + # [stagger_x_labels] false + # [rotate_x_labels] false + # [step_x_labels] 1 + # [step_include_first_x_label] true + # [show_y_labels] true + # [rotate_y_labels] false + # [scale_integers] false + # [show_x_title] false + # [x_title] 'X Field names' + # [show_y_title] false + # [y_title_text_direction] :bt + # [y_title] 'Y Scale' + # [show_graph_title] false + # [graph_title] 'Graph Title' + # [show_graph_subtitle] false + # [graph_subtitle] 'Graph Sub Title' + # [key] true, + # [key_position] :right, # bottom or righ + # [font_size] 12 + # [title_font_size] 16 + # [subtitle_font_size] 14 + # [x_label_font_size] 12 + # [x_title_font_size] 14 + # [y_label_font_size] 12 + # [y_title_font_size] 14 + # [key_font_size] 10 + # [no_css] false + # [add_popups] false + def initialize( config ) + @config = config + + self.top_align = self.top_font = self.right_align = self.right_font = 0 + + init_with({ + :width => 500, + :height => 300, + :show_x_guidelines => false, + :show_y_guidelines => true, + :show_data_values => true, + +# :min_scale_value => 0, + + :show_x_labels => true, + :stagger_x_labels => false, + :rotate_x_labels => false, + :step_x_labels => 1, + :step_include_first_x_label => true, + + :show_y_labels => true, + :rotate_y_labels => false, + :stagger_y_labels => false, + :scale_integers => false, + + :show_x_title => false, + :x_title => 'X Field names', + + :show_y_title => false, + :y_title_text_direction => :bt, + :y_title => 'Y Scale', + + :show_graph_title => false, + :graph_title => 'Graph Title', + :show_graph_subtitle => false, + :graph_subtitle => 'Graph Sub Title', + :key => true, + :key_position => :right, # bottom or right + + :font_size =>12, + :title_font_size =>16, + :subtitle_font_size =>14, + :x_label_font_size =>12, + :x_title_font_size =>14, + :y_label_font_size =>12, + :y_title_font_size =>14, + :key_font_size =>10, + + :no_css =>false, + :add_popups =>false, + }) + + set_defaults if respond_to? :set_defaults + + init_with config + end + + + # This method allows you do add data to the graph object. + # It can be called several times to add more data sets in. + # + # data_sales_02 = [12, 45, 21]; + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002' + # }) + def add_data conf + @data = [] unless defined? @data + + if conf[:data] and conf[:data].kind_of? Array + @data << conf + else + raise "No data provided by #{conf.inspect}" + end + end + + + # This method removes all data from the object so that you can + # reuse it to create a new graph but with the same config options. + # + # graph.clear_data + def clear_data + @data = [] + end + + + # This method processes the template with the data and + # config which has been set and returns the resulting SVG. + # + # This method will croak unless at least one data set has + # been added to the graph object. + # + # print graph.burn + def burn + raise "No data available" unless @data.size > 0 + + calculations if respond_to? :calculations + + start_svg + calculate_graph_dimensions + @foreground = Element.new( "g" ) + draw_graph + draw_titles + draw_legend + draw_data + @graph.add_element( @foreground ) + style + + data = "" + @doc.write( data, 0 ) + + if @config[:compress] + if @@__have_zlib + inp, out = IO.pipe + gz = Zlib::GzipWriter.new( out ) + gz.write data + gz.close + data = inp.read + else + data << "<!-- Ruby Zlib not available for SVGZ -->"; + end + end + + return data + end + + + # Set the height of the graph box, this is the total height + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :height + # Set the width of the graph box, this is the total width + # of the SVG box created - not the graph it self which auto + # scales to fix the space. + attr_accessor :width + # Set the path to an external stylesheet, set to '' if + # you want to revert back to using the defaut internal version. + # + # To create an external stylesheet create a graph using the + # default internal version and copy the stylesheet section to + # an external file and edit from there. + attr_accessor :style_sheet + # (Bool) Show the value of each element of data on the graph + attr_accessor :show_data_values + # The point at which the Y axis starts, defaults to '0', + # if set to nil it will default to the minimum data value. + attr_accessor :min_scale_value + # Whether to show labels on the X axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_x_labels + # This puts the X labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_x_labels + # This puts the Y labels at alternative levels so if they + # are long field names they will not overlap so easily. + # Default it false, to turn on set to true. + attr_accessor :stagger_y_labels + # This turns the X axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_x_labels + # This turns the Y axis labels by 90 degrees. + # Default it false, to turn on set to true. + attr_accessor :rotate_y_labels + # How many "steps" to use between displayed X axis labels, + # a step of one means display every label, a step of two results + # in every other label being displayed (label <gap> label <gap> label), + # a step of three results in every third label being displayed + # (label <gap> <gap> label <gap> <gap> label) and so on. + attr_accessor :step_x_labels + # Whether to (when taking "steps" between X axis labels) step from + # the first label (i.e. always include the first label) or step from + # the X axis origin (i.e. start with a gap if step_x_labels is greater + # than one). + attr_accessor :step_include_first_x_label + # Whether to show labels on the Y axis or not, defaults + # to true, set to false if you want to turn them off. + attr_accessor :show_y_labels + # Ensures only whole numbers are used as the scale divisions. + # Default it false, to turn on set to true. This has no effect if + # scale divisions are less than 1. + attr_accessor :scale_integers + # This defines the gap between markers on the Y axis, + # default is a 10th of the max_value, e.g. you will have + # 10 markers on the Y axis. NOTE: do not set this too + # low - you are limited to 999 markers, after that the + # graph won't generate. + attr_accessor :scale_divisions + # Whether to show the title under the X axis labels, + # default is false, set to true to show. + attr_accessor :show_x_title + # What the title under X axis should be, e.g. 'Months'. + attr_accessor :x_title + # Whether to show the title under the Y axis labels, + # default is false, set to true to show. + attr_accessor :show_y_title + # Aligns writing mode for Y axis label. + # Defaults to :bt (Bottom to Top). + # Change to :tb (Top to Bottom) to reverse. + attr_accessor :y_title_text_direction + # What the title under Y axis should be, e.g. 'Sales in thousands'. + attr_accessor :y_title + # Whether to show a title on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_title + # What the title on the graph should be. + attr_accessor :graph_title + # Whether to show a subtitle on the graph, defaults + # to false, set to true to show. + attr_accessor :show_graph_subtitle + # What the subtitle on the graph should be. + attr_accessor :graph_subtitle + # Whether to show a key, defaults to false, set to + # true if you want to show it. + attr_accessor :key + # Where the key should be positioned, defaults to + # :right, set to :bottom if you want to move it. + attr_accessor :key_position + # Set the font size (in points) of the data point labels + attr_accessor :font_size + # Set the font size of the X axis labels + attr_accessor :x_label_font_size + # Set the font size of the X axis title + attr_accessor :x_title_font_size + # Set the font size of the Y axis labels + attr_accessor :y_label_font_size + # Set the font size of the Y axis title + attr_accessor :y_title_font_size + # Set the title font size + attr_accessor :title_font_size + # Set the subtitle font size + attr_accessor :subtitle_font_size + # Set the key font size + attr_accessor :key_font_size + # Show guidelines for the X axis + attr_accessor :show_x_guidelines + # Show guidelines for the Y axis + attr_accessor :show_y_guidelines + # Do not use CSS if set to true. Many SVG viewers do not support CSS, but + # not using CSS can result in larger SVGs as well as making it impossible to + # change colors after the chart is generated. Defaults to false. + attr_accessor :no_css + # Add popups for the data points on some graphs + attr_accessor :add_popups + + + protected + + def sort( *arrys ) + sort_multiple( arrys ) + end + + # Overwrite configuration options with supplied options. Used + # by subclasses. + def init_with config + config.each { |key, value| + self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym + } + end + + attr_accessor :top_align, :top_font, :right_align, :right_font + + KEY_BOX_SIZE = 12 + + # Override this (and call super) to change the margin to the left + # of the plot area. Results in @border_left being set. + def calculate_left_margin + @border_left = 7 + # Check for Y labels + max_y_label_height_px = rotate_y_labels ? + y_label_font_size : + get_y_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * y_label_font_size * 0.6 + @border_left += max_y_label_height_px if show_y_labels + @border_left += max_y_label_height_px + 10 if stagger_y_labels + @border_left += y_title_font_size + 5 if show_y_title + end + + + # Calculates the width of the widest Y label. This will be the + # character height if the Y labels are rotated + def max_y_label_width_px + return font_size if rotate_y_labels + end + + + # Override this (and call super) to change the margin to the right + # of the plot area. Results in @border_right being set. + def calculate_right_margin + @border_right = 7 + if key and key_position == :right + val = keys.max { |a,b| a.length <=> b.length } + @border_right += val.length * key_font_size * 0.6 + @border_right += KEY_BOX_SIZE + @border_right += 10 # Some padding around the box + end + end + + + # Override this (and call super) to change the margin to the top + # of the plot area. Results in @border_top being set. + def calculate_top_margin + @border_top = 5 + @border_top += title_font_size if show_graph_title + @border_top += 5 + @border_top += subtitle_font_size if show_graph_subtitle + end + + + # Adds pop-up point information to a graph. + def add_popup( x, y, label ) + txt_width = label.length * font_size * 0.6 + 10 + tx = (x+txt_width > width ? x-5 : x+5) + t = @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => (y - font_size).to_s, + "visibility" => "hidden", + }) + t.attributes["style"] = "fill: #000; "+ + (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;") + t.text = label.to_s + t.attributes["id"] = t.object_id.to_s + + @foreground.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "10", + "style" => "opacity: 0", + "onmouseover" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )", + "onmouseout" => + "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )", + }) + + end + + + # Override this (and call super) to change the margin to the bottom + # of the plot area. Results in @border_bottom being set. + def calculate_bottom_margin + @border_bottom = 7 + if key and key_position == :bottom + @border_bottom += @data.size * (font_size + 5) + @border_bottom += 10 + end + if show_x_labels + max_x_label_height_px = (not rotate_x_labels) ? + x_label_font_size : + get_x_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * x_label_font_size * 0.6 + @border_bottom += max_x_label_height_px + @border_bottom += max_x_label_height_px + 10 if stagger_x_labels + end + @border_bottom += x_title_font_size + 5 if show_x_title + end + + + # Draws the background, axis, and labels. + def draw_graph + @graph = @root.add_element( "g", { + "transform" => "translate( #@border_left #@border_top )" + }) + + # Background + @graph.add_element( "rect", { + "x" => "0", + "y" => "0", + "width" => @graph_width.to_s, + "height" => @graph_height.to_s, + "class" => "graphBackground" + }) + + # Axis + @graph.add_element( "path", { + "d" => "M 0 0 v#@graph_height", + "class" => "axis", + "id" => "xAxis" + }) + @graph.add_element( "path", { + "d" => "M 0 #@graph_height h#@graph_width", + "class" => "axis", + "id" => "yAxis" + }) + + draw_x_labels + draw_y_labels + end + + + # Where in the X area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def x_label_offset( width ) + 0 + end + + def make_datapoint_text( x, y, value, style="" ) + if show_data_values + @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel", + "style" => "#{style} stroke: #fff; stroke-width: 2;" + }).text = value.to_s + text = @foreground.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "dataPointLabel" + }) + text.text = value.to_s + text.attributes["style"] = style if style.length > 0 + end + end + + + # Draws the X axis labels + def draw_x_labels + stagger = x_label_font_size + 5 + if show_x_labels + label_width = field_width + + count = 0 + for label in get_x_labels + if step_include_first_x_label == true then + step = count % step_x_labels + else + step = (count + 1) % step_x_labels + end + + if step == 0 then + text = @graph.add_element( "text" ) + text.attributes["class"] = "xAxisLabels" + text.text = label.to_s + + x = count * label_width + x_label_offset( label_width ) + y = @graph_height + x_label_font_size + 3 + t = 0 - (font_size / 2) + + if stagger_x_labels and count % 2 == 1 + y += stagger + @graph.add_element( "path", { + "d" => "M#{x} #@graph_height v#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text.attributes["x"] = x.to_s + text.attributes["y"] = y.to_s + if rotate_x_labels + text.attributes["transform"] = + "rotate( 90 #{x} #{y-x_label_font_size} )"+ + " translate( 0 -#{x_label_font_size/4} )" + text.attributes["style"] = "text-anchor: start" + else + text.attributes["style"] = "text-anchor: middle" + end + end + + draw_x_guidelines( label_width, count ) if show_x_guidelines + count += 1 + end + end + end + + + # Where in the Y area the label is drawn + # Centered in the field, should be width/2. Start, 0. + def y_label_offset( height ) + 0 + end + + + def field_width + (@graph_width.to_f - font_size*2*right_font) / + (get_x_labels.length - right_align) + end + + + def field_height + (@graph_height.to_f - font_size*2*top_font) / + (get_y_labels.length - top_align) + end + + + # Draws the Y axis labels + def draw_y_labels + stagger = y_label_font_size + 5 + if show_y_labels + label_height = field_height + + count = 0 + y_offset = @graph_height + y_label_offset( label_height ) + y_offset += font_size/1.2 unless rotate_y_labels + for label in get_y_labels + y = y_offset - (label_height * count) + x = rotate_y_labels ? 0 : -3 + + if stagger_y_labels and count % 2 == 1 + x -= stagger + @graph.add_element( "path", { + "d" => "M#{x} #{y} h#{stagger}", + "class" => "staggerGuideLine" + }) + end + + text = @graph.add_element( "text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisLabels" + }) + text.text = label.to_s + if rotate_y_labels + text.attributes["transform"] = "translate( -#{font_size} 0 ) "+ + "rotate( 90 #{x} #{y} ) " + text.attributes["style"] = "text-anchor: middle" + else + text.attributes["y"] = (y - (y_label_font_size/2)).to_s + text.attributes["style"] = "text-anchor: end" + end + draw_y_guidelines( label_height, count ) if show_y_guidelines + count += 1 + end + end + end + + + # Draws the X axis guidelines + def draw_x_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M#{label_height*count} 0 v#@graph_height", + "class" => "guideLines" + }) + end + end + + + # Draws the Y axis guidelines + def draw_y_guidelines( label_height, count ) + if count != 0 + @graph.add_element( "path", { + "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width", + "class" => "guideLines" + }) + end + end + + + # Draws the graph title and subtitle + def draw_titles + if show_graph_title + @root.add_element( "text", { + "x" => (width / 2).to_s, + "y" => (title_font_size).to_s, + "class" => "mainTitle" + }).text = graph_title.to_s + end + + if show_graph_subtitle + y_subtitle = show_graph_title ? + title_font_size + 10 : + subtitle_font_size + @root.add_element("text", { + "x" => (width / 2).to_s, + "y" => (y_subtitle).to_s, + "class" => "subTitle" + }).text = graph_subtitle.to_s + end + + if show_x_title + y = @graph_height + @border_top + x_title_font_size + if show_x_labels + y += x_label_font_size + 5 if stagger_x_labels + y += x_label_font_size + 5 + end + x = width / 2 + + @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "xAxisTitle", + }).text = x_title.to_s + end + + if show_y_title + x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3) + y = height / 2 + + text = @root.add_element("text", { + "x" => x.to_s, + "y" => y.to_s, + "class" => "yAxisTitle", + }) + text.text = y_title.to_s + if y_title_text_direction == :bt + text.attributes["transform"] = "rotate( -90, #{x}, #{y} )" + else + text.attributes["transform"] = "rotate( 90, #{x}, #{y} )" + end + end + end + + def keys + return @data.collect{ |d| d[:title] } + end + + # Draws the legend on the graph + def draw_legend + if key + group = @root.add_element( "g" ) + + key_count = 0 + for key_name in keys + y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5) + group.add_element( "rect", { + "x" => 0.to_s, + "y" => y_offset.to_s, + "width" => KEY_BOX_SIZE.to_s, + "height" => KEY_BOX_SIZE.to_s, + "class" => "key#{key_count+1}" + }) + group.add_element( "text", { + "x" => (KEY_BOX_SIZE + 5).to_s, + "y" => (y_offset + KEY_BOX_SIZE).to_s, + "class" => "keyText" + }).text = key_name.to_s + key_count += 1 + end + + case key_position + when :right + x_offset = @graph_width + @border_left + 10 + y_offset = @border_top + 20 + when :bottom + x_offset = @border_left + 20 + y_offset = @border_top + @graph_height + 5 + if show_x_labels + max_x_label_height_px = (not rotate_x_labels) ? + x_label_font_size : + get_x_labels.max{|a,b| + a.to_s.length<=>b.to_s.length + }.to_s.length * x_label_font_size * 0.6 + x_label_font_size + y_offset += max_x_label_height_px + y_offset += max_x_label_height_px + 5 if stagger_x_labels + end + y_offset += x_title_font_size + 5 if show_x_title + end + group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" + end + end + + + private + + def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) + if lo < hi + p = partition(arrys,lo,hi) + sort_multiple(arrys, lo, p-1) + sort_multiple(arrys, p+1, hi) + end + arrys + end + + def partition( arrys, lo, hi ) + p = arrys[0][lo] + l = lo + z = lo+1 + while z <= hi + if arrys[0][z] < p + l += 1 + arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } + end + z += 1 + end + arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } + l + end + + def style + if no_css + styles = parse_css + @root.elements.each("//*[@class]") { |el| + cl = el.attributes["class"] + style = styles[cl] + style += el.attributes["style"] if el.attributes["style"] + el.attributes["style"] = style + } + end + end + + def parse_css + css = get_style + rv = {} + while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m + names_orig = names = $1 + css = $' + css =~ /([^}]+)\}/m + content = $1 + css = $' + + nms = [] + while names =~ /^\s*,?\s*\.(\w+)/ + nms << $1 + names = $' + end + + content = content.tr( "\n\t", " ") + for name in nms + current = rv[name] + current = current ? current+"; "+content : content + rv[name] = current.strip.squeeze(" ") + end + end + return rv + end + + + # Override and place code to add defs here + def add_defs defs + end + + + def start_svg + # Base document + @doc = Document.new + @doc << XMLDecl.new + @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 << Instruction.new( "xml-stylesheet", + %Q{href="#{style_sheet}" type="text/css"} ) + end + @root = @doc.add_element( "svg", { + "width" => width.to_s, + "height" => height.to_s, + "viewBox" => "0 0 #{width} #{height}", + "xmlns" => "http://www.w3.org/2000/svg", + "xmlns:xlink" => "http://www.w3.org/1999/xlink", + "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", + "a3:scriptImplementation" => "Adobe" + }) + @root << Comment.new( " "+"\\"*66 ) + @root << Comment.new( " Created with SVG::Graph " ) + @root << Comment.new( " SVG::Graph by Sean E. Russell " ) + @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ + " Leo Lapworth & Stephan Morgan " ) + @root << Comment.new( " "+"/"*66 ) + + defs = @root.add_element( "defs" ) + add_defs defs + if not(style_sheet && style_sheet != '') and !no_css + @root << Comment.new(" include default stylesheet if none specified ") + style = defs.add_element( "style", {"type"=>"text/css"} ) + style << CData.new( get_style ) + end + + @root << Comment.new( "SVG Background" ) + @root.add_element( "rect", { + "width" => width.to_s, + "height" => height.to_s, + "x" => "0", + "y" => "0", + "class" => "svgBackground" + }) + end + + + def calculate_graph_dimensions + calculate_left_margin + calculate_right_margin + calculate_bottom_margin + calculate_top_margin + @graph_width = width - @border_left - @border_right + @graph_height = height - @border_top - @border_bottom + end + + def get_style + return <<EOL +/* Copy from here for external style sheet */ +.svgBackground{ + fill:#ffffff; +} +.graphBackground{ + fill:#f0f0f0; +} + +/* graphs titles */ +.mainTitle{ + text-anchor: middle; + fill: #000000; + font-size: #{title_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} +.subTitle{ + text-anchor: middle; + fill: #999999; + font-size: #{subtitle_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.axis{ + stroke: #000000; + stroke-width: 1px; +} + +.guideLines{ + stroke: #666666; + stroke-width: 1px; + stroke-dasharray: 5 5; +} + +.xAxisLabels{ + text-anchor: middle; + fill: #000000; + font-size: #{x_label_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.yAxisLabels{ + text-anchor: end; + fill: #000000; + font-size: #{y_label_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.xAxisTitle{ + text-anchor: middle; + fill: #ff0000; + font-size: #{x_title_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.yAxisTitle{ + fill: #ff0000; + text-anchor: middle; + font-size: #{y_title_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.dataPointLabel{ + fill: #000000; + text-anchor:middle; + font-size: 10px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +.staggerGuideLine{ + fill: none; + stroke: #000000; + stroke-width: 0.5px; +} + +#{get_css} + +.keyText{ + fill: #000000; + text-anchor:start; + font-size: #{key_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} +/* End copy for external style sheet */ +EOL + end + + end + end +end
--- a/lib/SVG/Graph/Pie.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/Pie.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,395 +1,395 @@ -require 'SVG/Graph/Graph' - -module SVG - module Graph - # === Create presentation quality SVG pie graphs easily - # - # == Synopsis - # - # require 'SVG/Graph/Pie' - # - # fields = %w(Jan Feb Mar) - # data_sales_02 = [12, 45, 21] - # - # graph = SVG::Graph::Pie.new({ - # :height => 500, - # :width => 300, - # :fields => fields, - # }) - # - # graph.add_data({ - # :data => data_sales_02, - # :title => 'Sales 2002', - # }) - # - # print "Content-type: image/svg+xml\r\n\r\n" - # print graph.burn(); - # - # == Description - # - # This object aims to allow you to easily create high quality - # SVG pie graphs. You can either use the default style sheet - # or supply your own. Either way there are many options which can - # be configured to give you control over how the graph is - # generated - with or without a key, display percent on pie chart, - # title, subtitle etc. - # - # = Examples - # - # http://www.germane-software/repositories/public/SVG/test/single.rb - # - # == See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries - # - # == Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class Pie < Graph - # Defaults are those set by Graph::initialize, and - # [show_shadow] true - # [shadow_offset] 10 - # [show_data_labels] false - # [show_actual_values] false - # [show_percent] true - # [show_key_data_labels] true - # [show_key_actual_values] true - # [show_key_percent] false - # [expanded] false - # [expand_greatest] false - # [expand_gap] 10 - # [show_x_labels] false - # [show_y_labels] false - # [datapoint_font_size] 12 - def set_defaults - init_with( - :show_shadow => true, - :shadow_offset => 10, - - :show_data_labels => false, - :show_actual_values => false, - :show_percent => true, - - :show_key_data_labels => true, - :show_key_actual_values => true, - :show_key_percent => false, - - :expanded => false, - :expand_greatest => false, - :expand_gap => 10, - - :show_x_labels => false, - :show_y_labels => false, - :datapoint_font_size => 12 - ) - @data = [] - end - - # Adds a data set to the graph. - # - # graph.add_data( { :data => [1,2,3,4] } ) - # - # Note that the :title is not necessary. If multiple - # data sets are added to the graph, the pie chart will - # display the +sums+ of the data. EG: - # - # graph.add_data( { :data => [1,2,3,4] } ) - # graph.add_data( { :data => [2,3,5,9] } ) - # - # is the same as: - # - # graph.add_data( { :data => [3,5,8,13] } ) - def add_data arg - arg[:data].each_index {|idx| - @data[idx] = 0 unless @data[idx] - @data[idx] += arg[:data][idx] - } - end - - # If true, displays a drop shadow for the chart - attr_accessor :show_shadow - # Sets the offset of the shadow from the pie chart - attr_accessor :shadow_offset - # If true, display the data labels on the chart - attr_accessor :show_data_labels - # If true, display the actual field values in the data labels - attr_accessor :show_actual_values - # If true, display the percentage value of each pie wedge in the data - # labels - attr_accessor :show_percent - # If true, display the labels in the key - attr_accessor :show_key_data_labels - # If true, display the actual value of the field in the key - attr_accessor :show_key_actual_values - # If true, display the percentage value of the wedges in the key - attr_accessor :show_key_percent - # If true, "explode" the pie (put space between the wedges) - attr_accessor :expanded - # If true, expand the largest pie wedge - attr_accessor :expand_greatest - # The amount of space between expanded wedges - attr_accessor :expand_gap - # The font size of the data point labels - attr_accessor :datapoint_font_size - - - protected - - def add_defs defs - gradient = defs.add_element( "filter", { - "id"=>"dropshadow", - "width" => "1.2", - "height" => "1.2", - } ) - gradient.add_element( "feGaussianBlur", { - "stdDeviation" => "4", - "result" => "blur" - }) - end - - # We don't need the graph - def draw_graph - end - - def get_y_labels - [""] - end - - def get_x_labels - [""] - end - - def keys - total = 0 - max_value = 0 - @data.each {|x| total += x } - percent_scale = 100.0 / total - count = -1 - a = @config[:fields].collect{ |x| - count += 1 - v = @data[count] - perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" - x + " [" + v.to_s + "]" + perc - } - end - - RADIANS = Math::PI/180 - - def draw_data - @graph = @root.add_element( "g" ) - background = @graph.add_element("g") - midground = @graph.add_element("g") - - diameter = @graph_height > @graph_width ? @graph_width : @graph_height - diameter -= expand_gap if expanded or expand_greatest - diameter -= datapoint_font_size if show_data_labels - diameter -= 10 if show_shadow - radius = diameter / 2.0 - - xoff = (width - diameter) / 2 - yoff = (height - @border_bottom - diameter) - yoff -= 10 if show_shadow - @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" - - wedge_text_pad = 5 - wedge_text_pad = 20 if show_percent and show_data_labels - - total = 0 - max_value = 0 - @data.each {|x| - max_value = max_value < x ? x : max_value - total += x - } - percent_scale = 100.0 / total - - prev_percent = 0 - rad_mult = 3.6 * RADIANS - @config[:fields].each_index { |count| - value = @data[count] - percent = percent_scale * value - - radians = prev_percent * rad_mult - x_start = radius+(Math.sin(radians) * radius) - y_start = radius-(Math.cos(radians) * radius) - radians = (prev_percent+percent) * rad_mult - x_end = radius+(Math.sin(radians) * radius) - x_end -= 0.00001 if @data.length == 1 - y_end = radius-(Math.cos(radians) * radius) - path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ - "A#{radius},#{radius} "+ - "0, #{percent >= 50 ? '1' : '0'},1, "+ - "#{x_end} #{y_end} Z" - - - wedge = @foreground.add_element( "path", { - "d" => path, - "class" => "fill#{count+1}" - }) - - translate = nil - tx = 0 - ty = 0 - half_percent = prev_percent + percent / 2 - radians = half_percent * rad_mult - - if show_shadow - shadow = background.add_element( "path", { - "d" => path, - "filter" => "url(#dropshadow)", - "style" => "fill: #ccc; stroke: none;" - }) - clear = midground.add_element( "path", { - "d" => path, - "style" => "fill: #fff; stroke: none;" - }) - end - - if expanded or (expand_greatest && value == max_value) - tx = (Math.sin(radians) * expand_gap) - ty = -(Math.cos(radians) * expand_gap) - translate = "translate( #{tx} #{ty} )" - wedge.attributes["transform"] = translate - clear.attributes["transform"] = translate if clear - end - - if show_shadow - shadow.attributes["transform"] = - "translate( #{tx+shadow_offset} #{ty+shadow_offset} )" - end - - if show_data_labels and value != 0 - label = "" - label += @config[:fields][count] if show_key_data_labels - label += " ["+value.to_s+"]" if show_actual_values - label += " "+percent.round.to_s+"%" if show_percent - - msr = Math.sin(radians) - mcr = Math.cos(radians) - tx = radius + (msr * radius) - ty = radius -(mcr * radius) - - if expanded or (expand_greatest && value == max_value) - tx += (msr * expand_gap) - ty -= (mcr * expand_gap) - end - @foreground.add_element( "text", { - "x" => tx.to_s, - "y" => ty.to_s, - "class" => "dataPointLabel", - "style" => "stroke: #fff; stroke-width: 2;" - }).text = label.to_s - @foreground.add_element( "text", { - "x" => tx.to_s, - "y" => ty.to_s, - "class" => "dataPointLabel", - }).text = label.to_s - end - - prev_percent += percent - } - end - - - def round val, to - up = 10**to.to_f - (val * up).to_i / up - end - - - def get_css - return <<EOL -.dataPointLabel{ - fill: #000000; - text-anchor:middle; - font-size: #{datapoint_font_size}px; - font-family: "Arial", sans-serif; - font-weight: normal; -} - -/* key - MUST match fill styles */ -.key1,.fill1{ - fill: #ff0000; - fill-opacity: 0.7; - stroke: none; - stroke-width: 1px; -} -.key2,.fill2{ - fill: #0000ff; - fill-opacity: 0.7; - stroke: none; - stroke-width: 1px; -} -.key3,.fill3{ - fill-opacity: 0.7; - fill: #00ff00; - stroke: none; - stroke-width: 1px; -} -.key4,.fill4{ - fill-opacity: 0.7; - fill: #ffcc00; - stroke: none; - stroke-width: 1px; -} -.key5,.fill5{ - fill-opacity: 0.7; - fill: #00ccff; - stroke: none; - stroke-width: 1px; -} -.key6,.fill6{ - fill-opacity: 0.7; - fill: #ff00ff; - stroke: none; - stroke-width: 1px; -} -.key7,.fill7{ - fill-opacity: 0.7; - fill: #00ff99; - stroke: none; - stroke-width: 1px; -} -.key8,.fill8{ - fill-opacity: 0.7; - fill: #ffff00; - stroke: none; - stroke-width: 1px; -} -.key9,.fill9{ - fill-opacity: 0.7; - fill: #cc6666; - stroke: none; - stroke-width: 1px; -} -.key10,.fill10{ - fill-opacity: 0.7; - fill: #663399; - stroke: none; - stroke-width: 1px; -} -.key11,.fill11{ - fill-opacity: 0.7; - fill: #339900; - stroke: none; - stroke-width: 1px; -} -.key12,.fill12{ - fill-opacity: 0.7; - fill: #9966FF; - stroke: none; - stroke-width: 1px; -} -EOL - end - end - end -end +require 'SVG/Graph/Graph' + +module SVG + module Graph + # === Create presentation quality SVG pie graphs easily + # + # == Synopsis + # + # require 'SVG/Graph/Pie' + # + # fields = %w(Jan Feb Mar) + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::Pie.new({ + # :height => 500, + # :width => 300, + # :fields => fields, + # }) + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002', + # }) + # + # print "Content-type: image/svg+xml\r\n\r\n" + # print graph.burn(); + # + # == Description + # + # This object aims to allow you to easily create high quality + # SVG pie graphs. You can either use the default style sheet + # or supply your own. Either way there are many options which can + # be configured to give you control over how the graph is + # generated - with or without a key, display percent on pie chart, + # title, subtitle etc. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/single.rb + # + # == See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Pie < Graph + # Defaults are those set by Graph::initialize, and + # [show_shadow] true + # [shadow_offset] 10 + # [show_data_labels] false + # [show_actual_values] false + # [show_percent] true + # [show_key_data_labels] true + # [show_key_actual_values] true + # [show_key_percent] false + # [expanded] false + # [expand_greatest] false + # [expand_gap] 10 + # [show_x_labels] false + # [show_y_labels] false + # [datapoint_font_size] 12 + def set_defaults + init_with( + :show_shadow => true, + :shadow_offset => 10, + + :show_data_labels => false, + :show_actual_values => false, + :show_percent => true, + + :show_key_data_labels => true, + :show_key_actual_values => true, + :show_key_percent => false, + + :expanded => false, + :expand_greatest => false, + :expand_gap => 10, + + :show_x_labels => false, + :show_y_labels => false, + :datapoint_font_size => 12 + ) + @data = [] + end + + # Adds a data set to the graph. + # + # graph.add_data( { :data => [1,2,3,4] } ) + # + # Note that the :title is not necessary. If multiple + # data sets are added to the graph, the pie chart will + # display the +sums+ of the data. EG: + # + # graph.add_data( { :data => [1,2,3,4] } ) + # graph.add_data( { :data => [2,3,5,9] } ) + # + # is the same as: + # + # graph.add_data( { :data => [3,5,8,13] } ) + def add_data arg + arg[:data].each_index {|idx| + @data[idx] = 0 unless @data[idx] + @data[idx] += arg[:data][idx] + } + end + + # If true, displays a drop shadow for the chart + attr_accessor :show_shadow + # Sets the offset of the shadow from the pie chart + attr_accessor :shadow_offset + # If true, display the data labels on the chart + attr_accessor :show_data_labels + # If true, display the actual field values in the data labels + attr_accessor :show_actual_values + # If true, display the percentage value of each pie wedge in the data + # labels + attr_accessor :show_percent + # If true, display the labels in the key + attr_accessor :show_key_data_labels + # If true, display the actual value of the field in the key + attr_accessor :show_key_actual_values + # If true, display the percentage value of the wedges in the key + attr_accessor :show_key_percent + # If true, "explode" the pie (put space between the wedges) + attr_accessor :expanded + # If true, expand the largest pie wedge + attr_accessor :expand_greatest + # The amount of space between expanded wedges + attr_accessor :expand_gap + # The font size of the data point labels + attr_accessor :datapoint_font_size + + + protected + + def add_defs defs + gradient = defs.add_element( "filter", { + "id"=>"dropshadow", + "width" => "1.2", + "height" => "1.2", + } ) + gradient.add_element( "feGaussianBlur", { + "stdDeviation" => "4", + "result" => "blur" + }) + end + + # We don't need the graph + def draw_graph + end + + def get_y_labels + [""] + end + + def get_x_labels + [""] + end + + def keys + total = 0 + max_value = 0 + @data.each {|x| total += x } + percent_scale = 100.0 / total + count = -1 + a = @config[:fields].collect{ |x| + count += 1 + v = @data[count] + perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" + x + " [" + v.to_s + "]" + perc + } + end + + RADIANS = Math::PI/180 + + def draw_data + @graph = @root.add_element( "g" ) + background = @graph.add_element("g") + midground = @graph.add_element("g") + + diameter = @graph_height > @graph_width ? @graph_width : @graph_height + diameter -= expand_gap if expanded or expand_greatest + diameter -= datapoint_font_size if show_data_labels + diameter -= 10 if show_shadow + radius = diameter / 2.0 + + xoff = (width - diameter) / 2 + yoff = (height - @border_bottom - diameter) + yoff -= 10 if show_shadow + @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" + + wedge_text_pad = 5 + wedge_text_pad = 20 if show_percent and show_data_labels + + total = 0 + max_value = 0 + @data.each {|x| + max_value = max_value < x ? x : max_value + total += x + } + percent_scale = 100.0 / total + + prev_percent = 0 + rad_mult = 3.6 * RADIANS + @config[:fields].each_index { |count| + value = @data[count] + percent = percent_scale * value + + radians = prev_percent * rad_mult + x_start = radius+(Math.sin(radians) * radius) + y_start = radius-(Math.cos(radians) * radius) + radians = (prev_percent+percent) * rad_mult + x_end = radius+(Math.sin(radians) * radius) + x_end -= 0.00001 if @data.length == 1 + y_end = radius-(Math.cos(radians) * radius) + path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ + "A#{radius},#{radius} "+ + "0, #{percent >= 50 ? '1' : '0'},1, "+ + "#{x_end} #{y_end} Z" + + + wedge = @foreground.add_element( "path", { + "d" => path, + "class" => "fill#{count+1}" + }) + + translate = nil + tx = 0 + ty = 0 + half_percent = prev_percent + percent / 2 + radians = half_percent * rad_mult + + if show_shadow + shadow = background.add_element( "path", { + "d" => path, + "filter" => "url(#dropshadow)", + "style" => "fill: #ccc; stroke: none;" + }) + clear = midground.add_element( "path", { + "d" => path, + "style" => "fill: #fff; stroke: none;" + }) + end + + if expanded or (expand_greatest && value == max_value) + tx = (Math.sin(radians) * expand_gap) + ty = -(Math.cos(radians) * expand_gap) + translate = "translate( #{tx} #{ty} )" + wedge.attributes["transform"] = translate + clear.attributes["transform"] = translate if clear + end + + if show_shadow + shadow.attributes["transform"] = + "translate( #{tx+shadow_offset} #{ty+shadow_offset} )" + end + + if show_data_labels and value != 0 + label = "" + label += @config[:fields][count] if show_key_data_labels + label += " ["+value.to_s+"]" if show_actual_values + label += " "+percent.round.to_s+"%" if show_percent + + msr = Math.sin(radians) + mcr = Math.cos(radians) + tx = radius + (msr * radius) + ty = radius -(mcr * radius) + + if expanded or (expand_greatest && value == max_value) + tx += (msr * expand_gap) + ty -= (mcr * expand_gap) + end + @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => ty.to_s, + "class" => "dataPointLabel", + "style" => "stroke: #fff; stroke-width: 2;" + }).text = label.to_s + @foreground.add_element( "text", { + "x" => tx.to_s, + "y" => ty.to_s, + "class" => "dataPointLabel", + }).text = label.to_s + end + + prev_percent += percent + } + end + + + def round val, to + up = 10**to.to_f + (val * up).to_i / up + end + + + def get_css + return <<EOL +.dataPointLabel{ + fill: #000000; + text-anchor:middle; + font-size: #{datapoint_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +/* key - MUST match fill styles */ +.key1,.fill1{ + fill: #ff0000; + fill-opacity: 0.7; + stroke: none; + stroke-width: 1px; +} +.key2,.fill2{ + fill: #0000ff; + fill-opacity: 0.7; + stroke: none; + stroke-width: 1px; +} +.key3,.fill3{ + fill-opacity: 0.7; + fill: #00ff00; + stroke: none; + stroke-width: 1px; +} +.key4,.fill4{ + fill-opacity: 0.7; + fill: #ffcc00; + stroke: none; + stroke-width: 1px; +} +.key5,.fill5{ + fill-opacity: 0.7; + fill: #00ccff; + stroke: none; + stroke-width: 1px; +} +.key6,.fill6{ + fill-opacity: 0.7; + fill: #ff00ff; + stroke: none; + stroke-width: 1px; +} +.key7,.fill7{ + fill-opacity: 0.7; + fill: #00ff99; + stroke: none; + stroke-width: 1px; +} +.key8,.fill8{ + fill-opacity: 0.7; + fill: #ffff00; + stroke: none; + stroke-width: 1px; +} +.key9,.fill9{ + fill-opacity: 0.7; + fill: #cc6666; + stroke: none; + stroke-width: 1px; +} +.key10,.fill10{ + fill-opacity: 0.7; + fill: #663399; + stroke: none; + stroke-width: 1px; +} +.key11,.fill11{ + fill-opacity: 0.7; + fill: #339900; + stroke: none; + stroke-width: 1px; +} +.key12,.fill12{ + fill-opacity: 0.7; + fill: #9966FF; + stroke: none; + stroke-width: 1px; +} +EOL + end + end + end +end
--- a/lib/SVG/Graph/Plot.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/Plot.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,500 +1,500 @@ -require 'SVG/Graph/Graph' - -module SVG - module Graph - # === For creating SVG plots of scalar data - # - # = Synopsis - # - # require 'SVG/Graph/Plot' - # - # # Data sets are x,y pairs - # # Note that multiple data sets can differ in length, and that the - # # data in the datasets needn't be in order; they will be ordered - # # by the plot along the X-axis. - # projection = [ - # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, - # 7, 9 - # ] - # actual = [ - # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, - # 15, 6, 4, 17, 2, 12 - # ] - # - # graph = SVG::Graph::Plot.new({ - # :height => 500, - # :width => 300, - # :key => true, - # :scale_x_integers => true, - # :scale_y_integerrs => true, - # }) - # - # graph.add_data({ - # :data => projection - # :title => 'Projected', - # }) - # - # graph.add_data({ - # :data => actual, - # :title => 'Actual', - # }) - # - # print graph.burn() - # - # = Description - # - # Produces a graph of scalar data. - # - # This object aims to allow you to easily create high quality - # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the - # default style sheet or supply your own. Either way there are many options - # which can be configured to give you control over how the graph is - # generated - with or without a key, data elements at each point, title, - # subtitle etc. - # - # = Examples - # - # http://www.germane-software/repositories/public/SVG/test/plot.rb - # - # = Notes - # - # The default stylesheet handles upto 10 data sets, if you - # use more you must create your own stylesheet and add the - # additional settings for the extra data sets. You will know - # if you go over 10 data sets as they will have no style and - # be in black. - # - # Unlike the other types of charts, data sets must contain x,y pairs: - # - # [ 1, 2 ] # A data set with 1 point: (1,2) - # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::TimeSeries - # - # == Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class Plot < Graph - - # In addition to the defaults set by Graph::initialize, sets - # [show_data_values] true - # [show_data_points] true - # [area_fill] false - # [stacked] false - def set_defaults - init_with( - :show_data_values => true, - :show_data_points => true, - :area_fill => false, - :stacked => false - ) - self.top_align = self.right_align = self.top_font = self.right_font = 1 - end - - # Determines the scaling for the X axis divisions. - # - # graph.scale_x_divisions = 2 - # - # would cause the graph to attempt to generate labels stepped by 2; EG: - # 0,2,4,6,8... - attr_accessor :scale_x_divisions - # Determines the scaling for the Y axis divisions. - # - # graph.scale_y_divisions = 0.5 - # - # would cause the graph to attempt to generate labels stepped by 0.5; EG: - # 0, 0.5, 1, 1.5, 2, ... - attr_accessor :scale_y_divisions - # Make the X axis labels integers - attr_accessor :scale_x_integers - # Make the Y axis labels integers - attr_accessor :scale_y_integers - # Fill the area under the line - attr_accessor :area_fill - # Show a small circle on the graph where the line - # goes from one point to the next. - attr_accessor :show_data_points - # Set the minimum value of the X axis - attr_accessor :min_x_value - # Set the minimum value of the Y axis - attr_accessor :min_y_value - - - # Adds data to the plot. The data must be in X,Y pairs; EG - # [ 1, 2 ] # A data set with 1 point: (1,2) - # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) - def add_data data - @data = [] unless @data - - raise "No data provided by #{conf.inspect}" unless data[:data] and - data[:data].kind_of? Array - raise "Data supplied must be x,y pairs! "+ - "The data provided contained an odd set of "+ - "data points" unless data[:data].length % 2 == 0 - return if data[:data].length == 0 - - x = [] - y = [] - data[:data].each_index {|i| - (i%2 == 0 ? x : y) << data[:data][i] - } - sort( x, y ) - data[:data] = [x,y] - @data << data - end - - - protected - - def keys - @data.collect{ |x| x[:title] } - end - - def calculate_left_margin - super - label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 - @border_left = label_left if label_left > @border_left - end - - def calculate_right_margin - super - label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 - @border_right = label_right if label_right > @border_right - end - - - X = 0 - Y = 1 - def x_range - max_value = @data.collect{|x| x[:data][X][-1] }.max - min_value = @data.collect{|x| x[:data][X][0] }.min - min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value - - range = max_value - min_value - right_pad = range == 0 ? 10 : range / 20.0 - scale_range = (max_value + right_pad) - min_value - - scale_division = scale_x_divisions || (scale_range / 10.0) - - if scale_x_integers - scale_division = scale_division < 1 ? 1 : scale_division.round - end - - [min_value, max_value, scale_division] - end - - def get_x_values - min_value, max_value, scale_division = x_range - rv = [] - min_value.step( max_value, scale_division ) {|v| rv << v} - return rv - end - alias :get_x_labels :get_x_values - - def field_width - values = get_x_values - max = @data.collect{|x| x[:data][X][-1]}.max - dx = (max - values[-1]).to_f / (values[-1] - values[-2]) - (@graph_width.to_f - font_size*2*right_font) / - (values.length + dx - right_align) - end - - - def y_range - max_value = @data.collect{|x| x[:data][Y].max }.max - min_value = @data.collect{|x| x[:data][Y].min }.min - min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value - - range = max_value - min_value - top_pad = range == 0 ? 10 : range / 20.0 - scale_range = (max_value + top_pad) - min_value - - scale_division = scale_y_divisions || (scale_range / 10.0) - - if scale_y_integers - scale_division = scale_division < 1 ? 1 : scale_division.round - end - - return [min_value, max_value, scale_division] - end - - def get_y_values - min_value, max_value, scale_division = y_range - rv = [] - min_value.step( max_value, scale_division ) {|v| rv << v} - return rv - end - alias :get_y_labels :get_y_values - - def field_height - values = get_y_values - max = @data.collect{|x| x[:data][Y].max }.max - if values.length == 1 - dx = values[-1] - else - dx = (max - values[-1]).to_f / (values[-1] - values[-2]) - end - (@graph_height.to_f - font_size*2*top_font) / - (values.length + dx - top_align) - end - - def draw_data - line = 1 - - x_min, x_max, x_div = x_range - y_min, y_max, y_div = y_range - x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min) - y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min) - - for data in @data - x_points = data[:data][X] - y_points = data[:data][Y] - - lpath = "L" - x_start = 0 - y_start = 0 - x_points.each_index { |idx| - x = (x_points[idx] - x_min) * x_step - y = @graph_height - (y_points[idx] - y_min) * y_step - x_start, y_start = x,y if idx == 0 - lpath << "#{x} #{y} " - } - - if area_fill - @graph.add_element( "path", { - "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", - "class" => "fill#{line}" - }) - end - - @graph.add_element( "path", { - "d" => "M#{x_start} #{y_start} #{lpath}", - "class" => "line#{line}" - }) - - if show_data_points || show_data_values - x_points.each_index { |idx| - x = (x_points[idx] - x_min) * x_step - y = @graph_height - (y_points[idx] - y_min) * y_step - if show_data_points - @graph.add_element( "circle", { - "cx" => x.to_s, - "cy" => y.to_s, - "r" => "2.5", - "class" => "dataPoint#{line}" - }) - add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups - end - make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values - } - end - line += 1 - end - end - - def format x, y - "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})" - end - - def get_css - return <<EOL -/* default line styles */ -.line1{ - fill: none; - stroke: #ff0000; - stroke-width: 1px; -} -.line2{ - fill: none; - stroke: #0000ff; - stroke-width: 1px; -} -.line3{ - fill: none; - stroke: #00ff00; - stroke-width: 1px; -} -.line4{ - fill: none; - stroke: #ffcc00; - stroke-width: 1px; -} -.line5{ - fill: none; - stroke: #00ccff; - stroke-width: 1px; -} -.line6{ - fill: none; - stroke: #ff00ff; - stroke-width: 1px; -} -.line7{ - fill: none; - stroke: #00ffff; - stroke-width: 1px; -} -.line8{ - fill: none; - stroke: #ffff00; - stroke-width: 1px; -} -.line9{ - fill: none; - stroke: #ccc6666; - stroke-width: 1px; -} -.line10{ - fill: none; - stroke: #663399; - stroke-width: 1px; -} -.line11{ - fill: none; - stroke: #339900; - stroke-width: 1px; -} -.line12{ - fill: none; - stroke: #9966FF; - stroke-width: 1px; -} -/* default fill styles */ -.fill1{ - fill: #cc0000; - fill-opacity: 0.2; - stroke: none; -} -.fill2{ - fill: #0000cc; - fill-opacity: 0.2; - stroke: none; -} -.fill3{ - fill: #00cc00; - fill-opacity: 0.2; - stroke: none; -} -.fill4{ - fill: #ffcc00; - fill-opacity: 0.2; - stroke: none; -} -.fill5{ - fill: #00ccff; - fill-opacity: 0.2; - stroke: none; -} -.fill6{ - fill: #ff00ff; - fill-opacity: 0.2; - stroke: none; -} -.fill7{ - fill: #00ffff; - fill-opacity: 0.2; - stroke: none; -} -.fill8{ - fill: #ffff00; - fill-opacity: 0.2; - stroke: none; -} -.fill9{ - fill: #cc6666; - fill-opacity: 0.2; - stroke: none; -} -.fill10{ - fill: #663399; - fill-opacity: 0.2; - stroke: none; -} -.fill11{ - fill: #339900; - fill-opacity: 0.2; - stroke: none; -} -.fill12{ - fill: #9966FF; - fill-opacity: 0.2; - stroke: none; -} -/* default line styles */ -.key1,.dataPoint1{ - fill: #ff0000; - stroke: none; - stroke-width: 1px; -} -.key2,.dataPoint2{ - fill: #0000ff; - stroke: none; - stroke-width: 1px; -} -.key3,.dataPoint3{ - fill: #00ff00; - stroke: none; - stroke-width: 1px; -} -.key4,.dataPoint4{ - fill: #ffcc00; - stroke: none; - stroke-width: 1px; -} -.key5,.dataPoint5{ - fill: #00ccff; - stroke: none; - stroke-width: 1px; -} -.key6,.dataPoint6{ - fill: #ff00ff; - stroke: none; - stroke-width: 1px; -} -.key7,.dataPoint7{ - fill: #00ffff; - stroke: none; - stroke-width: 1px; -} -.key8,.dataPoint8{ - fill: #ffff00; - stroke: none; - stroke-width: 1px; -} -.key9,.dataPoint9{ - fill: #cc6666; - stroke: none; - stroke-width: 1px; -} -.key10,.dataPoint10{ - fill: #663399; - stroke: none; - stroke-width: 1px; -} -.key11,.dataPoint11{ - fill: #339900; - stroke: none; - stroke-width: 1px; -} -.key12,.dataPoint12{ - fill: #9966FF; - stroke: none; - stroke-width: 1px; -} -EOL - end - - end - end -end +require 'SVG/Graph/Graph' + +module SVG + module Graph + # === For creating SVG plots of scalar data + # + # = Synopsis + # + # require 'SVG/Graph/Plot' + # + # # Data sets are x,y pairs + # # Note that multiple data sets can differ in length, and that the + # # data in the datasets needn't be in order; they will be ordered + # # by the plot along the X-axis. + # projection = [ + # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, + # 7, 9 + # ] + # actual = [ + # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, + # 15, 6, 4, 17, 2, 12 + # ] + # + # graph = SVG::Graph::Plot.new({ + # :height => 500, + # :width => 300, + # :key => true, + # :scale_x_integers => true, + # :scale_y_integerrs => true, + # }) + # + # graph.add_data({ + # :data => projection + # :title => 'Projected', + # }) + # + # graph.add_data({ + # :data => actual, + # :title => 'Actual', + # }) + # + # print graph.burn() + # + # = Description + # + # Produces a graph of scalar data. + # + # This object aims to allow you to easily create high quality + # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the + # default style sheet or supply your own. Either way there are many options + # which can be configured to give you control over how the graph is + # generated - with or without a key, data elements at each point, title, + # subtitle etc. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/plot.rb + # + # = Notes + # + # The default stylesheet handles upto 10 data sets, if you + # use more you must create your own stylesheet and add the + # additional settings for the extra data sets. You will know + # if you go over 10 data sets as they will have no style and + # be in black. + # + # Unlike the other types of charts, data sets must contain x,y pairs: + # + # [ 1, 2 ] # A data set with 1 point: (1,2) + # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Plot < Graph + + # In addition to the defaults set by Graph::initialize, sets + # [show_data_values] true + # [show_data_points] true + # [area_fill] false + # [stacked] false + def set_defaults + init_with( + :show_data_values => true, + :show_data_points => true, + :area_fill => false, + :stacked => false + ) + self.top_align = self.right_align = self.top_font = self.right_font = 1 + end + + # Determines the scaling for the X axis divisions. + # + # graph.scale_x_divisions = 2 + # + # would cause the graph to attempt to generate labels stepped by 2; EG: + # 0,2,4,6,8... + attr_accessor :scale_x_divisions + # Determines the scaling for the Y axis divisions. + # + # graph.scale_y_divisions = 0.5 + # + # would cause the graph to attempt to generate labels stepped by 0.5; EG: + # 0, 0.5, 1, 1.5, 2, ... + attr_accessor :scale_y_divisions + # Make the X axis labels integers + attr_accessor :scale_x_integers + # Make the Y axis labels integers + attr_accessor :scale_y_integers + # Fill the area under the line + attr_accessor :area_fill + # Show a small circle on the graph where the line + # goes from one point to the next. + attr_accessor :show_data_points + # Set the minimum value of the X axis + attr_accessor :min_x_value + # Set the minimum value of the Y axis + attr_accessor :min_y_value + + + # Adds data to the plot. The data must be in X,Y pairs; EG + # [ 1, 2 ] # A data set with 1 point: (1,2) + # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) + def add_data data + @data = [] unless @data + + raise "No data provided by #{conf.inspect}" unless data[:data] and + data[:data].kind_of? Array + raise "Data supplied must be x,y pairs! "+ + "The data provided contained an odd set of "+ + "data points" unless data[:data].length % 2 == 0 + return if data[:data].length == 0 + + x = [] + y = [] + data[:data].each_index {|i| + (i%2 == 0 ? x : y) << data[:data][i] + } + sort( x, y ) + data[:data] = [x,y] + @data << data + end + + + protected + + def keys + @data.collect{ |x| x[:title] } + end + + def calculate_left_margin + super + label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 + @border_left = label_left if label_left > @border_left + end + + def calculate_right_margin + super + label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 + @border_right = label_right if label_right > @border_right + end + + + X = 0 + Y = 1 + def x_range + max_value = @data.collect{|x| x[:data][X][-1] }.max + min_value = @data.collect{|x| x[:data][X][0] }.min + min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value + + range = max_value - min_value + right_pad = range == 0 ? 10 : range / 20.0 + scale_range = (max_value + right_pad) - min_value + + scale_division = scale_x_divisions || (scale_range / 10.0) + + if scale_x_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + [min_value, max_value, scale_division] + end + + def get_x_values + min_value, max_value, scale_division = x_range + rv = [] + min_value.step( max_value, scale_division ) {|v| rv << v} + return rv + end + alias :get_x_labels :get_x_values + + def field_width + values = get_x_values + max = @data.collect{|x| x[:data][X][-1]}.max + dx = (max - values[-1]).to_f / (values[-1] - values[-2]) + (@graph_width.to_f - font_size*2*right_font) / + (values.length + dx - right_align) + end + + + def y_range + max_value = @data.collect{|x| x[:data][Y].max }.max + min_value = @data.collect{|x| x[:data][Y].min }.min + min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value + + range = max_value - min_value + top_pad = range == 0 ? 10 : range / 20.0 + scale_range = (max_value + top_pad) - min_value + + scale_division = scale_y_divisions || (scale_range / 10.0) + + if scale_y_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + return [min_value, max_value, scale_division] + end + + def get_y_values + min_value, max_value, scale_division = y_range + rv = [] + min_value.step( max_value, scale_division ) {|v| rv << v} + return rv + end + alias :get_y_labels :get_y_values + + def field_height + values = get_y_values + max = @data.collect{|x| x[:data][Y].max }.max + if values.length == 1 + dx = values[-1] + else + dx = (max - values[-1]).to_f / (values[-1] - values[-2]) + end + (@graph_height.to_f - font_size*2*top_font) / + (values.length + dx - top_align) + end + + def draw_data + line = 1 + + x_min, x_max, x_div = x_range + y_min, y_max, y_div = y_range + x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min) + y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min) + + for data in @data + x_points = data[:data][X] + y_points = data[:data][Y] + + lpath = "L" + x_start = 0 + y_start = 0 + x_points.each_index { |idx| + x = (x_points[idx] - x_min) * x_step + y = @graph_height - (y_points[idx] - y_min) * y_step + x_start, y_start = x,y if idx == 0 + lpath << "#{x} #{y} " + } + + if area_fill + @graph.add_element( "path", { + "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", + "class" => "fill#{line}" + }) + end + + @graph.add_element( "path", { + "d" => "M#{x_start} #{y_start} #{lpath}", + "class" => "line#{line}" + }) + + if show_data_points || show_data_values + x_points.each_index { |idx| + x = (x_points[idx] - x_min) * x_step + y = @graph_height - (y_points[idx] - y_min) * y_step + if show_data_points + @graph.add_element( "circle", { + "cx" => x.to_s, + "cy" => y.to_s, + "r" => "2.5", + "class" => "dataPoint#{line}" + }) + add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups + end + make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values + } + end + line += 1 + end + end + + def format x, y + "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})" + end + + def get_css + return <<EOL +/* default line styles */ +.line1{ + fill: none; + stroke: #ff0000; + stroke-width: 1px; +} +.line2{ + fill: none; + stroke: #0000ff; + stroke-width: 1px; +} +.line3{ + fill: none; + stroke: #00ff00; + stroke-width: 1px; +} +.line4{ + fill: none; + stroke: #ffcc00; + stroke-width: 1px; +} +.line5{ + fill: none; + stroke: #00ccff; + stroke-width: 1px; +} +.line6{ + fill: none; + stroke: #ff00ff; + stroke-width: 1px; +} +.line7{ + fill: none; + stroke: #00ffff; + stroke-width: 1px; +} +.line8{ + fill: none; + stroke: #ffff00; + stroke-width: 1px; +} +.line9{ + fill: none; + stroke: #ccc6666; + stroke-width: 1px; +} +.line10{ + fill: none; + stroke: #663399; + stroke-width: 1px; +} +.line11{ + fill: none; + stroke: #339900; + stroke-width: 1px; +} +.line12{ + fill: none; + stroke: #9966FF; + stroke-width: 1px; +} +/* default fill styles */ +.fill1{ + fill: #cc0000; + fill-opacity: 0.2; + stroke: none; +} +.fill2{ + fill: #0000cc; + fill-opacity: 0.2; + stroke: none; +} +.fill3{ + fill: #00cc00; + fill-opacity: 0.2; + stroke: none; +} +.fill4{ + fill: #ffcc00; + fill-opacity: 0.2; + stroke: none; +} +.fill5{ + fill: #00ccff; + fill-opacity: 0.2; + stroke: none; +} +.fill6{ + fill: #ff00ff; + fill-opacity: 0.2; + stroke: none; +} +.fill7{ + fill: #00ffff; + fill-opacity: 0.2; + stroke: none; +} +.fill8{ + fill: #ffff00; + fill-opacity: 0.2; + stroke: none; +} +.fill9{ + fill: #cc6666; + fill-opacity: 0.2; + stroke: none; +} +.fill10{ + fill: #663399; + fill-opacity: 0.2; + stroke: none; +} +.fill11{ + fill: #339900; + fill-opacity: 0.2; + stroke: none; +} +.fill12{ + fill: #9966FF; + fill-opacity: 0.2; + stroke: none; +} +/* default line styles */ +.key1,.dataPoint1{ + fill: #ff0000; + stroke: none; + stroke-width: 1px; +} +.key2,.dataPoint2{ + fill: #0000ff; + stroke: none; + stroke-width: 1px; +} +.key3,.dataPoint3{ + fill: #00ff00; + stroke: none; + stroke-width: 1px; +} +.key4,.dataPoint4{ + fill: #ffcc00; + stroke: none; + stroke-width: 1px; +} +.key5,.dataPoint5{ + fill: #00ccff; + stroke: none; + stroke-width: 1px; +} +.key6,.dataPoint6{ + fill: #ff00ff; + stroke: none; + stroke-width: 1px; +} +.key7,.dataPoint7{ + fill: #00ffff; + stroke: none; + stroke-width: 1px; +} +.key8,.dataPoint8{ + fill: #ffff00; + stroke: none; + stroke-width: 1px; +} +.key9,.dataPoint9{ + fill: #cc6666; + stroke: none; + stroke-width: 1px; +} +.key10,.dataPoint10{ + fill: #663399; + stroke: none; + stroke-width: 1px; +} +.key11,.dataPoint11{ + fill: #339900; + stroke: none; + stroke-width: 1px; +} +.key12,.dataPoint12{ + fill: #9966FF; + stroke: none; + stroke-width: 1px; +} +EOL + end + + end + end +end
--- a/lib/SVG/Graph/TimeSeries.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/SVG/Graph/TimeSeries.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,238 +1,238 @@ -require 'SVG/Graph/Plot' - -module SVG - module Graph - # === For creating SVG plots of scalar temporal data - # - # = Synopsis - # - # require 'SVG/Graph/TimeSeriess' - # - # # Data sets are x,y pairs - # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, - # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] - # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, - # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, - # "5/1/84", 17, "10/1/80", 12] - # - # graph = SVG::Graph::TimeSeries.new( { - # :width => 640, - # :height => 480, - # :graph_title => title, - # :show_graph_title => true, - # :no_css => true, - # :key => true, - # :scale_x_integers => true, - # :scale_y_integers => true, - # :min_x_value => 0, - # :min_y_value => 0, - # :show_data_labels => true, - # :show_x_guidelines => true, - # :show_x_title => true, - # :x_title => "Time", - # :show_y_title => true, - # :y_title => "Ice Cream Cones", - # :y_title_text_direction => :bt, - # :stagger_x_labels => true, - # :x_label_format => "%m/%d/%y", - # }) - # - # graph.add_data({ - # :data => projection - # :title => 'Projected', - # }) - # - # graph.add_data({ - # :data => actual, - # :title => 'Actual', - # }) - # - # print graph.burn() - # - # = Description - # - # Produces a graph of temporal scalar data. - # - # = Examples - # - # http://www.germane-software/repositories/public/SVG/test/timeseries.rb - # - # = Notes - # - # The default stylesheet handles upto 10 data sets, if you - # use more you must create your own stylesheet and add the - # additional settings for the extra data sets. You will know - # if you go over 10 data sets as they will have no style and - # be in black. - # - # Unlike the other types of charts, data sets must contain x,y pairs: - # - # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) - # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and - # # ("14:20",6) - # - # Note that multiple data sets within the same chart can differ in length, - # and that the data in the datasets needn't be in order; they will be ordered - # by the plot along the X-axis. - # - # The dates must be parseable by ParseDate, but otherwise can be - # any order of magnitude (seconds within the hour, or years) - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Bar - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # - # == Author - # - # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> - # - # Copyright 2004 Sean E. Russell - # This software is available under the Ruby license[LICENSE.txt] - # - class TimeSeries < Plot - # In addition to the defaults set by Graph::initialize and - # Plot::set_defaults, sets: - # [x_label_format] '%Y-%m-%d %H:%M:%S' - # [popup_format] '%Y-%m-%d %H:%M:%S' - def set_defaults - super - init_with( - #:max_time_span => '', - :x_label_format => '%Y-%m-%d %H:%M:%S', - :popup_format => '%Y-%m-%d %H:%M:%S' - ) - end - - # The format string use do format the X axis labels. - # See Time::strformat - attr_accessor :x_label_format - # Use this to set the spacing between dates on the axis. The value - # must be of the form - # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" - # - # EG: - # - # graph.timescale_divisions = "2 weeks" - # - # will cause the chart to try to divide the X axis up into segments of - # two week periods. - attr_accessor :timescale_divisions - # The formatting used for the popups. See x_label_format - attr_accessor :popup_format - - # Add data to the plot. - # - # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) - # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and - # # ("14:20",6) - # graph.add_data( - # :data => d1, - # :title => 'One' - # ) - # graph.add_data( - # :data => d2, - # :title => 'Two' - # ) - # - # Note that the data must be in time,value pairs, and that the date format - # may be any date that is parseable by ParseDate. - def add_data data - @data = [] unless @data - - raise "No data provided by #{@data.inspect}" unless data[:data] and - data[:data].kind_of? Array - raise "Data supplied must be x,y pairs! "+ - "The data provided contained an odd set of "+ - "data points" unless data[:data].length % 2 == 0 - return if data[:data].length == 0 - - - x = [] - y = [] - data[:data].each_index {|i| - if i%2 == 0 - t = DateTime.parse( data[:data][i] ).to_time - x << t.to_i - else - y << data[:data][i] - end - } - sort( x, y ) - data[:data] = [x,y] - @data << data - end - - - protected - - def min_x_value=(value) - @min_x_value = DateTime.parse( value ).to_time - end - - - def format x, y - Time.at( x ).strftime( popup_format ) - end - - def get_x_labels - get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } - end - - private - def get_x_values - rv = [] - min, max, scale_division = x_range - if timescale_divisions - timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/ - division_units = $2 ? $2 : "day" - amount = $1.to_i - if amount - step = nil - case division_units - when "month" - cur = min - while cur < max - rv << cur - arr = Time.at( cur ).to_a - arr[4] += amount - if arr[4] > 12 - arr[5] += (arr[4] / 12).to_i - arr[4] = (arr[4] % 12) - end - cur = Time.local(*arr).to_i - end - when "year" - cur = min - while cur < max - rv << cur - arr = Time.at( cur ).to_a - arr[5] += amount - cur = Time.local(*arr).to_i - end - when "week" - step = 7 * 24 * 60 * 60 * amount - when "day" - step = 24 * 60 * 60 * amount - when "hour" - step = 60 * 60 * amount - when "minute" - step = 60 * amount - when "second" - step = amount - end - min.step( max, step ) {|v| rv << v} if step - - return rv - end - end - min.step( max, scale_division ) {|v| rv << v} - return rv - end - end - end -end +require 'SVG/Graph/Plot' + +module SVG + module Graph + # === For creating SVG plots of scalar temporal data + # + # = Synopsis + # + # require 'SVG/Graph/TimeSeriess' + # + # # Data sets are x,y pairs + # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, + # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] + # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, + # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, + # "5/1/84", 17, "10/1/80", 12] + # + # graph = SVG::Graph::TimeSeries.new( { + # :width => 640, + # :height => 480, + # :graph_title => title, + # :show_graph_title => true, + # :no_css => true, + # :key => true, + # :scale_x_integers => true, + # :scale_y_integers => true, + # :min_x_value => 0, + # :min_y_value => 0, + # :show_data_labels => true, + # :show_x_guidelines => true, + # :show_x_title => true, + # :x_title => "Time", + # :show_y_title => true, + # :y_title => "Ice Cream Cones", + # :y_title_text_direction => :bt, + # :stagger_x_labels => true, + # :x_label_format => "%m/%d/%y", + # }) + # + # graph.add_data({ + # :data => projection + # :title => 'Projected', + # }) + # + # graph.add_data({ + # :data => actual, + # :title => 'Actual', + # }) + # + # print graph.burn() + # + # = Description + # + # Produces a graph of temporal scalar data. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/timeseries.rb + # + # = Notes + # + # The default stylesheet handles upto 10 data sets, if you + # use more you must create your own stylesheet and add the + # additional settings for the extra data sets. You will know + # if you go over 10 data sets as they will have no style and + # be in black. + # + # Unlike the other types of charts, data sets must contain x,y pairs: + # + # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) + # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and + # # ("14:20",6) + # + # Note that multiple data sets within the same chart can differ in length, + # and that the data in the datasets needn't be in order; they will be ordered + # by the plot along the X-axis. + # + # The dates must be parseable by ParseDate, but otherwise can be + # any order of magnitude (seconds within the hour, or years) + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class TimeSeries < Plot + # In addition to the defaults set by Graph::initialize and + # Plot::set_defaults, sets: + # [x_label_format] '%Y-%m-%d %H:%M:%S' + # [popup_format] '%Y-%m-%d %H:%M:%S' + def set_defaults + super + init_with( + #:max_time_span => '', + :x_label_format => '%Y-%m-%d %H:%M:%S', + :popup_format => '%Y-%m-%d %H:%M:%S' + ) + end + + # The format string use do format the X axis labels. + # See Time::strformat + attr_accessor :x_label_format + # Use this to set the spacing between dates on the axis. The value + # must be of the form + # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" + # + # EG: + # + # graph.timescale_divisions = "2 weeks" + # + # will cause the chart to try to divide the X axis up into segments of + # two week periods. + attr_accessor :timescale_divisions + # The formatting used for the popups. See x_label_format + attr_accessor :popup_format + + # Add data to the plot. + # + # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2) + # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and + # # ("14:20",6) + # graph.add_data( + # :data => d1, + # :title => 'One' + # ) + # graph.add_data( + # :data => d2, + # :title => 'Two' + # ) + # + # Note that the data must be in time,value pairs, and that the date format + # may be any date that is parseable by ParseDate. + def add_data data + @data = [] unless @data + + raise "No data provided by #{@data.inspect}" unless data[:data] and + data[:data].kind_of? Array + raise "Data supplied must be x,y pairs! "+ + "The data provided contained an odd set of "+ + "data points" unless data[:data].length % 2 == 0 + return if data[:data].length == 0 + + + x = [] + y = [] + data[:data].each_index {|i| + if i%2 == 0 + t = DateTime.parse( data[:data][i] ).to_time + x << t.to_i + else + y << data[:data][i] + end + } + sort( x, y ) + data[:data] = [x,y] + @data << data + end + + + protected + + def min_x_value=(value) + @min_x_value = DateTime.parse( value ).to_time + end + + + def format x, y + Time.at( x ).strftime( popup_format ) + end + + def get_x_labels + get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } + end + + private + def get_x_values + rv = [] + min, max, scale_division = x_range + if timescale_divisions + timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/ + division_units = $2 ? $2 : "day" + amount = $1.to_i + if amount + step = nil + case division_units + when "month" + cur = min + while cur < max + rv << cur + arr = Time.at( cur ).to_a + arr[4] += amount + if arr[4] > 12 + arr[5] += (arr[4] / 12).to_i + arr[4] = (arr[4] % 12) + end + cur = Time.local(*arr).to_i + end + when "year" + cur = min + while cur < max + rv << cur + arr = Time.at( cur ).to_a + arr[5] += amount + cur = Time.local(*arr).to_i + end + when "week" + step = 7 * 24 * 60 * 60 * amount + when "day" + step = 24 * 60 * 60 * amount + when "hour" + step = 60 * 60 * amount + when "minute" + step = 60 * amount + when "second" + step = amount + end + min.step( max, step ) {|v| rv << v} if step + + return rv + end + end + min.step( max, scale_division ) {|v| rv << v} + return rv + end + end + end +end
--- a/lib/plugins/rfpdf/README Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/plugins/rfpdf/README Fri Jun 14 09:01:12 2013 +0100 @@ -10,10 +10,6 @@ == == -If you are using HTML, it is recommended you install: - -gem install -r htmlentities - TCPDF Documentation located at: http://phpdocs.moodle.org/com-tecnick-tcpdf/TCPDF.html @@ -42,4 +38,4 @@ pdf = TCPDF.new -ENJOY! \ No newline at end of file +ENJOY!
--- a/lib/plugins/rfpdf/init.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/plugins/rfpdf/init.rb Fri Jun 14 09:01:12 2013 +0100 @@ -1,8 +1,3 @@ -begin - require('htmlentities') -rescue LoadError - # This gem is not required - just nice to have. -end require('cgi') require 'rfpdf'
--- a/lib/plugins/rfpdf/lib/tcpdf.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/plugins/rfpdf/lib/tcpdf.rb Fri Jun 14 09:01:12 2013 +0100 @@ -94,8 +94,6 @@ cattr_accessor :k_path_url_cache @@k_path_url_cache = Rails.root.join('tmp') - cattr_accessor :decoder - attr_accessor :barcode attr_accessor :buffer @@ -223,12 +221,6 @@ #Some checks dochecks(); - begin - @@decoder = HTMLEntities.new - rescue - @@decoder = nil - end - #Initialization of properties @barcode ||= false @buffer ||= '' @@ -4002,6 +3994,10 @@ @quote_page[@quote_count] = @page; @quote_count += 1 when 'br' + if @tdbegin + @tdtext << "\n" + return + end Ln(); if (@li_spacer.length > 0) @@ -4340,11 +4336,7 @@ # @return string converted # def unhtmlentities(string) - if @@decoder.nil? CGI.unescapeHTML(string) - else - @@decoder.decode(string) - end end end # END OF CLASS
--- a/lib/redmine.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/redmine.rb Fri Jun 14 09:01:12 2013 +0100 @@ -198,7 +198,7 @@ menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id, :if => Proc.new { |p| p.shared_versions.any? } menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural - menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new, + menu.push :new_issue, { :controller => 'issues', :action => 'new', :copy_from => nil }, :param => :project_id, :caption => :label_issue_new, :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) } menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
--- a/lib/redmine/export/pdf.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/redmine/export/pdf.rb Fri Jun 14 09:01:12 2013 +0100 @@ -109,6 +109,13 @@ RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding)) end + def formatted_text(text) + html = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) + # Strip {{toc}} tags + html.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i, '') + html + end + def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='') Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link) end @@ -120,8 +127,7 @@ def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0) @attachments = attachments writeHTMLCell(w, h, x, y, - fix_text_encoding( - Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)), + fix_text_encoding(formatted_text(txt)), border, ln, fill) end
--- a/lib/redmine/version.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/redmine/version.rb Fri Jun 14 09:01:12 2013 +0100 @@ -4,7 +4,7 @@ module VERSION #:nodoc: MAJOR = 2 MINOR = 2 - TINY = 0 + TINY = 4 # Branch values: # * official release: nil
--- a/lib/redmine/wiki_formatting/macros.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/redmine/wiki_formatting/macros.rb Fri Jun 14 09:01:12 2013 +0100 @@ -147,10 +147,10 @@ unless block_given? raise "Can not create a macro without a block!" end - name = name.to_sym if name.is_a?(String) + name = name.to_s.downcase.to_sym available_macros[name] = {:desc => @@desc || ''}.merge(options) @@desc = nil - Definitions.send :define_method, "macro_#{name}".downcase, &block + Definitions.send :define_method, "macro_#{name}", &block end # Sets description for the next macro to be defined
--- a/lib/redmine/wiki_formatting/textile/formatter.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/redmine/wiki_formatting/textile/formatter.rb Fri Jun 14 09:01:12 2013 +0100 @@ -69,7 +69,7 @@ l = 1 started = false ended = false - text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level| + text.scan(/(((?:.*?)(\A|\r?\n\s*\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))?[ \t](.*?)$)|.*)/m).each do |all, content, lf, heading, level| if heading.nil? if ended after << all
--- a/lib/tasks/ci.rake Mon Jan 07 12:01:42 2013 +0000 +++ b/lib/tasks/ci.rake Fri Jun 14 09:01:12 2013 +0100 @@ -13,6 +13,7 @@ desc "Setup Redmine for a new build." task :setup do Rake::Task["ci:dump_environment"].invoke + Rake::Task["tmp:clear"].invoke Rake::Task["db:create"].invoke Rake::Task["db:migrate"].invoke Rake::Task["db:schema:dump"].invoke
--- a/public/help/wiki_syntax_detailed.html Mon Jan 07 12:01:42 2013 +0000 +++ b/public/help/wiki_syntax_detailed.html Fri Jun 14 09:01:12 2013 +0100 @@ -169,7 +169,7 @@ <h2><a name="5" class="wiki-page"></a>Text formatting</h2> - <p>For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See <a class="external" href="http://www.textism.com/tools/textile/">http://www.textism.com/tools/textile/</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p> + <p>For things such as headlines, bold, tables, lists, Redmine supports Textile syntax. See <a class="external" href="http://en.wikipedia.org/wiki/Textile_%28markup_language%29">http://en.wikipedia.org/wiki/Textile_(markup_language)</a> for information on using any of these features. A few samples are included below, but the engine is capable of much more of that.</p> <h3><a name="6" class="wiki-page"></a>Font style</h3>
--- a/public/javascripts/jstoolbar/lang/jstoolbar-bg.js Mon Jan 07 12:01:42 2013 +0000 +++ b/public/javascripts/jstoolbar/lang/jstoolbar-bg.js Fri Jun 14 09:01:12 2013 +0100 @@ -11,6 +11,6 @@ jsToolBar.strings['Ordered list'] = 'Подреден списък'; jsToolBar.strings['Quote'] = 'Цитат'; jsToolBar.strings['Unquote'] = 'Премахване на цитат'; -jsToolBar.strings['Preformatted text'] = 'Preformatted text'; +jsToolBar.strings['Preformatted text'] = 'Форматиран текст'; jsToolBar.strings['Wiki link'] = 'Връзка до Wiki страница'; jsToolBar.strings['Image'] = 'Изображение';
--- a/public/stylesheets/application.css Mon Jan 07 12:01:42 2013 +0000 +++ b/public/stylesheets/application.css Fri Jun 14 09:01:12 2013 +0100 @@ -25,7 +25,7 @@ #account {float:right;} -#header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} +#header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;} #header a {color:#f8f8f8;} #header h1 a.ancestor { font-size: 80%; } #quick-search {float:right;}
--- a/test/fixtures/wiki_content_versions.yml Mon Jan 07 12:01:42 2013 +0000 +++ b/test/fixtures/wiki_content_versions.yml Fri Jun 14 09:01:12 2013 +0100 @@ -99,4 +99,18 @@ version: 3 author_id: 1 comments: +wiki_content_versions_007: + data: |- + h1. Page with an inline image + + This is an inline image: + + !logo.gif! + updated_on: 2007-03-08 00:18:07 +01:00 + page_id: 4 + wiki_content_id: 4 + id: 7 + version: 1 + author_id: 1 + comments:
--- a/test/functional/activities_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/activities_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -37,7 +37,7 @@ end def test_previous_project_index - get :index, :id => 1, :from => 3.days.ago.to_date + get :index, :id => 1, :from => 2.days.ago.to_date assert_response :success assert_template 'index' assert_not_nil assigns(:events_by_day)
--- a/test/functional/boards_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/boards_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -69,6 +69,21 @@ assert topics.first.updated_on < topics.second.updated_on end + def test_show_should_display_message_with_last_reply_first + Message.update_all(:sticky => 0) + + # Reply to an old topic + old_topic = Message.where(:board_id => 1, :parent_id => nil).order('created_on ASC').first + reply = Message.new(:board_id => 1, :subject => 'New reply', :content => 'New reply', :author_id => 2) + old_topic.children << reply + + get :show, :project_id => 1, :id => 1 + assert_response :success + topics = assigns(:topics) + assert_not_nil topics + assert_equal old_topic, topics.first + end + def test_show_with_permission_should_display_the_new_message_form @request.session[:user_id] = 2 get :show, :project_id => 1, :id => 1
--- a/test/functional/context_menus_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/context_menus_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -132,7 +132,7 @@ :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo"} assert_tag 'a', :content => 'none', - :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D="} + :attributes => {:href => "/issues/bulk_update?ids%5B%5D=1&issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__"} end def test_context_menu_should_not_include_null_value_for_required_custom_fields
--- a/test/functional/issues_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/issues_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -2356,6 +2356,9 @@ assert_tag 'select', :attributes => {:name => 'issue[project_id]'}, :child => {:tag => 'option', :attributes => {:value => '2', :selected => nil}, :content => 'OnlineStore'} assert_tag 'input', :attributes => {:name => 'copy_from', :value => '1'} + + # "New issue" menu item should not link to copy + assert_select '#main-menu a.new-issue[href=/projects/ecookbook/issues/new]' end def test_new_as_copy_with_attachments_should_show_copy_attachments_checkbox @@ -2911,6 +2914,20 @@ assert_equal spent_hours_before + 2.5, issue.spent_hours end + def test_put_update_should_preserve_parent_issue_even_if_not_visible + parent = Issue.generate!(:project_id => 1, :is_private => true) + issue = Issue.generate!(:parent_issue_id => parent.id) + assert !parent.visible?(User.find(3)) + @request.session[:user_id] = 3 + + get :edit, :id => issue.id + assert_select 'input[name=?][value=?]', 'issue[parent_issue_id]', parent.id.to_s + + put :update, :id => issue.id, :issue => {:subject => 'New subject', :parent_issue_id => parent.id.to_s} + assert_response 302 + assert_equal parent, issue.parent + end + def test_put_update_with_attachment_only set_tmp_attachments_directory
--- a/test/functional/messages_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/messages_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -81,6 +81,11 @@ assert_response 404 end + def test_show_message_from_invalid_board_should_respond_with_404 + get :show, :board_id => 999, :id => 1 + assert_response 404 + end + def test_get_new @request.session[:user_id] = 2 get :new, :board_id => 1
--- a/test/functional/my_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/my_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -187,6 +187,11 @@ assert User.find(2).pref[:my_page_layout]['top'].include?('issuesreportedbyme') end + def test_add_invalid_block_should_redirect + post :add_block, :block => 'invalid' + assert_redirected_to '/my/page_layout' + end + def test_remove_block post :remove_block, :block => 'issuesassignedtome' assert_redirected_to '/my/page_layout'
--- a/test/functional/wiki_controller_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/functional/wiki_controller_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -75,6 +75,19 @@ assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/ end + def test_show_old_version_with_attachments + page = WikiPage.find(4) + assert page.attachments.any? + content = page.content + content.text = "update" + content.save! + + get :show, :project_id => 'ecookbook', :id => page.title, :version => '1' + assert_kind_of WikiContent::Version, assigns(:content) + assert_response :success + assert_template 'show' + end + def test_show_old_version_without_permission_should_be_denied Role.anonymous.remove_permission! :view_wiki_edits
--- a/test/object_helpers.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/object_helpers.rb Fri Jun 14 09:01:12 2013 +0100 @@ -118,4 +118,16 @@ board.save! board end + + def Attachment.generate!(attributes={}) + @generated_filename ||= 'testfile0' + @generated_filename.succ! + attributes = attributes.dup + attachment = Attachment.new(attributes) + attachment.container ||= Issue.find(1) + attachment.author ||= User.find(2) + attachment.filename = @generated_filename if attachment.filename.blank? + attachment.save! + attachment + end end
--- a/test/unit/helpers/application_helper_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/unit/helpers/application_helper_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -347,6 +347,15 @@ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" } end + def test_redmine_links_with_a_different_project_before_current_project + vp1 = Version.generate!(:project_id => 1, :name => '1.4.4') + vp3 = Version.generate!(:project_id => 3, :name => '1.4.4') + + @project = Project.find(3) + assert_equal %(<p><a href="/versions/#{vp1.id}" class="version">1.4.4</a> <a href="/versions/#{vp3.id}" class="version">1.4.4</a></p>), + textilizable("ecookbook:version:1.4.4 version:1.4.4") + end + def test_escaped_redmine_links_should_not_be_parsed to_test = [ '#3.', @@ -394,14 +403,14 @@ end def test_multiple_repositories_redmine_links - svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg') + svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg') Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123') hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg') Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd') changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2}, :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3') - svn_changeset_link = link_to('svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123}, + svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123}, :class => 'changeset', :title => '') hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'}, :class => 'changeset', :title => '') @@ -411,7 +420,7 @@ to_test = { 'r2' => changeset_link, - 'svn1|r123' => svn_changeset_link, + 'svn_repo-1|r123' => svn_changeset_link, 'invalid|r123' => 'invalid|r123', 'commit:hg1|abcd' => hg_changeset_link, 'commit:invalid|abcd' => 'commit:invalid|abcd', @@ -551,6 +560,15 @@ to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => Issue.find(3).attachments), "#{text} failed" } end + def test_attachment_link_should_link_to_latest_attachment + set_tmp_attachments_directory + a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago) + a2 = Attachment.generate!(:filename => "test.txt") + + assert_equal %(<p><a href="/attachments/download/#{a2.id}" class="attachment">test.txt</a></p>), + textilizable('attachment:test.txt', :attachments => [a1, a2]) + end + def test_wiki_links to_test = { '[[CookBook documentation]]' => '<a href="/projects/ecookbook/wiki/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
--- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -78,6 +78,12 @@ assert_equal "<p>Baz: (arg1,arg2) (String) (line1\nline2)</p>", textilizable("{{baz(arg1, arg2)\nline1\nline2\n}}") end + def test_macro_name_with_upper_case + Redmine::WikiFormatting::Macros.macro(:UpperCase) {|obj, args| "Upper"} + + assert_equal "<p>Upper</p>", textilizable("{{UpperCase}}") + end + def test_multiple_macros_on_the_same_line Redmine::WikiFormatting::Macros.macro :foo do |obj, args| args.any? ? "args: #{args.join(',')}" : "no args"
--- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Mon Jan 07 12:01:42 2013 +0000 +++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb Fri Jun 14 09:01:12 2013 +0100 @@ -419,6 +419,20 @@ end end + def test_get_section_should_support_headings_starting_with_a_tab + text = <<-STR +h1.\tHeading 1 + +Content 1 + +h1. Heading 2 + +Content 2 +STR + + assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first + end + private def assert_html_output(to_test, expect_paragraph = true)