From 9f682af0ebe7f381984fe07f68ba5346628bf1ca Mon Sep 17 00:00:00 2001 From: Jason Thistlethwaite Date: Fri, 24 Apr 2026 22:01:18 +0000 Subject: [PATCH] Initial Redmine tooling and local plugin forks --- .gitignore | 6 + AGENT.md | 338 ++++++ README.md | 427 +++++++ ...lpdesk-search-20260421T215548Z.MANIFEST.md | 13 + ...lpdesk-search-20260421T215548Z.MANIFEST.md | 14 + ....1-known-good-20260421T143957Z.MANIFEST.md | 47 + docs/event_outbox_spec.md | 588 +++++++++ docs/pre_existing_issues.md | 94 ++ docs/redmineup_local_fork_changelog.md | 85 ++ plugins/README.md | 14 + plugins/redmine_contacts/Gemfile | 3 + plugins/redmine_contacts/README.rdoc | 46 + .../controllers/contact_imports_controller.rb | 36 + .../app/controllers/contacts_controller.rb | 490 ++++++++ .../contacts_duplicates_controller.rb | 102 ++ .../controllers/contacts_issues_controller.rb | 109 ++ .../controllers/contacts_mailer_controller.rb | 38 + .../contacts_projects_controller.rb | 87 ++ .../contacts_settings_controller.rb | 34 + .../controllers/contacts_tags_controller.rb | 98 ++ .../controllers/contacts_vcf_controller.rb | 97 ++ .../app/controllers/crm_queries_controller.rb | 133 +++ .../controllers/deal_categories_controller.rb | 109 ++ .../controllers/deal_contacts_controller.rb | 81 ++ .../controllers/deal_imports_controller.rb | 36 + .../controllers/deal_statuses_controller.rb | 95 ++ .../app/controllers/deals_controller.rb | 333 ++++++ .../app/controllers/deals_tasks_controller.rb | 74 ++ .../controllers/importer_base_controller.rb | 149 +++ .../app/controllers/notes_controller.rb | 130 ++ .../app/controllers/tasks_controller.rb | 110 ++ .../app/helpers/contacts_helper.rb | 269 +++++ .../app/helpers/contacts_money_helper.rb | 24 + .../app/helpers/crm_queries_helper.rb | 94 ++ .../app/helpers/deals_helper.rb | 185 +++ .../app/helpers/notes_helper.rb | 132 +++ .../redmine_contacts/app/models/address.rb | 81 ++ .../redmine_contacts/app/models/contact.rb | 519 ++++++++ .../app/models/contact_custom_field.rb | 26 + .../app/models/contact_import.rb | 51 + .../app/models/contact_kernel_import.rb | 130 ++ .../app/models/contact_note.rb | 50 + .../app/models/contact_query.rb | 240 ++++ .../app/models/contacts_issue.rb | 36 + .../app/models/contacts_mailer.rb | 262 ++++ .../app/models/contacts_setting.rb | 171 +++ .../redmine_contacts/app/models/crm_query.rb | 244 ++++ plugins/redmine_contacts/app/models/deal.rb | 306 +++++ .../app/models/deal_category.rb | 52 + .../app/models/deal_custom_field.rb | 26 + .../app/models/deal_import.rb | 46 + .../app/models/deal_kernel_import.rb | 101 ++ .../redmine_contacts/app/models/deal_note.rb | 51 + .../app/models/deal_process.rb | 44 + .../redmine_contacts/app/models/deal_query.rb | 178 +++ .../app/models/deal_status.rb | 116 ++ .../app/models/deals_issue.rb | 33 + .../app/models/deals_pipeline_processor.rb | 79 ++ plugins/redmine_contacts/app/models/note.rb | 95 ++ .../app/models/note_custom_field.rb | 26 + .../app/models/recently_viewed.rb | 45 + plugins/redmine_contacts/app/models/task.rb | 32 + .../views/auto_completes/_companies.html.erb | 10 + .../views/auto_completes/_contacts.html.erb | 11 + .../auto_completes/_crm_tag_list.html.erb | 4 + .../app/views/auto_completes/_deals.html.erb | 9 + .../app/views/common/_address_form.html.erb | 8 + .../app/views/common/_contact_data.html.erb | 17 + .../app/views/common/_contact_tabs.html.erb | 29 + .../common/_contacts_select2_data.html.erb | 9 + .../views/common/_notes_attachments.html.erb | 4 + .../views/common/_recently_viewed.html.erb | 4 + .../views/common/_responsible_user.html.erb | 10 + .../app/views/common/_sidebar.html.erb | 15 + .../app/views/contacts/_attributes.html.erb | 74 ++ .../views/contacts/_company_contacts.html.erb | 13 + .../app/views/contacts/_contact_card.html.erb | 36 + .../contacts/_custom_field_form.html.erb | 4 + .../app/views/contacts/_form.html.erb | 105 ++ .../app/views/contacts/_form_tags.html.erb | 24 + .../app/views/contacts/_list.html.erb | 44 + .../app/views/contacts/_list_cards.html.erb | 51 + .../app/views/contacts/_list_excerpt.html.erb | 54 + .../views/contacts/_name_observer.html.erb | 37 + .../app/views/contacts/_new_modal.html.erb | 15 + .../app/views/contacts/_notes.html.erb | 29 + .../app/views/contacts/_tag_list.html.erb | 10 + .../app/views/contacts/_tags_cloud.html.erb | 7 + .../app/views/contacts/_tags_item.html.erb | 19 + .../app/views/contacts/bulk_edit.html.erb | 130 ++ .../views/contacts/contacts_notes.html.erb | 64 + .../app/views/contacts/context_menu.html.erb | 35 + .../app/views/contacts/create.js.erb | 13 + .../app/views/contacts/edit.html.erb | 28 + .../app/views/contacts/edit_mails.html.erb | 64 + .../app/views/contacts/index.api.rsb | 53 + .../app/views/contacts/index.html.erb | 203 ++++ .../app/views/contacts/load_tab.js.erb | 5 + .../app/views/contacts/new.html.erb | 23 + .../app/views/contacts/new.js.erb | 9 + .../app/views/contacts/show.api.rsb | 110 ++ .../app/views/contacts/show.html.erb | 86 ++ .../contacts_duplicates/_duplicates.html.erb | 23 + .../views/contacts_duplicates/_list.html.erb | 8 + .../views/contacts_duplicates/index.html.erb | 54 + .../_additional_assets.html.erb | 6 + .../contacts_issues/_attributes.html.erb | 35 + .../views/contacts_issues/_contacts.html.erb | 45 + .../contacts_issues/_issue_item.html.erb | 11 + .../views/contacts_issues/_issues.html.erb | 35 + .../views/contacts_issues/_new_modal.html.erb | 27 + .../autocomplete_for_contact.html.erb | 1 + .../app/views/contacts_issues/close.js.erb | 5 + .../app/views/contacts_issues/create.js.erb | 2 + .../app/views/contacts_issues/delete.js.erb | 1 + .../app/views/contacts_issues/new.js.erb | 3 + .../views/contacts_mailer/bulk_mail.html.erb | 1 + .../views/contacts_mailer/bulk_mail.text.erb | 1 + .../views/contacts_projects/_related.html.erb | 22 + .../app/views/contacts_projects/new.js.erb | 1 + .../views/contacts_tags/_tags_form.html.erb | 3 + .../views/contacts_tags/context_menu.html.erb | 12 + .../app/views/contacts_tags/edit.html.erb | 12 + .../app/views/contacts_tags/index.api.rsb | 9 + .../app/views/contacts_tags/merge.html.erb | 14 + .../app/views/contacts_vcf/_load.html.erb | 16 + .../app/views/crm_calendars/_buttons.html.erb | 10 + .../crm_calendars/_crm_calendar.html.erb | 21 + .../_deal_calendar_event.html.erb | 5 + .../app/views/crm_queries/edit.html.erb | 6 + .../app/views/crm_queries/index.api.rsb | 10 + .../app/views/crm_queries/index.html.erb | 25 + .../app/views/crm_queries/new.html.erb | 6 + .../app/views/deal_categories/_form.html.erb | 5 + .../views/deal_categories/destroy.html.erb | 15 + .../app/views/deal_categories/edit.html.erb | 6 + .../app/views/deal_categories/index.api.rsb | 8 + .../app/views/deal_categories/new.html.erb | 6 + .../views/deal_contacts/_contacts.html.erb | 19 + .../app/views/deal_contacts/_new_modal.erb | 23 + .../app/views/deal_contacts/add.js.erb | 1 + .../views/deal_contacts/autocomplete.html.erb | 1 + .../app/views/deal_contacts/delete.js.erb | 1 + .../app/views/deal_contacts/search.js.erb | 4 + .../app/views/deal_statuses/_form.html.erb | 40 + .../app/views/deal_statuses/edit.html.erb | 6 + .../app/views/deal_statuses/index.api.rsb | 12 + .../app/views/deal_statuses/index.html.erb | 30 + .../app/views/deal_statuses/new.html.erb | 6 + .../app/views/deals/_attributes.html.erb | 14 + .../views/deals/_board_deals_counts.html.erb | 7 + .../app/views/deals/_board_total.html.erb | 7 + .../views/deals/_custom_field_form.html.erb | 5 + .../views/deals/_deals_statistics.html.erb | 24 + .../app/views/deals/_form.html.erb | 68 ++ .../app/views/deals/_line_fields.html.erb | 45 + .../app/views/deals/_list.html.erb | 71 ++ .../app/views/deals/_list_board.html.erb | 104 ++ .../app/views/deals/_list_excerpt.html.erb | 71 ++ .../app/views/deals/_list_pipeline.html.erb | 45 + .../app/views/deals/_process_item.html.erb | 29 + .../app/views/deals/_related_deals.html.erb | 28 + .../app/views/deals/bulk_edit.html.erb | 99 ++ .../app/views/deals/context_menu.html.erb | 54 + .../app/views/deals/edit.html.erb | 16 + .../app/views/deals/index.api.rsb | 32 + .../app/views/deals/index.html.erb | 151 +++ .../app/views/deals/new.html.erb | 33 + .../app/views/deals/show.api.rsb | 45 + .../app/views/deals/show.html.erb | 129 ++ .../app/views/deals/update_form.js.erb | 1 + .../app/views/deals/update_total.js.erb | 2 + .../app/views/deals_issues/_form.html.erb | 23 + .../app/views/deals_issues/_issues.html.erb | 20 + .../app/views/deals_issues/_show.html.erb | 5 + .../app/views/deals_pipeline/index.html.erb | 64 + .../app/views/importers/_preview.html.erb | 18 + .../app/views/importers/kernel_new.erb | 12 + .../app/views/importers/mapping.html.erb | 12 + .../views/importers/mapping/_contact.html.erb | 10 + .../mapping/_contact_fields_mapping.html.erb | 86 ++ .../views/importers/mapping/_deal.html.erb | 10 + .../mapping/_deal_fields_mapping.html.erb | 51 + .../app/views/importers/new.html.erb | 18 + .../app/views/importers/run.html.erb | 16 + .../app/views/importers/run.js.erb | 11 + .../app/views/importers/settings.html.erb | 26 + .../app/views/importers/show.html.erb | 26 + .../app/views/mailer/_contact.text.erb | 1 + .../app/views/mailer/crm_contact_add.html.erb | 32 + .../app/views/mailer/crm_contact_add.text.erb | 30 + .../app/views/mailer/crm_deal_add.html.erb | 17 + .../app/views/mailer/crm_deal_add.text.erb | 19 + .../views/mailer/crm_deal_updated.html.erb | 22 + .../views/mailer/crm_deal_updated.text.erb | 18 + .../app/views/mailer/crm_note_add.html.erb | 5 + .../app/views/mailer/crm_note_add.text.erb | 5 + .../app/views/mailer/issue_connected.html.erb | 3 + .../app/views/mailer/issue_connected.text.erb | 3 + .../app/views/my/blocks/_my_contacts.html.erb | 28 + .../my/blocks/_my_contacts_avatars.html.erb | 42 + .../my/blocks/_my_contacts_stats.html.erb | 36 + .../app/views/my/blocks/_my_deals.html.erb | 27 + .../app/views/notes/_add.html.erb | 8 + .../app/views/notes/_form.html.erb | 22 + .../app/views/notes/_last_notes.html.erb | 8 + .../app/views/notes/_note_data.html.erb | 19 + .../app/views/notes/_note_header.html.erb | 15 + .../app/views/notes/_note_item.html.erb | 52 + .../app/views/notes/_notes_list.html.erb | 8 + .../app/views/notes/create.js.erb | 4 + .../app/views/notes/destroy.js.erb | 1 + .../app/views/notes/edit.html.erb | 51 + .../app/views/notes/new.html.erb | 57 + .../app/views/notes/show.api.rsb | 11 + .../app/views/notes/show.html.erb | 33 + .../app/views/projects/_contacts.html.erb | 45 + .../projects/_contacts_settings.html.erb | 14 + .../views/projects/_deal_statuses.html.erb | 28 + .../views/projects/_deals_settings.html.erb | 32 + .../settings/contacts/_contacts.html.erb | 5 + .../contacts/_contacts_deal_statuses.html.erb | 32 + .../contacts/_contacts_general.html.erb | 85 ++ .../contacts/_contacts_hidden.html.erb | 4 + .../settings/contacts/_contacts_tags.html.erb | 40 + .../views/settings/contacts/_money.html.erb | 43 + .../app/views/users/_contact.html.erb | 12 + .../assets/images/arrow_merge.png | Bin 0 -> 484 bytes .../assets/images/bullet_go.png | Bin 0 -> 428 bytes .../assets/images/calendar_view_day.png | Bin 0 -> 572 bytes .../redmine_contacts/assets/images/case.png | Bin 0 -> 2664 bytes .../assets/images/clock_red.png | Bin 0 -> 889 bytes .../assets/images/company.png | Bin 0 -> 3134 bytes .../redmine_contacts/assets/images/date.png | Bin 0 -> 626 bytes .../redmine_contacts/assets/images/deal.png | Bin 0 -> 4895 bytes .../redmine_contacts/assets/images/email.png | Bin 0 -> 641 bytes .../redmine_contacts/assets/images/feed.png | Bin 0 -> 427 bytes .../redmine_contacts/assets/images/money.png | Bin 0 -> 738 bytes .../assets/images/money_dollar.png | Bin 0 -> 630 bytes .../assets/images/money_euro.png | Bin 0 -> 605 bytes .../assets/images/money_pound.png | Bin 0 -> 565 bytes .../assets/images/money_yen.png | Bin 0 -> 562 bytes .../redmine_contacts/assets/images/person.png | Bin 0 -> 2674 bytes .../redmine_contacts/assets/images/phone.png | Bin 0 -> 488 bytes .../assets/images/rosette.png | Bin 0 -> 673 bytes .../assets/images/telephone.png | Bin 0 -> 791 bytes .../assets/images/unknown.png | Bin 0 -> 1166 bytes .../assets/images/user_suit.png | Bin 0 -> 748 bytes .../redmine_contacts/assets/images/vcard.png | Bin 0 -> 533 bytes .../assets/javascripts/contacts.js | 247 ++++ .../javascripts/contacts_autocomplete.js | 46 + .../assets/javascripts/contacts_select2.js | 41 + .../javascripts/jquery.colorPicker.min.js | 26 + .../assets/javascripts/tag-it.js | 588 +++++++++ .../assets/stylesheets/colorPicker.css | 30 + .../assets/stylesheets/contacts.css | 732 ++++++++++++ .../assets/stylesheets/contacts_sidebar.css | 9 + .../assets/stylesheets/jquery.tagit.css | 54 + .../redmine_contacts/config/locales/az.yml | 265 +++++ .../redmine_contacts/config/locales/cs.yml | 144 +++ .../redmine_contacts/config/locales/da.yml | 227 ++++ .../redmine_contacts/config/locales/de.yml | 578 +++++++++ .../redmine_contacts/config/locales/el.yml | 592 +++++++++ .../redmine_contacts/config/locales/en.yml | 622 ++++++++++ .../redmine_contacts/config/locales/es.yml | 614 ++++++++++ .../redmine_contacts/config/locales/fr.yml | 616 ++++++++++ .../redmine_contacts/config/locales/hu.yml | 272 +++++ .../redmine_contacts/config/locales/it.yml | 159 +++ .../redmine_contacts/config/locales/ja.yml | 588 +++++++++ .../redmine_contacts/config/locales/ko.yml | 272 +++++ .../redmine_contacts/config/locales/nl.yml | 142 +++ .../redmine_contacts/config/locales/no.yml | 189 +++ .../redmine_contacts/config/locales/pl.yml | 585 +++++++++ .../redmine_contacts/config/locales/pt-BR.yml | 607 ++++++++++ .../redmine_contacts/config/locales/ru.yml | 598 ++++++++++ .../redmine_contacts/config/locales/sk.yml | 144 +++ .../redmine_contacts/config/locales/sr-YU.yml | 595 ++++++++++ .../redmine_contacts/config/locales/sr.yml | 593 +++++++++ .../redmine_contacts/config/locales/sv.yml | 610 ++++++++++ .../redmine_contacts/config/locales/tr.yml | 159 +++ .../redmine_contacts/config/locales/vi.yml | 230 ++++ .../redmine_contacts/config/locales/zh-TW.yml | 609 ++++++++++ .../redmine_contacts/config/locales/zh.yml | 622 ++++++++++ plugins/redmine_contacts/config/routes.rb | 124 ++ .../db/migrate/016_create_contacts.rb | 55 + .../migrate/017_create_contacts_relations.rb | 50 + .../db/migrate/018_create_deals.rb | 49 + .../db/migrate/019_create_deals_relations.rb | 61 + .../db/migrate/020_create_notes.rb | 39 + .../db/migrate/021_create_tags.rb | 30 + .../db/migrate/022_create_recently_vieweds.rb | 37 + .../migrate/023_create_contacts_settings.rb | 34 + .../db/migrate/024_add_type_to_notes.rb | 29 + .../db/migrate/025_add_fields_to_deals.rb | 32 + .../db/migrate/026_create_contacts_queries.rb | 42 + .../migrate/027_change_deals_currency_type.rb | 27 + .../028_add_cached_tag_list_to_contacts.rb | 29 + .../migrate/029_add_visibility_to_contacts.rb | 38 + .../030_change_deal_statuses_is_closed.rb | 30 + .../migrate/031_populate_contacts_module.rb | 30 + .../db/migrate/032_create_addresses.rb | 49 + .../db/migrate/033_create_deals_issues.rb | 31 + .../034_change_deals_price_precision.rb | 27 + plugins/redmine_contacts/doc/CHANGELOG | 406 +++++++ plugins/redmine_contacts/doc/COPYING | 339 ++++++ plugins/redmine_contacts/doc/LICENSE | 26 + plugins/redmine_contacts/init.rb | 164 +++ .../lib/acts_as_priceable/init.rb | 21 + .../lib/acts_as_priceable.rb | 66 ++ .../lib/acts_as_taggable_on_patch.rb | 59 + .../lib/acts_as_viewable/init.rb | 30 + .../acts_as_viewable/lib/acts_as_viewable.rb | 59 + .../lib/company_custom_field_format.rb | 60 + .../redmine_contacts/lib/csv_importable.rb | 121 ++ .../redmine_contacts/lib/redmine_contacts.rb | 94 ++ .../contacts_project_setting.rb | 48 + .../helpers/contacts_helper.rb | 333 ++++++ .../helpers/crm_calendar_helper.rb | 91 ++ .../redmine_contacts/helpers/money_helper.rb | 91 ++ .../controllers_time_entry_reports_hook.rb | 34 + .../hooks/views_custom_fields_hook.rb | 27 + .../hooks/views_issues_hook.rb | 28 + .../hooks/views_layouts_hook.rb | 27 + .../hooks/views_projects_hook.rb | 26 + .../hooks/views_users_hook.rb | 28 + .../liquid/drops/addresses_drop.rb | 64 + .../liquid/drops/contacts_drop.rb | 91 ++ .../liquid/drops/deals_drop.rb | 77 ++ .../liquid/drops/notes_drop.rb | 61 + .../lib/redmine_contacts/liquid/liquid.rb | 97 ++ .../patches/action_controller_patch.rb | 51 + .../patches/application_controller_patch.rb | 44 + .../patches/attachments_controller_patch.rb | 104 ++ .../auto_completes_controller_patch.rb | 95 ++ .../patches/compatibility/2.3/query_patch.rb | 62 + .../compatibility/active_record_base_patch.rb | 84 ++ .../active_record_sanitization_patch.rb | 36 + .../compatibility/application_helper_patch.rb | 38 + .../patches/compatibility/user_patch.rb | 50 + .../patches/compatibility_patch.rb | 26 + .../patches/custom_fields_helper_patch.rb | 59 + .../redmine_contacts/patches/issue_patch.rb | 54 + .../patches/issue_query_patch.rb | 138 +++ .../patches/issues_controller_patch.rb | 47 + .../patches/issues_helper_patch.rb | 41 + .../redmine_contacts/patches/mailer_patch.rb | 115 ++ .../patches/notifiable_patch.rb | 52 + .../redmine_contacts/patches/project_patch.rb | 47 + .../patches/projects_helper_patch.rb | 58 + .../patches/queries_helper_patch.rb | 76 ++ .../patches/query_filter_patch.rb | 44 + .../redmine_contacts/patches/query_patch.rb | 68 ++ .../redmine_contacts/patches/setting_patch.rb | 73 ++ .../patches/settings_helper_patch.rb | 51 + .../patches/time_entry_query_patch.rb | 45 + .../patches/time_report_patch.rb | 54 + .../patches/timelog_helper_patch.rb | 50 + .../patches/users_controller_patch.rb | 51 + .../lib/redmine_contacts/utils/check_mail.rb | 120 ++ .../lib/redmine_contacts/utils/csv_utils.rb | 49 + .../lib/redmine_contacts/utils/date_utils.rb | 67 ++ .../lib/redmine_contacts/utils/thumbnail.rb | 51 + .../wiki_macros/contacts_wiki_macros.rb | 87 ++ .../lib/tasks/clear_tags_table.rake | 16 + .../redmine_contacts/lib/tasks/contacts.rake | 69 ++ .../lib/tasks/contacts_email.rake | 154 +++ .../test/fixtures/addresses.yml | 18 + .../test/fixtures/contacts.yml | 47 + .../test/fixtures/contacts_issues.yml | 10 + .../contacts_mailer/fwd_new_note_html.eml | 128 ++ .../contacts_mailer/fwd_new_note_plain.eml | 51 + .../contacts_mailer/new_deal_note_by_id.eml | 39 + .../contacts_mailer/new_deny_note.eml | 44 + .../fixtures/contacts_mailer/new_note.eml | 44 + .../contacts_mailer/new_note_by_id.eml | 43 + .../contacts_mailer/new_note_with_cc.eml | 44 + .../test/fixtures/contacts_projects.yml | 19 + .../test/fixtures/contacts_settings.yml | 13 + .../test/fixtures/deal_categories.yml | 9 + .../test/fixtures/deal_processes.yml | 27 + .../test/fixtures/deal_statuses.yml | 34 + .../test/fixtures/deal_statuses_projects.yml | 24 + .../redmine_contacts/test/fixtures/deals.yml | 59 + .../test/fixtures/deals_issues.yml | 10 + .../test/fixtures/files/contacts_cf.csv | 2 + .../test/fixtures/files/correct.csv | 5 + .../test/fixtures/files/deals_correct.csv | 2 + .../test/fixtures/files/image.jpg | Bin 0 -> 12197 bytes .../test/fixtures/files/kirill_bezrukov.vcf | 107 ++ .../test/fixtures/files/umlaut_card.vcf | 18 + .../fixtures/files/with_data_malformed.csv | 5 + .../redmine_contacts/test/fixtures/notes.yml | 41 + .../test/fixtures/queries.yml | 82 ++ .../test/fixtures/recently_vieweds.yml | 9 + .../test/fixtures/taggings.yml | 30 + .../redmine_contacts/test/fixtures/tags.yml | 7 + .../auto_completes_controller_test.rb | 148 +++ .../contact_imports_controller_test.rb | 158 +++ .../functional/contacts_controller_test.rb | 753 ++++++++++++ .../contacts_duplicates_controller_test.rb | 103 ++ .../contacts_issues_controller_test.rb | 138 +++ .../contacts_mailer_controller_test.rb | 75 ++ .../contacts_projects_controller_test.rb | 94 ++ .../contacts_settings_controller_test.rb | 60 + .../contacts_tags_controller_test.rb | 112 ++ .../contacts_vcf_controller_test.rb | 107 ++ .../functional/crm_queries_controller_test.rb | 138 +++ .../deal_categories_controller_test.rb | 120 ++ .../deal_imports_controller_test.rb | 151 +++ .../deal_statuses_controller_test.rb | 122 ++ .../test/functional/deals_controller_test.rb | 506 ++++++++ .../test/functional/issues_controller_test.rb | 149 +++ .../test/functional/notes_controller_test.rb | 91 ++ .../functional/queries_controller_test.rb | 45 + .../test/functional/search_controller_test.rb | 86 ++ .../functional/timelog_controller_test.rb | 89 ++ .../test/functional/users_controller_test.rb | 67 ++ .../test/functional/wiki_controller_test.rb | 112 ++ .../test/integration/api_test/contact.xml | 9 + .../api_test/contacts_projects_test.rb | 89 ++ .../integration/api_test/contacts_test.rb | 163 +++ .../test/integration/api_test/deal.xml | 6 + .../test/integration/api_test/deals_test.rb | 128 ++ .../test/integration/api_test/note.xml | 5 + .../test/integration/api_test/notes_test.rb | 113 ++ .../test/integration/common_views_test.rb | 160 +++ .../test/integration/routing_test.rb | 63 + plugins/redmine_contacts/test/test_helper.rb | 150 +++ .../test/unit/address_test.rb | 137 +++ .../test/unit/contact_import_test.rb | 66 ++ .../test/unit/contact_test.rb | 256 ++++ .../test/unit/contacts_issues_test.rb | 31 + .../test/unit/contacts_mailer_test.rb | 151 +++ .../unit/custom_field_company_format_test.rb | 78 ++ .../test/unit/deal_import_test.rb | 53 + .../test/unit/deal_status_test.rb | 77 ++ .../redmine_contacts/test/unit/deal_test.rb | 56 + .../unit/deals_pipeline_processor_test.rb | 96 ++ .../test/unit/helpers/contacts_helper_test.rb | 87 ++ .../test/unit/helpers/deals_helper_test.rb | 76 ++ .../test/unit/helpers/notes_helper_test.rb | 78 ++ .../unit/lib/contacts_project_setting_test.rb | 82 ++ .../test/unit/mailer_patch_test.rb | 124 ++ plugins/redmine_contacts_helpdesk/.drone.yml | 90 ++ .../LOCAL_CHANGELOG.md | 26 + .../canned_responses_controller.rb | 106 ++ .../app/controllers/helpdesk_controller.rb | 244 ++++ .../controllers/helpdesk_mailer_controller.rb | 44 + .../helpdesk_reports_controller.rb | 35 + .../controllers/helpdesk_search_controller.rb | 201 ++++ .../helpdesk_tickets_controller.rb | 67 ++ .../controllers/helpdesk_votes_controller.rb | 46 + .../controllers/helpdesk_widget_controller.rb | 206 ++++ .../controllers/mail_fetcher_controller.rb | 89 ++ .../controllers/public_tickets_controller.rb | 81 ++ .../app/helpers/helpdesk_helper.rb | 83 ++ .../app/helpers/helpdesk_mailer_helper.rb | 13 + .../app/helpers/public_tickets_helper.rb | 35 + .../app/models/canned_response.rb | 28 + .../helpdesk_data_collector_busiest_time.rb | 148 +++ .../helpdesk_data_collector_first_response.rb | 178 +++ .../models/helpdesk_data_collector_manager.rb | 14 + .../app/models/helpdesk_mailer.rb | 823 +++++++++++++ .../helpdesk_reports_busiest_time_query.rb | 18 + .../helpdesk_reports_first_response_query.rb | 21 + .../app/models/helpdesk_reports_query.rb | 94 ++ .../app/models/helpdesk_ticket.rb | 268 +++++ .../app/models/journal_message.rb | 58 + .../app/views/canned_responses/_form.html.erb | 41 + .../views/canned_responses/_index.html.erb | 37 + .../app/views/canned_responses/add.js.erb | 43 + .../app/views/canned_responses/edit.html.erb | 6 + .../app/views/canned_responses/index.html.erb | 1 + .../app/views/canned_responses/new.html.erb | 6 + .../views/contacts/_helpdesk_tickets.html.erb | 54 + .../context_menus/_helpdesk_contacts.html.erb | 13 + .../app/views/helpdesk/_index.html.erb | 81 ++ .../app/views/helpdesk/_list.html.erb | 66 ++ .../app/views/helpdesk/get_mail.js.erb | 1 + .../app/views/helpdesk/show.api.rsb | 7 + .../app/views/helpdesk/show.html.erb | 332 ++++++ .../helpdesk/update_customer_email.js.erb | 9 + .../views/helpdesk/update_ticket_data.js.erb | 1 + .../helpdesk_mailer/email_layout.html.erb | 21 + .../helpdesk_mailer/email_layout.text.erb | 3 + .../_busiest_time_of_day_metrics.html.erb | 25 + .../views/helpdesk_reports/_chart.html.erb | 33 + .../_first_response_time_metrics.html.erb | 33 + .../app/views/helpdesk_reports/show.html.erb | 29 + .../views/helpdesk_tickets/destroy.html.erb | 1 + .../app/views/helpdesk_tickets/edit.js.erb | 23 + .../views/helpdesk_tickets/update.html.erb | 1 + .../app/views/helpdesk_votes/show.html.erb | 28 + .../app/views/helpdesk_votes/vote.html.erb | 3 + .../app/views/helpdesk_widget/avatar.html.erb | 1 + .../app/views/helpdesk_widget/iframe.js.erb | 226 ++++ .../helpdesk_widget/load_custom_fields.erb | 6 + .../app/views/helpdesk_widget/widget.css | 135 +++ .../app/views/helpdesk_widget/widget.js.erb | 556 +++++++++ .../views/issues/_customer_to_email.html.erb | 2 + .../_helpdesk_customer_profile.html.erb | 87 ++ .../views/issues/_helpdesk_reports.html.erb | 6 + .../app/views/issues/_send_response.html.erb | 154 +++ .../app/views/issues/_ticket_data.html.erb | 56 + .../views/issues/_ticket_data_form.html.erb | 26 + .../views/journals/_journal_contact.html.erb | 41 + .../app/views/layouts/public_tickets.html.erb | 51 + .../my/blocks/_my_helpdesk_tickets.html.erb | 56 + .../views/projects/_helpdesk_tickets.html.erb | 10 + .../_helpdesk_canned_responses.html.erb | 43 + .../settings/_helpdesk_general.html.erb | 58 + .../settings/_helpdesk_server.html.erb | 184 +++ .../settings/_helpdesk_settings.html.erb | 28 + .../settings/_helpdesk_template.html.erb | 64 + .../public_tickets/_add_comment.html.erb | 8 + .../public_tickets/_attachment_links.html.erb | 13 + .../views/public_tickets/_history.html.erb | 11 + .../public_tickets/_sidebar_content.html.erb | 19 + .../app/views/public_tickets/show.html.erb | 79 ++ .../app/views/settings/_helpdesk.html.erb | 18 + .../_helpdesk_canned_responses.html.erb | 2 + .../views/settings/_helpdesk_general.html.erb | 50 + .../views/settings/_helpdesk_hidden.html.erb | 9 + .../views/settings/_helpdesk_public.html.erb | 20 + .../settings/_helpdesk_template.html.erb | 45 + .../views/settings/_helpdesk_vote.html.erb | 9 + .../app/views/settings/_helpdesk_widget.erb | 44 + .../assets/images/arrow_divide.png | Bin 0 -> 677 bytes .../assets/images/awesome.png | Bin 0 -> 795 bytes .../assets/images/bullet_down.png | Bin 0 -> 438 bytes .../assets/images/bullet_up.png | Bin 0 -> 439 bytes .../assets/images/email_error.png | Bin 0 -> 792 bytes .../assets/images/email_from.png | Bin 0 -> 3345 bytes .../assets/images/email_go.png | Bin 0 -> 754 bytes .../assets/images/just_ok.png | Bin 0 -> 766 bytes .../assets/images/magnifier.png | Bin 0 -> 615 bytes .../assets/images/not_good.png | Bin 0 -> 794 bytes .../assets/images/reply.png | Bin 0 -> 565 bytes .../assets/images/support.png | Bin 0 -> 837 bytes .../assets/images/user_comment.png | Bin 0 -> 743 bytes .../assets/images/world.png | Bin 0 -> 923 bytes .../assets/javascripts/redmine_helpdesk.js | 59 + .../assets/stylesheets/helpdesk.css | 370 ++++++ .../config/locales/de.yml | 113 ++ .../config/locales/en.yml | 247 ++++ .../config/locales/es.yml | 142 +++ .../config/locales/fr.yml | 196 +++ .../config/locales/it.yml | 154 +++ .../config/locales/nl.yml | 145 +++ .../config/locales/pl.yml | 123 ++ .../config/locales/pt-BR.yml | 174 +++ .../config/locales/ru.yml | 249 ++++ .../config/locales/sk.yml | 113 ++ .../config/locales/sr.yml | 145 +++ .../config/locales/sv.yml | 148 +++ .../config/locales/tr.yml | 174 +++ .../config/locales/zh.yml | 233 ++++ .../config/routes.rb | 52 + .../db/migrate/001_create_contact_journals.rb | 17 + .../db/migrate/002_create_helpdesk_tickets.rb | 52 + ...d_cc_and_message_id_to_helpdesk_tickets.rb | 10 + .../db/migrate/004_create_canned_responses.rb | 11 + ...005_add_is_incoming_to_helpdesk_tickets.rb | 5 + .../006_add_metrics_to_helpdesk_tickets.rb | 9 + .../007_populate_helpdesk_tickets_metrics.rb | 9 + .../008_add_vote_to_helpdesk_tickets.rb | 6 + .../redmine_contacts_helpdesk/doc/CHANGELOG | 311 +++++ plugins/redmine_contacts_helpdesk/doc/COPYING | 339 ++++++ plugins/redmine_contacts_helpdesk/doc/LICENSE | 26 + .../extra/rdm-helpdesk-mailhandler.rb | 166 +++ plugins/redmine_contacts_helpdesk/init.rb | 50 + .../lib/redmine_helpdesk.rb | 135 +++ .../controller_contacts_duplicates_hook.rb | 10 + .../hooks/helper_issues_hook.rb | 15 + .../hooks/issues_controller_hook.rb | 11 + .../hooks/view_contacts_hook.rb | 7 + .../hooks/view_issues_hook.rb | 13 + .../hooks/view_journals_hook.rb | 9 + .../hooks/view_layouts_hook.rb | 9 + .../hooks/view_projects_hook.rb | 10 + .../patches/application_helper_patch.rb | 45 + .../patches/attachments_controller_patch.rb | 27 + .../patches/compatibility_patch.rb | 5 + .../redmine_helpdesk/patches/contact_patch.rb | 47 + .../patches/contact_query_patch.rb | 57 + .../patches/contacts_helper_patch.rb | 26 + .../patches/gravatar_helper_patch.rb | 27 + .../redmine_helpdesk/patches/issue_patch.rb | 89 ++ .../patches/issue_query_patch.rb | 185 +++ .../patches/issues_controller_patch.rb | 82 ++ .../redmine_helpdesk/patches/journal_patch.rb | 84 ++ .../patches/journals_controller_patch.rb | 44 + .../patches/mail_handler_patch.rb | 99 ++ .../patches/projects_helper_patch.rb | 46 + .../patches/queries_helper_patch.rb | 63 + .../patches/time_report_patch.rb | 38 + .../wiki_macros/helpdesk_wiki_macro.rb | 26 + .../lib/tasks/helpdesk.rake | 51 + .../lib/tasks/helpdesk_mail.rake | 136 +++ .../test/fixtures/canned_responses.yml | 14 + .../fixtures/helpdesk_mailer/attachment.zip | Bin 0 -> 3855 bytes .../fixtures/helpdesk_mailer/auto_answer.eml | 67 ++ .../helpdesk_mailer/auto_answer_exchange.eml | 67 ++ .../helpdesk_mailer/emoji_message.eml | 66 ++ .../helpdesk_mailer/new_contact_bad_name.eml | 38 + .../new_contact_unicode_name.eml | 38 + .../new_issue_encoded_subject.eml | 30 + .../helpdesk_mailer/new_issue_french.eml | 240 ++++ .../helpdesk_mailer/new_issue_new_contact.eml | 44 + .../new_issue_new_contact_encode.eml | 38 + .../new_issue_new_contact_ru.eml | 94 ++ .../new_issue_new_contact_ru_2.eml | 101 ++ .../new_issue_new_contact_ru_4.eml | 59 + .../new_issue_new_contact_ru_5.eml | 81 ++ .../helpdesk_mailer/new_issue_no_subject.eml | 37 + .../helpdesk_mailer/new_issue_to_contact.eml | 38 + ...new_issue_without_text_with_attachment.eml | 37 + .../helpdesk_mailer/reply_from_contact.eml | 40 + .../helpdesk_mailer/reply_from_mail.eml | 26 + .../reply_from_mail_by_default.eml | 24 + .../reply_from_mail_with_keywords.eml | 28 + .../reply_from_mail_with_tag.eml | 26 + .../reply_from_mail_with_tag_in_cc.eml | 27 + .../helpdesk_mailer/reply_to_mail.eml | 44 + .../helpdesk_mailer/reply_with_attachment.eml | 248 ++++ .../reply_with_duplicated_attachment.eml | 177 +++ .../ticket_from_redmine_user.eml | 40 + .../helpdesk_mailer/ticket_html_only.eml | 21 + .../helpdesk_mailer/ticket_in_iso_8859_15.eml | 213 ++++ .../helpdesk_mailer/ticket_in_koi8_r.eml | 40 + .../helpdesk_mailer/ticket_in_win1251.eml | 44 + .../ticket_with_bq_encoding.eml | 101 ++ .../helpdesk_mailer/ticket_with_cc.eml | 39 + .../ticket_with_empty_from.eml | 38 + .../helpdesk_mailer/ticket_with_empty_to.eml | 39 + .../ticket_with_encoded_attachment.eml | 73 ++ .../ticket_with_in_reply_to.eml | 39 + .../helpdesk_mailer/ticket_with_japanese.eml | 60 + .../ticket_with_leading_spaces_to.eml | 40 + .../ticket_with_multiline_subject.eml | 40 + .../helpdesk_mailer/ticket_with_quotes.eml | 125 ++ .../ticket_with_rus_attachment.eml | 51 + .../helpdesk_mailer/ticket_without_name.eml | 40 + .../test/fixtures/helpdesk_tickets.yml | 37 + .../test/fixtures/journal_messages.yml | 15 + .../canned_responses_controller_test.rb | 85 ++ .../functional/contacts_controller_test.rb | 87 ++ .../contacts_duplicates_controller_test.rb | 60 + .../functional/helpdesk_controller_test.rb | 144 +++ .../helpdesk_mailer_controller_test.rb | 121 ++ .../helpdesk_reports_controller_test.rb | 129 ++ .../helpdesk_tickets_controller_test.rb | 85 ++ .../helpdesk_votes_controller_test.rb | 131 ++ .../test/functional/issues_controller_test.rb | 512 ++++++++ .../mail_fetcher_controller_test.rb | 8 + .../public_tickets_controller_test.rb | 115 ++ .../functional/timelog_controller_test.rb | 52 + .../integration/api_test/helpdesk_test.rb | 194 +++ .../test/integration/api_test/message.xml | 5 + .../test/integration/common_views_test.rb | 85 ++ .../test/test_helper.rb | 75 ++ .../test/unit/canned_response_test.rb | 9 + .../test/unit/helpdesk_mailer_test.rb | 1056 +++++++++++++++++ .../test/unit/helpdesk_ticket_test.rb | 200 ++++ .../test/unit/issue_query_patch_test.rb | 38 + .../test/unit/journal_message_test.rb | 10 + .../test/unit/mail_handler_patch_test.rb | 177 +++ plugins/redmine_event_outbox/README.md | 66 ++ .../app/models/event_outbox_event.rb | 15 + .../migrate/001_create_event_outbox_events.rb | 27 + plugins/redmine_event_outbox/init.rb | 11 + .../lib/redmine_event_outbox.rb | 36 + .../lib/redmine_event_outbox/event.rb | 268 +++++ .../redmine_event_outbox/hooks/issues_hook.rb | 24 + .../patches/contact_patch.rb | 22 + .../patches/helpdesk_ticket_patch.rb | 22 + .../patches/journal_message_patch.rb | 22 + .../patches/journal_patch.rb | 17 + .../lib/tasks/redmine_event_outbox.rake | 22 + redmine_contacts.py | 465 ++++++++ redmine_helpdesk_search.py | 471 ++++++++ redmine_outbox_worker.py | 504 ++++++++ reset_helpdesk_mail_settings.py | 285 +++++ 683 files changed, 56878 insertions(+) create mode 100644 .gitignore create mode 100644 AGENT.md create mode 100644 README.md create mode 100644 dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md create mode 100644 dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md create mode 100644 dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md create mode 100644 docs/event_outbox_spec.md create mode 100644 docs/pre_existing_issues.md create mode 100644 docs/redmineup_local_fork_changelog.md create mode 100644 plugins/README.md create mode 100644 plugins/redmine_contacts/Gemfile create mode 100644 plugins/redmine_contacts/README.rdoc create mode 100644 plugins/redmine_contacts/app/controllers/contact_imports_controller.rb create mode 100755 plugins/redmine_contacts/app/controllers/contacts_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_duplicates_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_issues_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_mailer_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_projects_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_settings_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_tags_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/contacts_vcf_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/crm_queries_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deal_categories_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deal_contacts_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deal_imports_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deal_statuses_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deals_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/deals_tasks_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/importer_base_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/notes_controller.rb create mode 100644 plugins/redmine_contacts/app/controllers/tasks_controller.rb create mode 100644 plugins/redmine_contacts/app/helpers/contacts_helper.rb create mode 100644 plugins/redmine_contacts/app/helpers/contacts_money_helper.rb create mode 100644 plugins/redmine_contacts/app/helpers/crm_queries_helper.rb create mode 100644 plugins/redmine_contacts/app/helpers/deals_helper.rb create mode 100644 plugins/redmine_contacts/app/helpers/notes_helper.rb create mode 100644 plugins/redmine_contacts/app/models/address.rb create mode 100755 plugins/redmine_contacts/app/models/contact.rb create mode 100644 plugins/redmine_contacts/app/models/contact_custom_field.rb create mode 100644 plugins/redmine_contacts/app/models/contact_import.rb create mode 100644 plugins/redmine_contacts/app/models/contact_kernel_import.rb create mode 100644 plugins/redmine_contacts/app/models/contact_note.rb create mode 100644 plugins/redmine_contacts/app/models/contact_query.rb create mode 100644 plugins/redmine_contacts/app/models/contacts_issue.rb create mode 100644 plugins/redmine_contacts/app/models/contacts_mailer.rb create mode 100644 plugins/redmine_contacts/app/models/contacts_setting.rb create mode 100644 plugins/redmine_contacts/app/models/crm_query.rb create mode 100644 plugins/redmine_contacts/app/models/deal.rb create mode 100644 plugins/redmine_contacts/app/models/deal_category.rb create mode 100644 plugins/redmine_contacts/app/models/deal_custom_field.rb create mode 100644 plugins/redmine_contacts/app/models/deal_import.rb create mode 100644 plugins/redmine_contacts/app/models/deal_kernel_import.rb create mode 100644 plugins/redmine_contacts/app/models/deal_note.rb create mode 100644 plugins/redmine_contacts/app/models/deal_process.rb create mode 100644 plugins/redmine_contacts/app/models/deal_query.rb create mode 100644 plugins/redmine_contacts/app/models/deal_status.rb create mode 100644 plugins/redmine_contacts/app/models/deals_issue.rb create mode 100644 plugins/redmine_contacts/app/models/deals_pipeline_processor.rb create mode 100644 plugins/redmine_contacts/app/models/note.rb create mode 100644 plugins/redmine_contacts/app/models/note_custom_field.rb create mode 100644 plugins/redmine_contacts/app/models/recently_viewed.rb create mode 100644 plugins/redmine_contacts/app/models/task.rb create mode 100644 plugins/redmine_contacts/app/views/auto_completes/_companies.html.erb create mode 100644 plugins/redmine_contacts/app/views/auto_completes/_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/auto_completes/_crm_tag_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/auto_completes/_deals.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_address_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_contact_data.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_contact_tabs.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_contacts_select2_data.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_notes_attachments.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_recently_viewed.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_responsible_user.html.erb create mode 100644 plugins/redmine_contacts/app/views/common/_sidebar.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_attributes.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_company_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_contact_card.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_custom_field_form.html.erb create mode 100755 plugins/redmine_contacts/app/views/contacts/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_form_tags.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_list_cards.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_list_excerpt.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_name_observer.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_new_modal.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_notes.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_tag_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_tags_cloud.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/_tags_item.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/bulk_edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/contacts_notes.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/context_menu.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/create.js.erb create mode 100755 plugins/redmine_contacts/app/views/contacts/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/edit_mails.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/index.api.rsb create mode 100755 plugins/redmine_contacts/app/views/contacts/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/load_tab.js.erb create mode 100755 plugins/redmine_contacts/app/views/contacts/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/new.js.erb create mode 100644 plugins/redmine_contacts/app/views/contacts/show.api.rsb create mode 100755 plugins/redmine_contacts/app/views/contacts/show.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_duplicates/_duplicates.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_duplicates/_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_duplicates/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_additional_assets.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_attributes.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_issue_item.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_issues.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/_new_modal.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/autocomplete_for_contact.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/close.js.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/create.js.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/delete.js.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_issues/new.js.erb create mode 100755 plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.html.erb create mode 100755 plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.text.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_projects/_related.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_projects/new.js.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_tags/_tags_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_tags/context_menu.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_tags/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_tags/index.api.rsb create mode 100644 plugins/redmine_contacts/app/views/contacts_tags/merge.html.erb create mode 100644 plugins/redmine_contacts/app/views/contacts_vcf/_load.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_calendars/_buttons.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_calendars/_crm_calendar.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_calendars/_deal_calendar_event.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_queries/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_queries/index.api.rsb create mode 100644 plugins/redmine_contacts/app/views/crm_queries/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/crm_queries/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_categories/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_categories/destroy.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_categories/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_categories/index.api.rsb create mode 100644 plugins/redmine_contacts/app/views/deal_categories/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/_new_modal.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/add.js.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/autocomplete.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/delete.js.erb create mode 100644 plugins/redmine_contacts/app/views/deal_contacts/search.js.erb create mode 100644 plugins/redmine_contacts/app/views/deal_statuses/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_statuses/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_statuses/index.api.rsb create mode 100644 plugins/redmine_contacts/app/views/deal_statuses/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/deal_statuses/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_attributes.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_board_deals_counts.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_board_total.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_custom_field_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_deals_statistics.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_line_fields.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_list_board.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_list_excerpt.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_list_pipeline.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_process_item.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/_related_deals.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/bulk_edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/context_menu.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/index.api.rsb create mode 100644 plugins/redmine_contacts/app/views/deals/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/show.api.rsb create mode 100644 plugins/redmine_contacts/app/views/deals/show.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals/update_form.js.erb create mode 100644 plugins/redmine_contacts/app/views/deals/update_total.js.erb create mode 100644 plugins/redmine_contacts/app/views/deals_issues/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals_issues/_issues.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals_issues/_show.html.erb create mode 100644 plugins/redmine_contacts/app/views/deals_pipeline/index.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/_preview.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/kernel_new.erb create mode 100644 plugins/redmine_contacts/app/views/importers/mapping.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/mapping/_contact.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/mapping/_contact_fields_mapping.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/mapping/_deal.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/mapping/_deal_fields_mapping.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/run.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/run.js.erb create mode 100644 plugins/redmine_contacts/app/views/importers/settings.html.erb create mode 100644 plugins/redmine_contacts/app/views/importers/show.html.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/_contact.text.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_contact_add.html.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_contact_add.text.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_deal_add.html.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_deal_add.text.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_deal_updated.html.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/crm_deal_updated.text.erb create mode 100755 plugins/redmine_contacts/app/views/mailer/crm_note_add.html.erb create mode 100755 plugins/redmine_contacts/app/views/mailer/crm_note_add.text.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/issue_connected.html.erb create mode 100644 plugins/redmine_contacts/app/views/mailer/issue_connected.text.erb create mode 100644 plugins/redmine_contacts/app/views/my/blocks/_my_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/my/blocks/_my_contacts_avatars.html.erb create mode 100644 plugins/redmine_contacts/app/views/my/blocks/_my_contacts_stats.html.erb create mode 100644 plugins/redmine_contacts/app/views/my/blocks/_my_deals.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_add.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_form.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_last_notes.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_note_data.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_note_header.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_note_item.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/_notes_list.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/create.js.erb create mode 100644 plugins/redmine_contacts/app/views/notes/destroy.js.erb create mode 100644 plugins/redmine_contacts/app/views/notes/edit.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/new.html.erb create mode 100644 plugins/redmine_contacts/app/views/notes/show.api.rsb create mode 100644 plugins/redmine_contacts/app/views/notes/show.html.erb create mode 100644 plugins/redmine_contacts/app/views/projects/_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/projects/_contacts_settings.html.erb create mode 100644 plugins/redmine_contacts/app/views/projects/_deal_statuses.html.erb create mode 100644 plugins/redmine_contacts/app/views/projects/_deals_settings.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_contacts.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_contacts_deal_statuses.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_contacts_general.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_contacts_hidden.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_contacts_tags.html.erb create mode 100644 plugins/redmine_contacts/app/views/settings/contacts/_money.html.erb create mode 100644 plugins/redmine_contacts/app/views/users/_contact.html.erb create mode 100644 plugins/redmine_contacts/assets/images/arrow_merge.png create mode 100644 plugins/redmine_contacts/assets/images/bullet_go.png create mode 100644 plugins/redmine_contacts/assets/images/calendar_view_day.png create mode 100644 plugins/redmine_contacts/assets/images/case.png create mode 100644 plugins/redmine_contacts/assets/images/clock_red.png create mode 100644 plugins/redmine_contacts/assets/images/company.png create mode 100644 plugins/redmine_contacts/assets/images/date.png create mode 100644 plugins/redmine_contacts/assets/images/deal.png create mode 100644 plugins/redmine_contacts/assets/images/email.png create mode 100644 plugins/redmine_contacts/assets/images/feed.png create mode 100755 plugins/redmine_contacts/assets/images/money.png create mode 100644 plugins/redmine_contacts/assets/images/money_dollar.png create mode 100644 plugins/redmine_contacts/assets/images/money_euro.png create mode 100644 plugins/redmine_contacts/assets/images/money_pound.png create mode 100644 plugins/redmine_contacts/assets/images/money_yen.png create mode 100644 plugins/redmine_contacts/assets/images/person.png create mode 100644 plugins/redmine_contacts/assets/images/phone.png create mode 100644 plugins/redmine_contacts/assets/images/rosette.png create mode 100644 plugins/redmine_contacts/assets/images/telephone.png create mode 100644 plugins/redmine_contacts/assets/images/unknown.png create mode 100755 plugins/redmine_contacts/assets/images/user_suit.png create mode 100644 plugins/redmine_contacts/assets/images/vcard.png create mode 100644 plugins/redmine_contacts/assets/javascripts/contacts.js create mode 100644 plugins/redmine_contacts/assets/javascripts/contacts_autocomplete.js create mode 100644 plugins/redmine_contacts/assets/javascripts/contacts_select2.js create mode 100755 plugins/redmine_contacts/assets/javascripts/jquery.colorPicker.min.js create mode 100755 plugins/redmine_contacts/assets/javascripts/tag-it.js create mode 100755 plugins/redmine_contacts/assets/stylesheets/colorPicker.css create mode 100644 plugins/redmine_contacts/assets/stylesheets/contacts.css create mode 100644 plugins/redmine_contacts/assets/stylesheets/contacts_sidebar.css create mode 100755 plugins/redmine_contacts/assets/stylesheets/jquery.tagit.css create mode 100644 plugins/redmine_contacts/config/locales/az.yml create mode 100644 plugins/redmine_contacts/config/locales/cs.yml create mode 100644 plugins/redmine_contacts/config/locales/da.yml create mode 100644 plugins/redmine_contacts/config/locales/de.yml create mode 100644 plugins/redmine_contacts/config/locales/el.yml create mode 100755 plugins/redmine_contacts/config/locales/en.yml create mode 100644 plugins/redmine_contacts/config/locales/es.yml create mode 100644 plugins/redmine_contacts/config/locales/fr.yml create mode 100644 plugins/redmine_contacts/config/locales/hu.yml create mode 100644 plugins/redmine_contacts/config/locales/it.yml create mode 100644 plugins/redmine_contacts/config/locales/ja.yml create mode 100644 plugins/redmine_contacts/config/locales/ko.yml create mode 100644 plugins/redmine_contacts/config/locales/nl.yml create mode 100755 plugins/redmine_contacts/config/locales/no.yml create mode 100644 plugins/redmine_contacts/config/locales/pl.yml create mode 100644 plugins/redmine_contacts/config/locales/pt-BR.yml create mode 100644 plugins/redmine_contacts/config/locales/ru.yml create mode 100644 plugins/redmine_contacts/config/locales/sk.yml create mode 100644 plugins/redmine_contacts/config/locales/sr-YU.yml create mode 100644 plugins/redmine_contacts/config/locales/sr.yml create mode 100644 plugins/redmine_contacts/config/locales/sv.yml create mode 100644 plugins/redmine_contacts/config/locales/tr.yml create mode 100644 plugins/redmine_contacts/config/locales/vi.yml create mode 100644 plugins/redmine_contacts/config/locales/zh-TW.yml create mode 100644 plugins/redmine_contacts/config/locales/zh.yml create mode 100644 plugins/redmine_contacts/config/routes.rb create mode 100644 plugins/redmine_contacts/db/migrate/016_create_contacts.rb create mode 100644 plugins/redmine_contacts/db/migrate/017_create_contacts_relations.rb create mode 100644 plugins/redmine_contacts/db/migrate/018_create_deals.rb create mode 100644 plugins/redmine_contacts/db/migrate/019_create_deals_relations.rb create mode 100644 plugins/redmine_contacts/db/migrate/020_create_notes.rb create mode 100644 plugins/redmine_contacts/db/migrate/021_create_tags.rb create mode 100644 plugins/redmine_contacts/db/migrate/022_create_recently_vieweds.rb create mode 100644 plugins/redmine_contacts/db/migrate/023_create_contacts_settings.rb create mode 100644 plugins/redmine_contacts/db/migrate/024_add_type_to_notes.rb create mode 100644 plugins/redmine_contacts/db/migrate/025_add_fields_to_deals.rb create mode 100644 plugins/redmine_contacts/db/migrate/026_create_contacts_queries.rb create mode 100644 plugins/redmine_contacts/db/migrate/027_change_deals_currency_type.rb create mode 100644 plugins/redmine_contacts/db/migrate/028_add_cached_tag_list_to_contacts.rb create mode 100644 plugins/redmine_contacts/db/migrate/029_add_visibility_to_contacts.rb create mode 100644 plugins/redmine_contacts/db/migrate/030_change_deal_statuses_is_closed.rb create mode 100644 plugins/redmine_contacts/db/migrate/031_populate_contacts_module.rb create mode 100644 plugins/redmine_contacts/db/migrate/032_create_addresses.rb create mode 100644 plugins/redmine_contacts/db/migrate/033_create_deals_issues.rb create mode 100644 plugins/redmine_contacts/db/migrate/034_change_deals_price_precision.rb create mode 100644 plugins/redmine_contacts/doc/CHANGELOG create mode 100644 plugins/redmine_contacts/doc/COPYING create mode 100644 plugins/redmine_contacts/doc/LICENSE create mode 100755 plugins/redmine_contacts/init.rb create mode 100644 plugins/redmine_contacts/lib/acts_as_priceable/init.rb create mode 100644 plugins/redmine_contacts/lib/acts_as_priceable/lib/acts_as_priceable.rb create mode 100644 plugins/redmine_contacts/lib/acts_as_taggable_on_patch.rb create mode 100644 plugins/redmine_contacts/lib/acts_as_viewable/init.rb create mode 100644 plugins/redmine_contacts/lib/acts_as_viewable/lib/acts_as_viewable.rb create mode 100644 plugins/redmine_contacts/lib/company_custom_field_format.rb create mode 100644 plugins/redmine_contacts/lib/csv_importable.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/contacts_project_setting.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/helpers/contacts_helper.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/helpers/crm_calendar_helper.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/helpers/money_helper.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/controllers_time_entry_reports_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/views_custom_fields_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/views_issues_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/views_layouts_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/views_projects_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/hooks/views_users_hook.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/addresses_drop.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/contacts_drop.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/deals_drop.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/notes_drop.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/liquid/liquid.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/action_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/application_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/attachments_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/auto_completes_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/2.3/query_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_base_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_sanitization_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/application_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/user_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/custom_fields_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/issue_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/issue_query_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/issues_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/issues_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/mailer_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/notifiable_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/project_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/projects_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/queries_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/query_filter_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/query_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/setting_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/settings_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/time_entry_query_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/time_report_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/timelog_helper_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/patches/users_controller_patch.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/utils/csv_utils.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/utils/date_utils.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/utils/thumbnail.rb create mode 100644 plugins/redmine_contacts/lib/redmine_contacts/wiki_macros/contacts_wiki_macros.rb create mode 100644 plugins/redmine_contacts/lib/tasks/clear_tags_table.rake create mode 100644 plugins/redmine_contacts/lib/tasks/contacts.rake create mode 100644 plugins/redmine_contacts/lib/tasks/contacts_email.rake create mode 100644 plugins/redmine_contacts/test/fixtures/addresses.yml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts.yml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_issues.yml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_html.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_plain.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deal_note_by_id.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deny_note.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_by_id.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_with_cc.eml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_projects.yml create mode 100644 plugins/redmine_contacts/test/fixtures/contacts_settings.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deal_categories.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deal_processes.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deal_statuses.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deal_statuses_projects.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deals.yml create mode 100644 plugins/redmine_contacts/test/fixtures/deals_issues.yml create mode 100644 plugins/redmine_contacts/test/fixtures/files/contacts_cf.csv create mode 100644 plugins/redmine_contacts/test/fixtures/files/correct.csv create mode 100644 plugins/redmine_contacts/test/fixtures/files/deals_correct.csv create mode 100644 plugins/redmine_contacts/test/fixtures/files/image.jpg create mode 100644 plugins/redmine_contacts/test/fixtures/files/kirill_bezrukov.vcf create mode 100644 plugins/redmine_contacts/test/fixtures/files/umlaut_card.vcf create mode 100644 plugins/redmine_contacts/test/fixtures/files/with_data_malformed.csv create mode 100644 plugins/redmine_contacts/test/fixtures/notes.yml create mode 100644 plugins/redmine_contacts/test/fixtures/queries.yml create mode 100644 plugins/redmine_contacts/test/fixtures/recently_vieweds.yml create mode 100644 plugins/redmine_contacts/test/fixtures/taggings.yml create mode 100644 plugins/redmine_contacts/test/fixtures/tags.yml create mode 100644 plugins/redmine_contacts/test/functional/auto_completes_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contact_imports_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_duplicates_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_issues_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_mailer_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_projects_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_settings_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_tags_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/contacts_vcf_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/crm_queries_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/deal_categories_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/deal_imports_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/deal_statuses_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/deals_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/issues_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/notes_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/queries_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/search_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/timelog_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/users_controller_test.rb create mode 100644 plugins/redmine_contacts/test/functional/wiki_controller_test.rb create mode 100644 plugins/redmine_contacts/test/integration/api_test/contact.xml create mode 100644 plugins/redmine_contacts/test/integration/api_test/contacts_projects_test.rb create mode 100644 plugins/redmine_contacts/test/integration/api_test/contacts_test.rb create mode 100644 plugins/redmine_contacts/test/integration/api_test/deal.xml create mode 100644 plugins/redmine_contacts/test/integration/api_test/deals_test.rb create mode 100644 plugins/redmine_contacts/test/integration/api_test/note.xml create mode 100644 plugins/redmine_contacts/test/integration/api_test/notes_test.rb create mode 100644 plugins/redmine_contacts/test/integration/common_views_test.rb create mode 100644 plugins/redmine_contacts/test/integration/routing_test.rb create mode 100644 plugins/redmine_contacts/test/test_helper.rb create mode 100644 plugins/redmine_contacts/test/unit/address_test.rb create mode 100644 plugins/redmine_contacts/test/unit/contact_import_test.rb create mode 100644 plugins/redmine_contacts/test/unit/contact_test.rb create mode 100644 plugins/redmine_contacts/test/unit/contacts_issues_test.rb create mode 100644 plugins/redmine_contacts/test/unit/contacts_mailer_test.rb create mode 100644 plugins/redmine_contacts/test/unit/custom_field_company_format_test.rb create mode 100644 plugins/redmine_contacts/test/unit/deal_import_test.rb create mode 100644 plugins/redmine_contacts/test/unit/deal_status_test.rb create mode 100644 plugins/redmine_contacts/test/unit/deal_test.rb create mode 100644 plugins/redmine_contacts/test/unit/deals_pipeline_processor_test.rb create mode 100644 plugins/redmine_contacts/test/unit/helpers/contacts_helper_test.rb create mode 100644 plugins/redmine_contacts/test/unit/helpers/deals_helper_test.rb create mode 100644 plugins/redmine_contacts/test/unit/helpers/notes_helper_test.rb create mode 100644 plugins/redmine_contacts/test/unit/lib/contacts_project_setting_test.rb create mode 100644 plugins/redmine_contacts/test/unit/mailer_patch_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/.drone.yml create mode 100644 plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/canned_responses_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_mailer_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_reports_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_tickets_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_votes_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_widget_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/mail_fetcher_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/controllers/public_tickets_controller.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_helper.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_mailer_helper.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/helpers/public_tickets_helper.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/canned_response.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_busiest_time.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_first_response.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_manager.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_busiest_time_query.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_first_response_query.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_query.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/helpdesk_ticket.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/models/journal_message.rb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/_form.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/_index.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/add.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/edit.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/index.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/canned_responses/new.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/contacts/_helpdesk_tickets.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/context_menus/_helpdesk_contacts.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/_index.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/_list.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/get_mail.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.api.rsb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_customer_email.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_ticket_data.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.text.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_busiest_time_of_day_metrics.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_chart.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_first_response_time_metrics.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/show.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/destroy.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/edit.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/update.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/show.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/vote.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/avatar.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/iframe.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/load_custom_fields.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.css create mode 100644 plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.js.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_customer_to_email.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_customer_profile.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_reports.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_send_response.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data_form.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/journals/_journal_contact.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/layouts/public_tickets.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/my/blocks/_my_helpdesk_tickets.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/_helpdesk_tickets.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_canned_responses.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_general.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_server.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_settings.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_template.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/public_tickets/_add_comment.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/public_tickets/_attachment_links.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/public_tickets/_history.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/public_tickets/_sidebar_content.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/public_tickets/show.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_canned_responses.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_general.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_hidden.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_public.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_template.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_vote.html.erb create mode 100644 plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_widget.erb create mode 100755 plugins/redmine_contacts_helpdesk/assets/images/arrow_divide.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/awesome.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/bullet_down.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/bullet_up.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/email_error.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/email_from.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/email_go.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/just_ok.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/magnifier.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/not_good.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/reply.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/support.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/user_comment.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/images/world.png create mode 100644 plugins/redmine_contacts_helpdesk/assets/javascripts/redmine_helpdesk.js create mode 100644 plugins/redmine_contacts_helpdesk/assets/stylesheets/helpdesk.css create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/de.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/en.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/es.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/fr.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/it.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/nl.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/pl.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/pt-BR.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/ru.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/sk.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/sr.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/sv.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/tr.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/locales/zh.yml create mode 100644 plugins/redmine_contacts_helpdesk/config/routes.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/001_create_contact_journals.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/002_create_helpdesk_tickets.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/003_add_cc_and_message_id_to_helpdesk_tickets.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/004_create_canned_responses.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/005_add_is_incoming_to_helpdesk_tickets.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/006_add_metrics_to_helpdesk_tickets.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/007_populate_helpdesk_tickets_metrics.rb create mode 100644 plugins/redmine_contacts_helpdesk/db/migrate/008_add_vote_to_helpdesk_tickets.rb create mode 100644 plugins/redmine_contacts_helpdesk/doc/CHANGELOG create mode 100644 plugins/redmine_contacts_helpdesk/doc/COPYING create mode 100644 plugins/redmine_contacts_helpdesk/doc/LICENSE create mode 100644 plugins/redmine_contacts_helpdesk/extra/rdm-helpdesk-mailhandler.rb create mode 100644 plugins/redmine_contacts_helpdesk/init.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/controller_contacts_duplicates_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/helper_issues_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/issues_controller_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_contacts_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_issues_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_journals_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_layouts_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_projects_hook.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/application_helper_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/attachments_controller_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/compatibility_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_query_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contacts_helper_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/gravatar_helper_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_query_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issues_controller_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journal_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journals_controller_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/mail_handler_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/projects_helper_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/queries_helper_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/time_report_patch.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/wiki_macros/helpdesk_wiki_macro.rb create mode 100644 plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk.rake create mode 100644 plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk_mail.rake create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/canned_responses.yml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/attachment.zip create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer_exchange.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/emoji_message.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_bad_name.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_unicode_name.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_encoded_subject.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_french.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_encode.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_2.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_4.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_5.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_no_subject.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_to_contact.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_without_text_with_attachment.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_contact.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_by_default.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_keywords.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag_in_cc.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_to_mail.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_attachment.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_duplicated_attachment.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_from_redmine_user.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_html_only.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_iso_8859_15.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_koi8_r.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_win1251.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_bq_encoding.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_cc.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_from.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_to.eml create mode 100755 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_encoded_attachment.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_in_reply_to.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_japanese.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_leading_spaces_to.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_multiline_subject.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_quotes.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_rus_attachment.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_without_name.eml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_tickets.yml create mode 100644 plugins/redmine_contacts_helpdesk/test/fixtures/journal_messages.yml create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/canned_responses_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/contacts_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/contacts_duplicates_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/helpdesk_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/helpdesk_mailer_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/helpdesk_reports_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/helpdesk_tickets_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/helpdesk_votes_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/issues_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/mail_fetcher_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/public_tickets_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/functional/timelog_controller_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/integration/api_test/helpdesk_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/integration/api_test/message.xml create mode 100644 plugins/redmine_contacts_helpdesk/test/integration/common_views_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/test_helper.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/canned_response_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/helpdesk_mailer_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/helpdesk_ticket_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/issue_query_patch_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/journal_message_test.rb create mode 100644 plugins/redmine_contacts_helpdesk/test/unit/mail_handler_patch_test.rb create mode 100644 plugins/redmine_event_outbox/README.md create mode 100644 plugins/redmine_event_outbox/app/models/event_outbox_event.rb create mode 100644 plugins/redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb create mode 100644 plugins/redmine_event_outbox/init.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/event.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/hooks/issues_hook.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/contact_patch.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/helpdesk_ticket_patch.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_message_patch.rb create mode 100644 plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_patch.rb create mode 100644 plugins/redmine_event_outbox/lib/tasks/redmine_event_outbox.rake create mode 100755 redmine_contacts.py create mode 100755 redmine_helpdesk_search.py create mode 100755 redmine_outbox_worker.py create mode 100755 reset_helpdesk_mail_settings.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe8a0ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.cache/ +/__pycache__/ +/redmine-copy/ +/dist/*.tar.gz +.env +*.pyc diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..18de2ed --- /dev/null +++ b/AGENT.md @@ -0,0 +1,338 @@ +# AGENT.md + +This file is a restart brief for future agent sessions on this repository. + +## Project Identity + +This repository supports a legacy, business-critical Redmine 3.4.4 +installation. The real purpose is to make CRM/helpdesk communication data +searchable and automatable without a risky near-term platform migration. + +The LAN copy used for testing is: + +```text +http://192.168.50.170/ +``` + +This is not a greenfield app. Treat it as a local-fork-and-integration project +around an existing Redmine install and old RedmineUP plugins. + +Tracked local plugin source lives in `plugins/`. The full `redmine-copy/` tree +is ignored and should be treated as a working/reference copy, not the source of +truth for local plugin changes. + +## Original Motivation + +The business stores most external communication inside Redmine through: + +- `redmine_contacts` +- `redmine_contacts_helpdesk` + +The default UI/API is not good enough for: + +- efficient contact search +- light contact maintenance +- helpdesk message search +- customer communication timeline lookup +- future semantic/vector search + +The key realization from prior work: + +```text +Helpdesk-created issues may have Anonymous issue authors. +The real customer identity lives in helpdesk_tickets, journal_messages, and contacts. +``` + +Never design the search/index layer as if `issues.author` were enough. + +## High-Level Architecture Decision + +The intended architecture is: + +1. Redmine writes local outbox rows. +2. External worker reads those rows. +3. Worker enriches from read-only MySQL joins. +4. Worker builds ticket/message documents. +5. External index stores/searches those documents. + +Safety rule: + +```text +External failures must not break Redmine saves. +``` + +That is why the event boundary is local DB outbox, not request-time webhooks or +embedding calls. + +## Redmine And Plugin Baseline + +- Redmine version: `3.4.4` +- Old RedmineUP plugin versions in use: + - `redmine_contacts` 4.1.2 PRO + - `redmine_contacts_helpdesk` 3.0.9 PRO +- Near-term upgrade to newer Redmine/RedmineUP is not the current goal. +- Treat these plugins as local legacy code when necessary. + +## Repository Landmarks + +Top-level docs: + +- [README.md](/home/iadnah/redmine/README.md:1) +- [docs/event_outbox_spec.md](/home/iadnah/redmine/docs/event_outbox_spec.md:1) +- [docs/redmineup_local_fork_changelog.md](/home/iadnah/redmine/docs/redmineup_local_fork_changelog.md:1) +- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1) + +Main scripts: + +- [redmine_contacts.py](/home/iadnah/redmine/redmine_contacts.py:1) +- [redmine_helpdesk_search.py](/home/iadnah/redmine/redmine_helpdesk_search.py:1) +- [redmine_outbox_worker.py](/home/iadnah/redmine/redmine_outbox_worker.py:1) + +Local Redmine copy: + +- [redmine-copy](/home/iadnah/redmine/redmine-copy) + +Important local plugin paths: + +- [redmine-copy/plugins/redmine_event_outbox](/home/iadnah/redmine/redmine-copy/plugins/redmine_event_outbox) +- [redmine-copy/plugins/redmine_contacts_helpdesk](/home/iadnah/redmine/redmine-copy/plugins/redmine_contacts_helpdesk) + +## What Has Already Been Done + +### Contact CLI + +`redmine_contacts.py` exists and supports: + +- contact fetch to local cache +- fuzzy-ish contact search +- light contact updates +- helpdesk read API calls: + - ticket by issue + - issues by contact + - messages by issue + - contact timeline + +### Event Outbox Plugin + +`redmine_event_outbox` exists and the known-good archive is: + +- `dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz` + +Known-good tested LAN events: + +- `issue.created` +- `issue.updated` +- `journal.created` +- `contact.created` +- `contact.updated` + +Local code also adds optional helpdesk events: + +- `helpdesk_ticket.created` +- `helpdesk_ticket.updated` +- `journal_message.created` +- `journal_message.updated` + +These helpdesk event paths are implemented locally but should still be treated +as needing fuller end-to-end validation. + +### Helpdesk Read API In Local Plugin Fork + +The local `redmine_contacts_helpdesk` fork includes: + +- `helpdesk_search_controller.rb` +- routes under `/helpdesk_search/*` +- alias/usage routes to avoid noisy routing errors + +This was deployed to the LAN copy and route-loaded successfully. + +### Helpdesk Export/Search CLI + +`redmine_helpdesk_search.py` was created to prove the data model and export +path. It: + +- SSHes to the LAN Redmine host +- reads remote MySQL credentials from `config/database.yml` +- runs read-only MySQL queries +- exports ticket/message docs to local JSONL cache +- provides rough local search/timeline/issues-by-contact commands + +Important: + +```text +This script is a diagnostic/export tool, not the final search architecture. +``` + +Do not get dragged into treating local CLI search speed as the core mission. + +### External Outbox Worker Prototype + +`redmine_outbox_worker.py` now exists as the first worker/indexer boundary. +It: + +- reads and optionally claims pending `event_outbox_events` over SSH/MySQL +- supports `--dry-run` for non-mutating previews +- uses `locked_at`, `locked_by`, `processed_at`, `attempts`, and `last_error` +- enriches helpdesk ticket/message/contact events with read-only joins +- writes derived JSONL to `.cache/redmine_outbox/derived_documents.jsonl` +- marks rows processed only after a successful local write + +This is still a prototype output target, not the final vector index. + +## LAN Host Facts + +These were verified previously: + +- SSH host/user: `reddev@192.168.50.170` +- SSH key used previously: `/tmp/reddev` +- remote Redmine path: `/usr/share/redmine` +- remote Ruby: `2.5.1` +- remote Bundler: `1.17.3` +- remote DB: MySQL 5.7 +- remote plugin rollback archives were stored in: + - `/home/reddev/redmine-plugin-backups/` + +If using those exact credentials again, verify they still exist before relying +on them. + +## Remote Plugin Changes Previously Deployed + +Changes were copied to the LAN Redmine host for: + +- `plugins/redmine_event_outbox/...` +- `plugins/redmine_contacts_helpdesk/...` + +Passenger was restarted with: + +```text +touch /usr/share/redmine/tmp/restart.txt +``` + +Routes were verified by remote `rake routes`. + +## Fork Hygiene Rules + +Before editing RedmineUP plugin code: + +1. create a rollback archive in `dist/` +2. record the change in: + - `docs/redmineup_local_fork_changelog.md` + - plugin-local changelog if appropriate +3. keep edits scoped +4. prefer read-only APIs/endpoints first +5. validate on the LAN copy before claiming the change is done + +Existing rollback archives: + +- `dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz` +- `dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz` + +## Important Pre-Existing Issues + +Read: + +- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1) + +Especially remember: + +1. duplicate `id="avatar"` is likely a real long-standing UI bug +2. `attachments/contacts_thumbnail` digest warning is probably log noise +3. `acts_as_list` warning is an upgrade-compatibility note, not a current + blocker + +Do not confuse those with the main search deliverable unless they directly block +the work in front of you. + +## Current Strategic State + +The project is past the "can we get data out?" phase. + +We now know: + +- contact JSON access exists +- helpdesk/customer identity can be extracted +- outbox events can be recorded safely +- helpdesk read API routes can be added locally +- full ticket/message export is feasible + +The most useful next step is: + +```text +Build the external worker/indexer pipeline. +``` + +Not: + +- polishing a local CLI into a full search product +- building a big Redmine admin UI +- chasing unrelated legacy plugin cleanup + +## Recommended Next Steps For A Future Session + +1. Re-read: + - `README.md` + - `docs/event_outbox_spec.md` + - `docs/redmineup_local_fork_changelog.md` + - `docs/pre_existing_issues.md` +2. Verify local and remote state: + - plugin files present + - LAN host reachable + - remote routes still load +3. Validate `redmine_outbox_worker.py` end to end against the LAN copy +4. Refine read-only joins that enrich: + - `helpdesk_tickets` + - `journal_messages` + - `contacts` + - `issues` + - `journals` +5. Confirm deterministic ticket/message/contact JSONL output +6. Only after that, connect Qdrant and embeddings + +## Query/Data Model Notes + +Authoritative helpdesk context lives in: + +- `helpdesk_tickets` + - `issue_id` + - `contact_id` + - `from_address` + - `to_address` + - `cc_address` + - `message_id` + - `ticket_date` + - `source` +- `journal_messages` + - `journal_id` + - `contact_id` + - `from_address` + - `to_address` + - `cc_address` + - `bcc_address` + - `message_id` + - `is_incoming` + - `message_date` +- `contacts` +- `issues` +- `journals` + +The worker/indexer should prefer direct DB joins as the source of truth for this +layer. + +## What To Be Careful About + +- Do not assume Redmine core issue API data is enough for helpdesk search. +- Do not treat old RedmineUP code as untouchable vendor code. +- Do not claim a plugin change is validated just because `ruby -c` passes. +- Do not burn time optimizing local CLI search if the real goal is external + indexing. +- Do not revert unrelated repo changes; the Redmine tree may be dirty. + +## If You Need A One-Paragraph Summary + +This project exists to safely extract and search real CRM/helpdesk communication +history from a legacy Redmine 3.4.4 install. We already have a contact CLI, a +local event outbox plugin, local helpdesk read API routes, and a rough +helpdesk-export CLI. The next meaningful milestone is an external worker that +consumes outbox rows, enriches from helpdesk/contact MySQL tables, and builds +deterministic ticket/message documents for a future Qdrant/OpenAI-backed search +index. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f760db --- /dev/null +++ b/README.md @@ -0,0 +1,427 @@ +# Legacy Redmine Search And Integration Project + +This repository exists to make a heavily customized, business-critical Redmine +3.4.4 installation easier to work with without forcing a risky near-term +platform upgrade. + +The original production-like copy used for testing is on the LAN at: + +```text +http://192.168.50.170/ +``` + +This project started because a large amount of customer/vendor communication is +stored in Redmine through the old RedmineUP CRM and Helpdesk plugins, but the +default UI and documented APIs are not good enough for operational search, +contact lookup, message history review, and future automation. + +The goal is not "modernize Redmine" in one jump. The goal is to create safe, +pragmatic tooling around the existing system so the business can search and use +its real communication history. + +## Why This Project Exists + +The Redmine install has been tightly integrated into day-to-day business +operations. It stores: + +- CRM contacts +- helpdesk tickets +- helpdesk email conversations +- issue status and assignment history +- internal journal notes + +The old RedmineUP plugin stack is effectively local legacy code now: + +- `redmine_contacts` 4.1.2 PRO +- `redmine_contacts_helpdesk` 3.0.9 PRO + +Tracked local plugin source lives under: + +- [plugins](/home/iadnah/redmine/plugins) + +The full `redmine-copy/` tree is an ignored working/reference copy of the legacy +install. Make local plugin changes in `plugins/` first, then deploy or copy them +into the test Redmine instance or `redmine-copy/` as needed. + +There is no realistic short-term plan to: + +- migrate to a newer RedmineUP package +- upgrade cleanly to Redmine 6 +- replace the whole system before extracting value from the data already inside + it + +So this repository treats those plugins as maintainable local forks where needed. + +## Core Problem We Are Solving + +The main operational need is better search over real customer/vendor +communications. + +The crucial discovery was that core Redmine issue data is not sufficient on its +own. In the helpdesk workflow, the issue author can be `Anonymous`, while the +actual customer identity and email metadata live in: + +- `helpdesk_tickets` +- `journal_messages` +- `contacts` + +That means any serious search/indexing system must treat helpdesk data as +first-class and must not rely only on Redmine issue API fields. + +## Architecture Direction + +The working architecture is: + +1. Redmine records low-risk local outbox events. +2. An external worker reads outbox rows. +3. The worker enriches those rows with read-only MySQL joins against + helpdesk/contact tables. +4. The worker builds ticket-level and message-level documents. +5. Those documents are indexed externally, eventually with vector search + support. + +The important safety rule is: + +```text +External search/indexing failures must not break normal Redmine saves. +``` + +That is why the event boundary lives in a local DB outbox table rather than in +request-time webhooks, brokers, or direct embedding calls. + +## What Has Been Finished + +### 1. Contact API Exploration And CLI + +The old RedmineUP contacts plugin already exposes useful JSON routes such as: + +```text +/projects/customer-service/contacts.json +``` + +That led to the first standalone helper: + +- [redmine_contacts.py](/home/iadnah/redmine/redmine_contacts.py:1) + +It currently supports: + +- fetching contacts to a local cache +- fuzzy-ish contact search over cached JSON +- light contact updates through the Redmine API +- helpdesk metadata lookup through the local read-only helpdesk API routes + +### 2. Event Outbox Plugin + +A small plugin was created at: + +- [redmine-copy/plugins/redmine_event_outbox](/home/iadnah/redmine/redmine-copy/plugins/redmine_event_outbox) + +It records local database events into `event_outbox_events`. + +Known-good archive: + +- [dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz](/home/iadnah/redmine/dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz) +- [manifest](/home/iadnah/redmine/dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md:1) + +Tested event types on the LAN copy: + +- `issue.created` +- `issue.updated` +- `journal.created` +- `contact.created` +- `contact.updated` + +Planned/implemented locally but not fully LAN-validated as a complete workflow: + +- `helpdesk_ticket.created` +- `helpdesk_ticket.updated` +- `journal_message.created` +- `journal_message.updated` + +### 3. Local Helpdesk Plugin Fork Changes + +We made targeted changes to the local fork of `redmine_contacts_helpdesk`: + +- added a read-only JSON controller: + - [helpdesk_search_controller.rb](/home/iadnah/redmine/redmine-copy/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb:1) +- added routes for: + - ticket by issue + - issues by contact + - messages by issue + - contact timeline +- added shorter alias/usage routes to avoid noisy routing errors in logs +- guarded those endpoints with the existing `view_helpdesk_tickets` permission + +These changes were deployed to the LAN Redmine copy and route-loaded +successfully. + +### 4. Local Fork Hygiene + +Before touching the RedmineUP plugin forks, rollback archives were created: + +- [redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz](/home/iadnah/redmine/dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz) +- [redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz](/home/iadnah/redmine/dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz) + +Manifests: + +- [contacts manifest](/home/iadnah/redmine/dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md:1) +- [helpdesk manifest](/home/iadnah/redmine/dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md:1) + +Change tracking docs: + +- [docs/redmineup_local_fork_changelog.md](/home/iadnah/redmine/docs/redmineup_local_fork_changelog.md:1) +- [redmine-copy/plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md](/home/iadnah/redmine/redmine-copy/plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md:1) + +### 5. Read-Only Helpdesk Export/Search CLI + +We also built: + +- [redmine_helpdesk_search.py](/home/iadnah/redmine/redmine_helpdesk_search.py:1) + +Purpose: + +- prove that helpdesk/customer/message data can be pulled correctly using + read-only SQL joins +- produce ticket-level and message-level documents locally +- provide rough CLI search/timeline tooling for debugging and validation + +Current state: + +- works over SSH to the LAN Redmine host +- reads MySQL credentials from remote `config/database.yml` +- fetches helpdesk ticket and journal message documents into local JSONL cache +- can search/timeline/issues-by-contact over the local cache + +Important note: + +This script is a diagnostic/export tool, not the final search architecture. We +intentionally stopped short of treating CLI speed optimization as the main goal. + +### 6. External Outbox Worker Prototype + +The first external worker prototype is: + +- [redmine_outbox_worker.py](/home/iadnah/redmine/redmine_outbox_worker.py:1) + +It runs outside Redmine and consumes `event_outbox_events` over SSH/MySQL. The +initial output target is deterministic local JSONL rather than a live search +service: + +- default output: `.cache/redmine_outbox/derived_documents.jsonl` + +Current behavior: + +- dry-runs pending rows without marking them processed +- claims bounded batches with `locked_at` and `locked_by` +- enriches helpdesk ticket/message/contact-related events with read-only joins +- writes derived event/ticket/message/contact documents as JSONL +- marks rows processed only after a successful local write +- increments `attempts` and writes `last_error` when processing fails + +### 7. Test Helpdesk Mail Reset + +After importing a production database into the LAN test instance, reset all +Helpdesk-enabled projects to use the local Mailpit test mailbox with: + +- [reset_helpdesk_mail_settings.py](/home/iadnah/redmine/reset_helpdesk_mail_settings.py:1) + +Preview the affected projects and settings: + +```sh +./reset_helpdesk_mail_settings.py --dry-run +``` + +Apply the reset: + +```sh +./reset_helpdesk_mail_settings.py +``` + +Defaults match the current Mailpit test setup: + +- incoming POP3: `192.168.1.105:1110` +- outgoing SMTP: `192.168.1.105:1025` +- incoming username/password: `test` / `testpass` +- outgoing SMTP authentication: none +- answer-from pattern: `helpdesk-{identifier}@example.test` + +If Mailpit moves, pass the host that Redmine can reach: + +```sh +./reset_helpdesk_mail_settings.py --mailpit-host 192.168.50.170 +``` + +## LAN Deployment Progress + +The LAN Redmine copy at `192.168.50.170` was inspected and updated via SSH. + +Confirmed environment facts: + +- SSH user used: `reddev` +- remote Redmine path: `/usr/share/redmine` +- remote Ruby: `2.5.1` +- remote Bundler: `1.17.3` +- remote DB config: MySQL via `config/database.yml` + +Remote plugin rollback archives were created on the LAN host under: + +```text +/home/reddev/redmine-plugin-backups/ +``` + +The deployed helpdesk search routes were verified with: + +- remote `rake routes` +- HTTP requests returning normal login redirects when unauthenticated + +## What Is Not Finished Yet + +### 1. The Real Worker/Indexer + +This is the main unfinished piece. + +Still needed: + +- run and document end-to-end validation of `redmine_outbox_worker.py` against + the LAN copy +- decide the first real external index target +- map the derived JSONL document shape into that index + +### 2. External Search Index + +We have not yet built the actual external index. + +Planned direction: + +- Qdrant first +- OpenAI embeddings first +- ticket-level docs for "which issue mentioned this?" +- message-level docs for "how did we handle a similar case?" + +### 3. Full Helpdesk Event Validation + +The local code includes helpdesk outbox hooks and read-only helpdesk API +changes, but the complete create/update test matrix for: + +- `helpdesk_ticket.*` +- `journal_message.*` + +still needs to be run and documented cleanly on the LAN copy. + +### 4. Pre-Existing UI/Plugin Bugs + +We discovered old plugin issues while working: + +- duplicate avatar DOM ids likely causing repeated/wrong thumbnails +- `attachments/contacts_thumbnail` digest warning +- `acts_as_list` Redmine 4 compatibility warning + +These are logged in: + +- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1) + +They are important context but are not the primary search deliverable. + +## Current Repo Files That Matter Most + +Project docs: + +- [docs/event_outbox_spec.md](/home/iadnah/redmine/docs/event_outbox_spec.md:1) +- [docs/redmineup_local_fork_changelog.md](/home/iadnah/redmine/docs/redmineup_local_fork_changelog.md:1) +- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1) + +Tooling: + +- [redmine_contacts.py](/home/iadnah/redmine/redmine_contacts.py:1) +- [redmine_helpdesk_search.py](/home/iadnah/redmine/redmine_helpdesk_search.py:1) + +Local plugin work: + +- [redmine-copy/plugins/redmine_event_outbox](/home/iadnah/redmine/redmine-copy/plugins/redmine_event_outbox) +- [redmine-copy/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb](/home/iadnah/redmine/redmine-copy/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb:1) + +## Current Recommended Next Steps + +If continuing this project, the next best work is: + +1. Stop treating the CLI exporter as the end product. +2. Build the external worker that consumes `event_outbox_events`. +3. Start with a simple derived output target: + - JSONL + - SQLite + - or local files +4. Then connect that worker to: + - Qdrant + - OpenAI embeddings +5. Validate end-to-end helpdesk search using real historical message data. + +## Practical Commands + +Fetch contacts: + +```sh +./redmine_contacts.py fetch +``` + +Search contacts: + +```sh +./redmine_contacts.py search "customer name or phone" +``` + +Preview a contact update: + +```sh +./redmine_contacts.py update 123 --set first_name=Corrected +``` + +Fetch helpdesk metadata through the Redmine read API: + +```sh +./redmine_contacts.py helpdesk-ticket 39858 +./redmine_contacts.py helpdesk-issues 4337 --limit 50 +./redmine_contacts.py helpdesk-messages 39858 --limit 100 +./redmine_contacts.py helpdesk-timeline 4337 --limit 100 +``` + +Fetch read-only helpdesk documents directly over SSH/MySQL: + +```sh +./redmine_helpdesk_search.py fetch +``` + +Preview pending outbox events and their derived documents without marking them +processed: + +```sh +./redmine_outbox_worker.py --dry-run --batch-size 10 +``` + +Process a bounded outbox batch into local JSONL and mark successful rows: + +```sh +./redmine_outbox_worker.py --batch-size 20 +``` + +Search the cached helpdesk documents: + +```sh +./redmine_helpdesk_search.py search "inventory not updated" --type message --limit 10 +``` + +Show issues by contact: + +```sh +./redmine_helpdesk_search.py issues-by-contact 1299 --limit 20 +``` + +## Summary + +This project began as a way to search and use legacy Redmine data without +breaking the system or forcing a full upgrade. The important progress so far is +not cosmetic UI work. It is the architectural clarification that helpdesk data +is the authoritative customer communication layer, plus the first safe event and +export tooling around it. + +The repo is now at the point where the next meaningful leap is an external +worker/indexer, not more Redmine UI surface. diff --git a/dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md b/dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md new file mode 100644 index 0000000..15226b0 --- /dev/null +++ b/dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md @@ -0,0 +1,13 @@ +# redmine_contacts 4.1.2 Local Snapshot + +- Archive: `redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz` +- Created: `2026-04-21T21:55:48Z` +- Source directory: `redmine-copy/plugins/redmine_contacts` +- Purpose: rollback snapshot before local helpdesk search/outbox work. +- SHA256: + +```text +527562df67a4a38e0f80282d5ee897c13a5eceabe183c7cdc49dc6471a327fe9 dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz +``` + +No code changes were made to `redmine_contacts` in this work item. diff --git a/dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md b/dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md new file mode 100644 index 0000000..f229861 --- /dev/null +++ b/dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.MANIFEST.md @@ -0,0 +1,14 @@ +# redmine_contacts_helpdesk 3.0.9 Local Snapshot + +- Archive: `redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz` +- Created: `2026-04-21T21:55:48Z` +- Source directory: `redmine-copy/plugins/redmine_contacts_helpdesk` +- Purpose: rollback snapshot before local helpdesk search/outbox work. +- SHA256: + +```text +6ed55e3e5918c283a684c682ef4bf7e772a0156315b1c40f084a7bcf7e672b89 dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz +``` + +This snapshot predates the local read-only `helpdesk_search/*` endpoints and +the optional `redmine_event_outbox` helpdesk callbacks. diff --git a/dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md b/dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md new file mode 100644 index 0000000..9ca3d34 --- /dev/null +++ b/dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md @@ -0,0 +1,47 @@ +# redmine_event_outbox 0.0.1 Known-Good Archive + +Archive: + +```text +dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz +``` + +SHA256: + +```text +e7f31491554cbcf87fdb49ea92ecccc24265952616e661ff6a45cc9b8c172dbc +``` + +Contents: + +```text +redmine_event_outbox/ +redmine_event_outbox/README.md +redmine_event_outbox/app/models/event_outbox_event.rb +redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb +redmine_event_outbox/init.rb +redmine_event_outbox/lib/redmine_event_outbox.rb +redmine_event_outbox/lib/redmine_event_outbox/event.rb +redmine_event_outbox/lib/redmine_event_outbox/hooks/issues_hook.rb +redmine_event_outbox/lib/redmine_event_outbox/patches/contact_patch.rb +redmine_event_outbox/lib/redmine_event_outbox/patches/journal_patch.rb +redmine_event_outbox/lib/tasks/redmine_event_outbox.rake +``` + +Tested on LAN Redmine copy: + +- Migrated successfully. +- Passenger restart via `touch tmp/restart.txt`. +- Verified `issue.created`. +- Verified `issue.updated`. +- Verified `journal.created`. +- Verified `contact.created`. +- Verified `contact.updated`. +- Verified `redmine_event_outbox:dump`. + +Test artifacts left on LAN copy for traceability: + +- Issue `#39858` in project `Meetings`. +- Contact `#4337` in project `Email Test`. +- Six outbox rows in `event_outbox_events`. + diff --git a/docs/event_outbox_spec.md b/docs/event_outbox_spec.md new file mode 100644 index 0000000..1f735ae --- /dev/null +++ b/docs/event_outbox_spec.md @@ -0,0 +1,588 @@ +# Redmine Event Outbox Spec + +## Purpose + +Add a low-risk event boundary around the legacy Redmine install so external tools +can react to Redmine changes without polling heavily and without making Redmine +dependent on a message bus, search service, or experimental automation code. + +The first version should capture issue and journal activity into a local database +outbox table. A separate worker can later publish those rows to Redis, RabbitMQ, +a webhook, or a search/indexing service. + +## Goals + +- Record issue creation and update events. +- Record issue journal/comment events. +- Record contact creation and update events from `redmine_contacts`. +- Keep Redmine issue saves working even if external messaging/search systems are + down. +- Make event processing replayable from a durable local table. +- Keep the plugin small and compatible with Redmine 3.4.4-era plugin patterns. +- Support future indexing for queries like "recent events for customer A". + +## Non-Goals For V1 + +- No direct Redis/RabbitMQ publish inside Redmine request callbacks. +- No semantic/vector search inside Redmine. +- No replacement of Redmine email handling in the first version. +- No guarantee that external consumers receive each event exactly once. +- No broad UI in Redmine beyond optional admin/status views later. +- No binary image indexing inside Redmine. + +## Safety Rule + +Redmine must not fail an issue create/update because a broker, worker, search +index, DNS lookup, network call, or external service failed. + +V1 should therefore only write to Redmine's own database from inside Redmine. All +network publishing happens outside Redmine in a worker. + +There is one important tradeoff: + +- If outbox insert failures are rescued, ticket saves are maximally protected but + rare event loss is possible. +- If outbox rows are written transactionally and failures are not rescued, event + durability is stronger but ticket saves can fail because the outbox write + failed. + +For this project, prefer protecting ticket saves. The outbox insert should be +simple and local, and failures should be logged with enough detail to diagnose. + +## Proposed Plugin + +Plugin name: + +```text +redmine_event_outbox +``` + +Directory: + +```text +redmine-copy/plugins/redmine_event_outbox/ +``` + +Initial structure: + +```text +init.rb +db/migrate/001_create_event_outbox_events.rb +app/models/event_outbox_event.rb +lib/redmine_event_outbox.rb +lib/redmine_event_outbox/hooks/issues_hook.rb +lib/redmine_event_outbox/patches/contact_patch.rb +lib/redmine_event_outbox/patches/journal_patch.rb +lib/tasks/redmine_event_outbox.rake +``` + +## Event Table + +Table name: + +```text +event_outbox_events +``` + +Columns: + +```text +id integer primary key +event_type string, required +source_type string, required +source_id integer, required +project_id integer, nullable +issue_id integer, nullable +journal_id integer, nullable +user_id integer, nullable +occurred_at datetime, required +payload text/json, required +processed_at datetime, nullable +attempts integer, default 0 +last_error text, nullable +locked_at datetime, nullable +locked_by string, nullable +created_at datetime +updated_at datetime +``` + +Production Redmine currently uses MySQL. Store `payload` as `text` for Redmine +3.4.4 compatibility and serialize JSON in application code. Do not rely on +native MySQL JSON behavior in V1. + +Useful indexes: + +```text +index_event_outbox_events_on_processed_at_id +index_event_outbox_events_on_event_type +index_event_outbox_events_on_issue_id +index_event_outbox_events_on_project_id +index_event_outbox_events_on_occurred_at +``` + +## V1 Event Types + +### issue.created + +Created after a new issue is saved. + +Trigger candidate found in Redmine 3.4.4: + +```ruby +controller_issues_new_after_save +``` + +Payload: + +```json +{ + "event_type": "issue.created", + "issue_id": 123, + "project_id": 4, + "tracker_id": 1, + "status_id": 1, + "priority_id": 2, + "author_id": 5, + "author_name": "Jane User", + "assigned_to_id": null, + "assigned_to_name": null, + "subject": "Example", + "created_on": "2026-04-21T12:00:00Z", + "updated_on": "2026-04-21T12:00:00Z" +} +``` + +### issue.updated + +Created after an existing issue update succeeds. + +Trigger candidate found in Redmine 3.4.4: + +```ruby +controller_issues_edit_after_save +``` + +Payload should include issue identity plus the current journal id when present: + +```json +{ + "event_type": "issue.updated", + "issue_id": 123, + "journal_id": 456, + "project_id": 4, + "status_id": 2, + "assigned_to_id": 7, + "assigned_to_name": "Support User", + "actor_id": 5, + "actor_name": "Jane User", + "subject": "Example", + "updated_on": "2026-04-21T12:15:00Z" +} +``` + +### journal.created + +Created when a journal row for an issue is committed. + +Trigger candidate found in Redmine 3.4.4: + +```ruby +Journal.after_commit :on => :create +``` + +The plugin can patch `Journal` with a separate `after_commit` callback. + +Payload: + +```json +{ + "event_type": "journal.created", + "journal_id": 456, + "issue_id": 123, + "project_id": 4, + "user_id": 5, + "user_name": "Jane User", + "subject": "Example", + "private_notes": false, + "has_notes": true, + "changed_fields": ["status_id", "assigned_to_id"], + "created_on": "2026-04-21T12:15:00Z" +} +``` + +V1 should not put full private note text into the payload. The indexing worker can +fetch detail with appropriate credentials if needed. + +### contact.created + +Created after a contact is committed. + +Trigger candidate: + +```ruby +Contact.after_commit :on => :create +``` + +The plugin can patch `Contact` from `redmine_contacts` when that plugin is +installed. + +Payload: + +```json +{ + "event_type": "contact.created", + "contact_id": 321, + "project_ids": [4], + "is_company": false, + "name": "Customer Name", + "company": "Customer Company", + "author_id": 5, + "author_name": "Jane User", + "assigned_to_id": 7, + "assigned_to_name": "Support User", + "created_on": "2026-04-21T12:20:00Z", + "updated_on": "2026-04-21T12:20:00Z" +} +``` + +### contact.updated + +Created after a contact update is committed. + +Trigger candidate: + +```ruby +Contact.after_commit :on => :update +``` + +Payload: + +```json +{ + "event_type": "contact.updated", + "contact_id": 321, + "project_ids": [4], + "is_company": false, + "name": "Customer Name", + "company": "Customer Company", + "actor_id": 5, + "actor_name": "Jane User", + "assigned_to_id": 7, + "assigned_to_name": "Support User", + "updated_on": "2026-04-21T12:25:00Z" +} +``` + +Contact payloads should include enough human context for downstream consumers to +decide whether to fetch the full contact. Avoid embedding all phone/email/address +data in V1 payloads unless a consumer proves it needs those fields inline. + +### helpdesk_ticket.created + +Created after a `HelpdeskTicket` row is committed when +`redmine_contacts_helpdesk` is installed. + +Payload: + +```json +{ + "event_type": "helpdesk_ticket.created", + "helpdesk_ticket_id": 10, + "issue_id": 123, + "project_id": 4, + "contact_id": 321, + "message_id": "", + "is_incoming": true, + "source": 0, + "from_address": "customer@example.com", + "to_address": "support@example.com", + "cc_address": null, + "subject": "Example", + "ticket_date": "2026-04-21T12:00:00Z" +} +``` + +### helpdesk_ticket.updated + +Created after an existing `HelpdeskTicket` row is updated. This is useful when +the contact, source, or message metadata is corrected after ticket creation. + +### journal_message.created + +Created after a `JournalMessage` row is committed. This is the authoritative +per-email metadata layer for helpdesk conversation search. + +Payload: + +```json +{ + "event_type": "journal_message.created", + "journal_message_id": 20, + "journal_id": 456, + "issue_id": 123, + "project_id": 4, + "contact_id": 321, + "message_id": "", + "is_incoming": false, + "source": 0, + "from_address": "support@example.com", + "to_address": "customer@example.com", + "cc_address": null, + "has_bcc_address": false, + "private_notes": false, + "has_notes": true, + "message_date": "2026-04-21T12:15:00Z" +} +``` + +The event records whether BCC metadata exists but does not store the BCC address +itself. + +### journal_message.updated + +Created after an existing `JournalMessage` row is updated. This lets downstream +indexes repair message-level documents when message metadata is corrected. + +## Payload Context Policy + +Payloads should be lightweight but useful. Include: + +- ids needed for follow-up fetches +- event type and timestamp +- issue/contact subject or display name +- user id and user name when known +- project id(s) +- changed field names when available + +Avoid: + +- full private notes +- large descriptions/background fields +- attachments or binary content +- full email bodies +- BCC addresses +- large custom field dumps + +This gives consumers enough information to decide whether they care about an +event while keeping the outbox table compact and lower-risk. + +## Delivery Semantics + +V1 should provide at-least-once processing from the outbox to downstream systems. + +Consumers must be idempotent. Use the outbox row id as the event id: + +```json +{ + "event_id": 98765, + "event_type": "issue.updated" +} +``` + +Duplicates are acceptable. Silent message loss is not acceptable once an outbox +row exists. + +## Worker/Rake Task + +Current implemented worker-facing command: + +```sh +bundle exec rake redmine_event_outbox:dump RAILS_ENV=production +``` + +This prints pending rows as JSON and does not mark them processed. + +Next worker command: + +```sh +bundle exec rake redmine_event_outbox:publish RAILS_ENV=production +``` + +V1 modes: + +```sh +# Print pending rows as JSON without marking processed. +bundle exec rake redmine_event_outbox:dump + +# Process pending rows and mark success. +bundle exec rake redmine_event_outbox:publish + +# Retry failed/unprocessed rows. +bundle exec rake redmine_event_outbox:publish RETRY=1 +``` + +Initial publisher target can be stdout or a local JSONL file. That lets us test +the Redmine side before choosing Redis Streams or RabbitMQ. + +The next implementation should keep the worker small and conservative: + +- select pending rows in id order +- lock or claim a bounded batch +- publish each row +- mark `processed_at` only after publish succeeds +- increment `attempts` and write `last_error` on failure +- leave failed rows available for retry +- make duplicate delivery acceptable to consumers + +Later publisher targets: + +- Redis Streams +- RabbitMQ +- webhook HTTP POST +- local search/indexing service + +## Search/Indexing Direction + +The semantic/fuzzy search system should be external to Redmine. + +The first external search index should have strong vector-search support because +future work will use embeddings heavily, including image embeddings. Redmine +should only emit events and identifiers; the external indexer should fetch, +transform, chunk, embed, and store searchable material. + +Likely derived entities: + +- contacts +- issues +- journals +- helpdesk email messages +- status/assignee changes +- timestamps and project links +- future image/document embeddings + +Likely query examples: + +```text +recent events for customer A +open issues involving customer A +recent emails from customer A +status changes for customer A this week +``` + +The outbox does not need to contain all searchable text. It only needs enough +identity and timestamps for a worker to fetch and update the external index. + +Candidate vector-capable search/index options to evaluate later: + +- PostgreSQL with `pgvector` +- Qdrant +- Weaviate +- OpenSearch/Elasticsearch with vector fields +- LanceDB + +Since production Redmine uses MySQL, the search/index database should be treated +as a separate derived system rather than as a feature of the Redmine database. + +## Email Handling Direction + +Email handling should be handled after the basic issue/journal outbox is stable. + +Observed areas to inspect further: + +- Redmine core `MailHandler` +- `redmine_contacts` rake tasks under `lib/tasks/contacts_email.rake` +- `redmine_contacts_helpdesk` +- `helpdesk_mailer` endpoint and related mail handling code + +Future event types may include: + +```text +email.received +email.ignored +email.created_issue +email.updated_issue +helpdesk_ticket.created +helpdesk_ticket.updated +``` + +Custom mail routing should have a safe fallback to current behavior if external +decision code fails. + +## Implementation Phases + +### Phase 1: Outbox Skeleton + +- Status: implemented and tested on the LAN Redmine copy. +- Created plugin. +- Added migration and model. +- Added helper method for safe event creation. +- Added issue create/update hooks. +- Added basic rake task to dump pending rows. +- Verified issue create/update writes outbox rows in the `Meetings` project. + +### Phase 2: Journal Events + +- Status: implemented and tested on the LAN Redmine copy. +- Patched `Journal` with `after_commit`. +- Records `journal.created`. +- Includes changed field names from journal details. +- Avoids full private note content in payload. + +### Phase 3: Contact Events + +- Status: implemented and tested on the LAN Redmine copy. +- Patched `Contact` from `redmine_contacts` if available. +- Records `contact.created`. +- Records `contact.updated`. +- Includes contact display name, company, project ids, and relevant user context. +- Keeps payloads small; full contact detail can be fetched by external workers. + +### Phase 4: Worker + +- Add locking fields. +- Process pending rows in batches. +- Mark `processed_at` only after successful publish. +- Track attempts and last error. +- Support dry-run/dump mode. + +### Phase 5: External Index Prototype + +- Build a small external worker outside Redmine. +- Read outbox rows. +- Fetch affected issue/contact/journal details through API or DB. +- Store in a vector-capable search database. +- Add a CLI for recent customer timelines. + +### Phase 6: Message Bus + +- Choose Redis Streams or RabbitMQ based on actual consumer needs. +- Keep Redmine unchanged; only the worker publisher changes. + +## Open Questions + +- How long should processed outbox rows be retained? +- Do we need a Redmine admin page to inspect outbox health, or is a rake task/log enough? +- What is the current production mail ingestion path? +- Which vector-capable external search index should be used first? +- Should contact payloads include normalized primary email/phone, or is that too much inline data? + +## Current Checkpoint + +Known-good archive: + +```text +dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz +``` + +Manifest: + +```text +dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.MANIFEST.md +``` + +Tested on LAN Redmine copy: + +- Migrated successfully. +- Passenger restart via `touch tmp/restart.txt`. +- Verified `issue.created`. +- Verified `issue.updated`. +- Verified `journal.created`. +- Verified `contact.created`. +- Verified `contact.updated`. +- Verified `redmine_event_outbox:dump`. + +Test artifacts left on LAN copy for traceability: + +- Issue `#39858` in project `Meetings`. +- Contact `#4337` in project `Email Test`. +- Six outbox rows in `event_outbox_events`. diff --git a/docs/pre_existing_issues.md b/docs/pre_existing_issues.md new file mode 100644 index 0000000..9979ac0 --- /dev/null +++ b/docs/pre_existing_issues.md @@ -0,0 +1,94 @@ +# Pre-Existing Issues Log + +This log tracks bugs, warnings, and confusing behaviors noticed while working on +the Redmine 3.4.4 local fork. It is not a task tracker; it is a place to keep +context so future plugin edits do not have to rediscover old problems. + +## 2026-04-21 - Duplicate Contact/Helpdesk Avatar IDs + +- Area: `redmine_contacts`, `redmine_contacts_helpdesk` +- Status: observed/analyzing +- Symptom: pages that display multiple contact/user avatars may show or behave + as if every row has the same thumbnail/avatar. +- Relevant code: + - `redmine_contacts/lib/redmine_contacts/helpers/contacts_helper.rb` + - helpdesk/contact views that call `link_to ..., :id => "avatar"` repeatedly +- Current assessment: + - Several views/helpers render repeated `id="avatar"` attributes on lists of + contacts, issues, notes, deals, and helpdesk journals. + - Repeated DOM ids are invalid HTML and can cause JavaScript, tooltip, popup, + or CSS selectors using `#avatar` to bind to the wrong element or reuse the + first matching element. + - This is a better fit for the long-standing "same thumbnail for every user" + symptom than the Rails cache digest warning below. +- Next diagnostic/fix idea: + - Replace repeated `:id => "avatar"` with `:class => "avatar"` where no unique + id is required. + - Where an id is required, generate stable unique ids such as + `contact-avatar-#{contact.id}` or `journal-avatar-#{journal.id}`. + - Test on a page that currently displays multiple contacts/users with distinct + avatars. + +## 2026-04-21 - `attachments/contacts_thumbnail` Cache Digest Warning + +- Area: `redmine_contacts` +- Status: observed/analyzing +- Log message: + +```text +Couldn't find template for digesting: attachments/contacts_thumbnail +``` + +- Relevant code: + - `redmine_contacts/lib/redmine_contacts/patches/attachments_controller_patch.rb` + - route: `attachments/contacts_thumbnail/:id(/:size)` +- Current assessment: + - `AttachmentsController#contacts_thumbnail` streams a generated thumbnail via + `send_file` or returns 404; it normally does not render a view template. + - Rails 4 cache digesting still probes for a conventional action template and + logs the missing-template warning. + - This is likely log noise and probably not the cause of the duplicate-avatar + symptom. +- Possible fix: + - Add a blank placeholder template at + `redmine_contacts/app/views/attachments/contacts_thumbnail.html.erb` with a + local fork comment explaining that the action streams files. + - Do this only after confirming it does not mask the duplicate-avatar bug. + +## 2026-04-21 - Helpdesk Search Manual URL Confusion + +- Area: local `redmine_contacts_helpdesk` search API change +- Status: mitigated in current working copy and LAN deployment +- Symptom: + - Visiting `/helpdesk_search/issues` or `/helpdesk_search/issues/1` produced + `ActionController::RoutingError` stack traces in `production.log`. +- Current assessment: + - The originally implemented API route was + `/helpdesk_search/issues/:issue_id/ticket`. + - Manual browser tests naturally tried the shorter paths. +- Mitigation: + - Added usage routes for `/helpdesk_search`, `/helpdesk_search/issues`, and + `/helpdesk_search/contacts`. + - Added `/helpdesk_search/issues/:issue_id` as an alias for the ticket lookup. + +## 2026-04-21 - `acts_as_list` Redmine 4 Deprecation Warning + +- Area: Redmine core/plugin compatibility +- Status: observed; low urgency while Redmine 3.4.4 remains the baseline +- Log message: + +```text +DEPRECATION WARNING: The acts_as_list plugin will be removed from Redmine 4 core, use the acts_as_list gem or similar implementation instead. (called from acts_as_list at /usr/share/redmine/lib/plugins/acts_as_list/lib/active_record/acts/list.rb:34) +``` + +- Current assessment: + - This is an upgrade-compatibility warning from the Redmine/Rails stack, not a + current runtime failure. + - It means some installed plugin or model uses `acts_as_list` from Redmine + core. Redmine 4 removes that bundled implementation, so any future Redmine 4+ + migration would need an explicit `acts_as_list` gem or a local replacement. + - Since the near-term baseline is Redmine 3.4.4 and upgrading is not currently + a goal, this should not block helpdesk search work. +- Next diagnostic/fix idea: + - If upgrade work resumes, search installed plugins and app models for + `acts_as_list`, then decide whether to add the gem or patch each caller. diff --git a/docs/redmineup_local_fork_changelog.md b/docs/redmineup_local_fork_changelog.md new file mode 100644 index 0000000..4bd017f --- /dev/null +++ b/docs/redmineup_local_fork_changelog.md @@ -0,0 +1,85 @@ +# RedmineUP Local Fork Changelog + +The installed RedmineUP `redmine_contacts` and `redmine_contacts_helpdesk` +plugins are treated as locally maintained legacy code for this Redmine 3.4.4 +environment. Before risky edits, archive the current plugin directories in +`dist/` and record the purpose, touched behavior, and LAN test result here. + +## Current Checkpoint + +- Baseline: + - Redmine `3.4.4` + - `redmine_contacts` `4.1.2 PRO` + - `redmine_contacts_helpdesk` `3.0.9 PRO` +- Strategic direction: + - Treat helpdesk/customer data as first-class. + - Prefer local-fork plugin edits when they unlock safer search/indexing. + - Keep Redmine request paths independent from external worker/index failures. +- Implemented locally: + - `redmine_event_outbox` plugin with issue/journal/contact events. + - Optional helpdesk outbox hooks for `HelpdeskTicket` and `JournalMessage`. + - Read-only `helpdesk_search/*` JSON endpoints in the local helpdesk fork. + - Standalone contact CLI and read-only helpdesk export/search CLI. +- LAN deployment status: + - Helpdesk search routes were deployed and route-loaded successfully on the + LAN Redmine copy. + - Short alias/usage routes were added to avoid noisy routing errors during + manual browser testing. + - Full end-to-end helpdesk outbox validation is still pending. +- Next meaningful milestone: + - Build the external worker/indexer that consumes `event_outbox_events`, + enriches via read-only MySQL joins, and emits deterministic ticket/message + documents for external indexing. + +## 2026-04-24 - POP3 Get Mail Compatibility Fix + +- Touched plugin: + - `redmine_contacts` + - `redmine_contacts_helpdesk` +- Purpose: + - Fix Helpdesk POP3 retrieval on the LAN test host when Ruby 2.5 raises + `FrozenError: can't modify frozen String` inside `Net::POP3`. + - Allow Helpdesk outbound mail to use Mailpit's unauthenticated SMTP listener. +- Behavior changed: + - Changed POP3 message retrieval from `msg.pop` to `msg.pop(String.new)` so + Ruby's POP3 code appends chunks into an explicit mutable destination string. + - This does not change message handling semantics; it only avoids relying on + Ruby's default empty string argument being mutable. + - Changed Helpdesk SMTP delivery option construction to omit + `authentication`, `user_name`, and `password` when the project SMTP + authentication setting is blank. +- LAN test result: + - Deployed to `/usr/share/redmine/plugins/redmine_contacts`. + - `HelpdeskMailer.check_project(Project.find("fud-helpdesk").id)` completed + successfully and processed 1 message. + - Deployed to `/usr/share/redmine/plugins/redmine_contacts_helpdesk`. + - Mailpit rejected `AUTH PLAIN` with `502 5.5.1 Command not implemented`. + After blanking SMTP auth settings and omitting auth options, a Helpdesk test + mail for issue `#39863` was delivered to Mailpit. + +## 2026-04-21 - Helpdesk Search Foundation + +- Archives created before plugin edits: + - `dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz` + - `dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz` +- Touched plugins: + - `redmine_contacts_helpdesk` + - `redmine_event_outbox` +- Purpose: + - Make helpdesk ticket and message identity available to external search and + indexing workers. + - Avoid relying on Redmine issue author when helpdesk-created tickets use + `Anonymous`. +- Behavior changed: + - Added read-only `helpdesk_search/*` JSON endpoints guarded by the existing + `view_helpdesk_tickets` permission. + - Added optional outbox hooks for `HelpdeskTicket` and `JournalMessage`. +- Payload/content policy: + - Include ids, source, direction, message id, and non-body address metadata. + - Do not copy email bodies, private note text, attachments, or BCC addresses + into event rows or the read API. +- LAN test result: + - Pending. Validate on the LAN Redmine copy by creating/updating a controlled + helpdesk ticket and journal message, checking `event_outbox_events`, and + calling the new `helpdesk_search/*` endpoints with a user/API key that has + `view_helpdesk_tickets`. diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..8e51e17 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,14 @@ +# Local Redmine Plugin Forks + +This directory is the tracked source of truth for local Redmine plugin work. + +The ignored `redmine-copy/` tree is a working/reference copy of the full legacy +Redmine install. Make plugin edits here first, then deploy or copy them into the +test Redmine instance or `redmine-copy/` as needed. + +Tracked plugin folders: + +- `redmine_event_outbox` - local event outbox plugin. +- `redmine_contacts` - RedmineUP contacts plugin with local compatibility fixes. +- `redmine_contacts_helpdesk` - RedmineUP helpdesk plugin with local API and + mail compatibility fixes. diff --git a/plugins/redmine_contacts/Gemfile b/plugins/redmine_contacts/Gemfile new file mode 100644 index 0000000..0d67398 --- /dev/null +++ b/plugins/redmine_contacts/Gemfile @@ -0,0 +1,3 @@ +gem "redmine_crm" +gem "vcard", "~> 0.2.8" +gem "spreadsheet", "~> 0.6.8" diff --git a/plugins/redmine_contacts/README.rdoc b/plugins/redmine_contacts/README.rdoc new file mode 100644 index 0000000..aff8ba1 --- /dev/null +++ b/plugins/redmine_contacts/README.rdoc @@ -0,0 +1,46 @@ += Contacts plugin + +== Install + +* Copy redmine_contacts plugin to {RAILS_APP}/plugins on your redmine path +* Run bundle install --without development test RAILS_ENV=production +* Run rake redmine:plugins NAME=redmine_contacts RAILS_ENV=production + +== Uninstall + +
+rake redmine:plugins NAME=redmine_contacts VERSION=0 RAILS_ENV=production
+rm -r plugins/redmine_contacts
+
+ +=== Tables created by CRM plugin + +* contacts +* contacts_deals +* contacts_issues +* contacts_projects +* deals +* deal_categories +* deal_processes +* deal_statuses +* deal_statuses_projects +* notes +* tags +* taggings +* recently_vieweds +* contacts_settings +* contacts_queries +* addresses +* deals_issues + +== Requirements + +* Redmine 2.3+ + +== Test +bundle exec rake db:drop db:create db:migrate redmine:plugins RAILS_ENV=test_sqlite3 +bundle exec rake test TEST="plugins/redmine_contacts/test/**/*_test.rb" RAILS_ENV=test_sqlite3 + +=== Test API + +curl -v -H "Content-Type: application/xml" -X POST --data "@contact.xml" -u admin:admin http://localhost:3000/contacts.xml \ No newline at end of file diff --git a/plugins/redmine_contacts/app/controllers/contact_imports_controller.rb b/plugins/redmine_contacts/app/controllers/contact_imports_controller.rb new file mode 100644 index 0000000..94e7fa6 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contact_imports_controller.rb @@ -0,0 +1,36 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactImportsController < ImporterBaseController + menu_item :contacts + helper :contacts + + def klass + ContactImport + end + + def importer_klass + ContactKernelImport + end + + def instance_index + project_contacts_path(:project_id => @project.id) + end + +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_controller.rb new file mode 100755 index 0000000..05698f5 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_controller.rb @@ -0,0 +1,490 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsController < ApplicationController + unloadable + + Mime::Type.register 'text/x-vcard', :vcf + Mime::Type.register 'application/vnd.ms-excel', :xls + + default_search_scope :contacts + + before_action :find_contact, :only => [:show, :edit, :update, :destroy, :load_tab] + before_action :find_project, :only => [:new, :create] + before_action :authorize, :only => [:create, :new] + before_action :authorize_contacts, :only => [:edit, :update, :destroy] + before_action :find_optional_project, :only => [:index, :contacts_notes, :edit_mails, :send_mails, :bulk_update] + + accept_rss_auth :index, :show + accept_api_auth :index, :show, :create, :update, :destroy + + helper :attachments + helper :contacts + include ContactsHelper + helper :watchers + helper :deals + helper :notes + helper :custom_fields + include CustomFieldsHelper + helper :context_menus + include WatchersHelper + helper :sort + include SortHelper + helper :queries + include QueriesHelper + helper :crm_queries + include CrmQueriesHelper + include ApplicationHelper + include NotesHelper + + def index + retrieve_crm_query('contact') + sort_init(@query.sort_criteria.empty? ? [['last_name', 'asc'], ['first_name', 'asc']] : @query.sort_criteria) + sort_update(@query.sortable_columns) + @query.sort_criteria = sort_criteria.to_a + if @query.valid? + case params[:format] + when 'csv', 'xls', 'vcf' + @limit = Setting.issues_export_limit.to_i + if Redmine::VERSION::STRING < '3.2' && params[:columns] == 'all' + @query.column_names = @query.available_columns.map(&:name) + end + when 'atom' + @limit = Setting.feeds_limit.to_i + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = per_page_option + end + @contacts_count = @query.object_count + @contacts_pages = Paginator.new(@contacts_count, @limit, params['page']) + @offset ||= @contacts_pages.offset + @contact_count_by_group = @query.object_count_by_group + @contacts = @query.results_scope( + :include => [:avatar], + :search => params[:search], + :order => sort_clause, + :limit => @limit, + :offset => @offset + ) + @filter_tags = @query.filters['tags'] && @query.filters['tags'][:values] + respond_to do |format| + format.html { + unless request.xhr? + last_notes + @tags = Contact.available_tags(:project => @project) + else + render :partial => contacts_list_style, :layout => false + end + } + format.api + format.atom { render_feed(@contacts, :title => "#{@project || Setting.app_title}: #{l(:label_contact_plural)}") } + format.csv { + send_data(query_to_csv(@contacts, @query, params[:csv] || {}), + :type => 'text/csv; header=present', + :filename => 'contacts.csv') + } + format.xls { + send_data(contacts_to_xls(@contacts), + :filename => 'contacts.xls', + :type => 'application/vnd.ms-excel', + :disposition => 'attachment') + } + format.vcf { + send_data(contacts_to_vcard(@contacts), + :filename => 'contacts.vcf', + :type => 'text/x-vcard', + :disposition => 'attachment') + } + end + else + respond_to do |format| + format.html { + last_notes + @tags = Contact.available_tags(:project => @project) + render(:template => 'contacts/index', :layout => !request.xhr?) + } + format.any(:atom, :csv, :pdf) { render(:nothing => true) } + format.api { render_validation_errors(@query) } + end + end + end + + def show + find_contact_issues + respond_to do |format| + format.js if request.xhr? + format.html { @contact.viewed } + format.api + format.atom { render_feed(@notes, :title => "#{@contact.name || Setting.app_title}: #{l(:label_crm_note_plural)}") } + format.vcf { send_data(contact_to_vcard(@contact), :filename => "#{@contact.name}.vcf", :type => 'text/x-vcard;', :disposition => 'attachment') } + end + end + + def edit + end + + def update + @contact.safe_attributes = params[:contact] + @contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads])) + if @contact.save + flash[:notice] = l(:notice_successful_update) + remove_old_avatars + respond_to do |format| + format.html { redirect_to :action => 'show', :project_id => params[:project_id], :id => @contact } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render 'edit', :project_id => params[:project_id], :id => @contact } + format.api { render_validation_errors(@contact) } + end + end + end + + def destroy + if @contact.destroy + flash[:notice] = l(:notice_successful_delete) + else + flash[:error] = l(:notice_unsuccessful_save) + end + respond_to do |format| + format.html { redirect_back_or_default :action => 'index', :project_id => params[:project_id] } + format.api { render_api_ok } + end + end + + def new + @duplicates = [] + @contact = Contact.new + @contact.safe_attributes = params[:contact] if params[:contact] && params[:contact].is_a?(Hash) + end + + def create + @contact = Contact.new(:project => @project, :author => User.current) + @contact.safe_attributes = params[:contact] + @contact.save_attachments(params[:attachments] || (params[:contact] && params[:contact][:uploads])) + if @contact.save + flash[:notice] = l(:notice_successful_create) + remove_old_avatars + respond_to do |format| + format.html { redirect_to (params[:continue] ? { :action => 'new', :project_id => @project } : { :action => 'show', :project_id => @project, :id => @contact }) } + format.js + format.api { redirect_on_create(params) } + end + else + respond_to do |format| + format.api { render_validation_errors(@contact) } + format.js { render :action => 'new' } + format.html { render :action => 'new' } + end + end + end + + def contacts_notes + unless request.xhr? + @tags = Contact.available_tags(:project => @project) + end + + contacts = find_contacts(false) + deals = find_deals + + joins = " " + joins << " LEFT OUTER JOIN #{Contact.table_name} ON #{Note.table_name}.source_id = #{Contact.table_name}.id AND #{Note.table_name}.source_type = 'Contact' " + joins << " LEFT OUTER JOIN #{Deal.table_name} ON #{Note.table_name}.source_id = #{Deal.table_name}.id AND #{Note.table_name}.source_type = 'Deal' " + cond = "(1 = 1) " + cond << "and (#{Contact.table_name}.id in (#{contacts.any? ? contacts.map(&:id).join(', ') : 'NULL'})" + + cond << " or #{Deal.table_name}.id in (#{deals.any? ? deals.map(&:id).join(', ') : 'NULL'}))" + cond << " and (LOWER(#{Note.table_name}.content) LIKE '%#{params[:search_note].downcase}%')" if params[:search_note] and request.xhr? + cond << " and (#{Note.table_name}.author_id = #{params[:note_author_id]})" if !params[:note_author_id].blank? + cond << " and (#{Note.table_name}.type_id = #{params[:type_id]})" if !params[:type_id].blank? + + scope = Note.joins(joins).where(cond).order("#{Note.table_name}.created_on DESC") + @notes_pages = Paginator.new(scope.count, 20, params['page']) + @notes = scope.limit(20).offset(@notes_pages.offset) + + respond_to do |format| + format.html { render :partial => "notes/notes_list", :layout => false, :locals => { :notes => @notes, :notes_pages => @notes_pages } if request.xhr? } + format.xml { render :xml => @notes } + format.csv { send_data(notes_to_csv(@notes), :type => 'text/csv; header=present', :filename => 'notes.csv') } + format.atom { render_feed(@notes, :title => "#{l(:label_crm_note_plural)}") } + end + end + + def context_menu + @project = Project.find(params[:project_id]) unless params[:project_id].blank? + @contacts = Contact.visible.where(:id => params[:selected_contacts]) + @contact = @contacts.first if (@contacts.size == 1) + @can = { :edit => (@contact && @contact.editable?) || (@contacts && @contacts.collect { |c| c.editable? }.inject { |memo, d| memo && d }), + :create_deal => (@project && User.current.allowed_to?(:add_deals, @project)), + :create => (@project && User.current.allowed_to?(:add_contacts, @project)), + :delete => @contacts.collect { |c| c.deletable? }.inject { |memo, d| memo && d }, + :send_mails => @contacts.collect { |c| c.send_mail_allowed? && !c.primary_email.blank? }.inject { |memo, d| memo && d } + } + + render :layout => false + end + + def bulk_destroy + @contacts = Contact.deletable.where(:id => params[:ids]) + raise ActiveRecord::RecordNotFound if @contacts.empty? + @contacts.each(&:destroy) + redirect_back_or_default({ :action => 'index', :project_id => params[:project_id] }) + end + def bulk_edit + @contacts = Contact.editable.where(:id => params[:ids]) + @projects = @contacts.collect { |p| p.projects.to_a.compact }.compact.flatten.uniq + raise ActiveRecord::RecordNotFound if @contacts.empty? + @custom_fields = ContactCustomField.order(:name) + @tag_list = RedmineCrm::TagList.from(@contacts.map(&:tag_list).inject { |memo, t| memo | t }) + @project = @projects.first + @assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a } + @add_projects = Project.allowed_to(:edit_contacts).order(:lft) + end + + def bulk_update + @contacts = Contact.editable.where(:id => params[:ids]) + raise ActiveRecord::RecordNotFound if @contacts.empty? + unsaved_contact_ids = [] + @contacts.each do |contact| + contact.reload + params[:contact][:tag_list] = (contact.tag_list + RedmineCrm::TagList.from(params[:add_tag_list]) - RedmineCrm::TagList.from(params[:delete_tag_list])).uniq + + add_project_ids = (!params[:add_projects_list].to_s.blank? && params[:add_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:add_projects_list].collect{|p| p.to_i}).map(&:id) : [] + delete_project_ids = (!params[:delete_projects_list].to_s.blank? && params[:delete_projects_list].is_a?(Array)) ? Project.allowed_to(:edit_contacts).where(:id => params[:delete_projects_list].collect{|p| p.to_i}).map(&:id) : [] + project_ids = contact.project_ids + add_project_ids - delete_project_ids + params[:contact][:project_ids] = project_ids.uniq if project_ids.any? + + contact.tags.clear + contact.safe_attributes = parse_params_for_bulk_contact_attributes(params) + unless contact.save + # Keep unsaved issue ids to display them in flash error + unsaved_contact_ids << contact.id + end + if !params[:note][:content].blank? + note = ContactNote.new + note.safe_attributes = params[:note] + note.author = User.current + contact.notes << note + end + end + set_flash_from_bulk_contact_save(@contacts, unsaved_contact_ids) + redirect_back_or_default({ :controller => 'contacts', :action => 'index', :project_id => @project }) + end + + def edit_mails + @contacts = Contact.visible.where(:id => params[:ids]).reject { |c| c.email.blank? } + raise ActiveRecord::RecordNotFound if @contacts.empty? + if !@contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d } + deny_access + return + end + end + + def send_mails + contacts = Contact.visible.where(:id => params[:ids]) + raise ActiveRecord::RecordNotFound if contacts.empty? + if !contacts.collect { |c| c.send_mail_allowed? }.inject { |memo, d| memo && d } + deny_access + return + end + raise_delivery_errors = ActionMailer::Base.raise_delivery_errors + # Force ActionMailer to raise delivery errors so we can catch it + ActionMailer::Base.raise_delivery_errors = true + delivered_contacts = [] + error_contacts = [] + contacts.each do |contact| + begin + params[:message] = mail_macro(contact, params[:"message-content"]) + ContactsMailer.bulk_mail(contact, params).deliver + delivered_contacts << contact + + note = ContactNote.new + note.subject = params[:subject] + note.content = params[:message] + note.author = User.current + note.type_id = Note.note_types[:email] + contact.notes << note + Attachment.attach_files(note, params[:attachments]) + render_attachment_warning_if_needed(note) + + rescue Exception => e + error_contacts << [contact, e.message] + end + flash[:notice] = l(:notice_email_sent, delivered_contacts.map { |c| "#{c.name} #{c.emails.first}" }.join(', ')).chomp[0, 500] if delivered_contacts.any? + flash[:error] = l(:notice_email_error, error_contacts.map { |e| "#{e[0].name}: #{e[1]}"}.join(', ')).chomp[0, 500] if error_contacts.any? + end + + ActionMailer::Base.raise_delivery_errors = raise_delivery_errors + redirect_back_or_default({:controller => 'contacts', :action => 'index', :project_id => @project}) + end + + def preview_email + @text = mail_macro(Contact.visible.where(:id => params[:ids][0]).first, params[:"message-content"]) + render :partial => 'common/preview' + end + + def load_tab + end + + private + + def find_contact_issues + scope = @contact.issues + scope = scope.open unless RedmineContacts.settings[:show_closed_issues] + @contact_issues_count = scope.visible.count + @contact_issues = scope.visible.order("#{Issue.table_name}.status_id, #{Issue.table_name}.updated_on DESC").limit(10) + end + + def remove_old_avatars + params_hash = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params + avatar_params = params_hash[:attachments].find { |_k, v| v['description'] == 'avatar' }.try(:last) if params_hash[:attachments].present? + return unless avatar_params + avatar_id = avatar_params['token'].split('.').first.to_i + @contact.attachments.where(:description => 'avatar').where('id != ?', avatar_id).destroy_all if @contact.avatar + end + + def last_notes(count = 5) + scope = ContactNote.where({}) + scope = scope.where("#{Project.table_name}.id = ?", @project.id) if @project + scope = scope.includes(:attachments) + + @last_notes = scope.visible. + limit(count). + order("#{ContactNote.table_name}.created_on DESC").uniq + end + + def find_contact + @contact = Contact.find(params[:id]) + unless @contact.visible? + deny_access + return + end + project_id = (params[:contact] && params[:contact][:project_id]) || params[:project_id] + @project = Project.find_by_identifier(project_id) + @project ||= @contact.project + rescue ActiveRecord::RecordNotFound + render_404 + end + def find_deals + scope = Deal.where({}) + scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project + scope = scope.where("#{Deal.table_name}.name LIKE ? ", '%' + params[:search] + '%') if params[:search] + scope = scope.where('1=0') if params[:tag] + @deals = scope.visible + end + + def parse_params_for_bulk_contact_attributes(params) + attributes = (params[:contact] || {}).reject { |_k, v| v.blank? } + attributes.each { |k, v| attributes[k] = v.reject { |_key, val| val.blank? } if v.is_a?(Hash) } + attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' } + if custom = attributes[:custom_field_values] + custom.reject! { |_k, v| v.blank? } + custom.keys.each do |k| + if custom[k].is_a?(Array) + custom[k] << '' if custom[k].delete('__none__') + else + custom[k] = '' if custom[k] == '__none__' + end + end + attributes[:custom_field_values] = custom + end + attributes + end + + def find_contacts(pages = true) + @tag = RedmineCrm::TagList.from(params[:tag]) unless params[:tag].blank? + + scope = Contact.where({}) + scope = scope.where("#{Contact.table_name}.job_title = ?", params[:job_title]) unless params[:job_title].blank? + scope = scope.where("#{Contact.table_name}.assigned_to_id = ?", params[:assigned_to_id]) unless params[:assigned_to_id].blank? + scope = scope.where("#{Contact.table_name}.is_company = ?", params[:query]) unless (params[:query].blank? || params[:query] == '2' || params[:query] == '3') + scope = scope.where("#{Contact.table_name}.author_id = ?", User.current) if params[:query] == '3' + + case params[:query] + when '2' then scope = scope.order_by_creation + when '3' then scope = scope.order_by_creation + else scope = scope.order_by_name + end + + scope = scope.by_project(@project) + + params[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } if !params[:search].blank? + scope = scope.visible + + scope = scope.tagged_with(params[:tag]) if !params[:tag].blank? + scope = scope.tagged_with(params[:notag], :exclude => true) if !params[:notag].blank? + + @contacts_count = scope.count + @contacts = scope + + if pages + page_size = params[:page_size].blank? ? 20 : params[:page_size].to_i + @contacts_pages = Paginator.new(self, @contacts_count, page_size, params[:page]) + @offset = @contacts_pages.offset + @limit = @contacts_pages.items_per_page + + @contacts = @contacts.eager_load([:tags, :avatar]).limit(@limit).offset(@offset) + + fake_name = @contacts.first.name if @contacts.length > 0 + end + @contacts + end + + # Filter for bulk issue operations + def bulk_find_contacts + @contacts = Deal.find_all_by_id(params[:id] || params[:ids], :include => :project) + raise ActiveRecord::RecordNotFound if @contact.empty? + if @contacts.detect { |contact| !contact.visible? } + deny_access + return + end + @projects = @contacts.collect(&:projects).compact.uniq + @project = @projects.first if @projects.size == 1 + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_project(project_id = nil) + project_id ||= (params[:contact] && params[:contact][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def authorize_contacts(action = params[:action], _global = false) + case action.to_s + when 'edit', 'update' + @contact.editable? ? true : deny_access + when 'destroy' + @contact.deletable? ? true : deny_access + else + deny_access + end + end + + def redirect_on_create(options) + if options[:redirect_on_success].to_s.match('^(http|https):\/\/') + redirect_to options[:redirect_on_success].to_s + else + render :action => 'show', :status => :created, :location => contact_url(@contact) + end + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_duplicates_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_duplicates_controller.rb new file mode 100644 index 0000000..29b3aeb --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_duplicates_controller.rb @@ -0,0 +1,102 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsDuplicatesController < ApplicationController + unloadable + helper :contacts + + before_action :find_project_by_project_id, :authorize, :except => :search + before_action :find_contact, :except => :duplicates + before_action :find_duplicate, :only => :merge + + helper :contacts + + def index + @contacts = @contact.duplicates + end + + def duplicates + search_first_name = params[:contact][:first_name] if params[:contact] && !params[:contact][:first_name].blank? + search_last_name = params[:contact][:last_name] if params[:contact] && !params[:contact][:last_name].blank? + search_middle_name = params[:contact][:middle_name] if params[:contact] && !params[:contact][:middle_name].blank? + + @contact = (Contact.find(params[:contact_id]) if !params[:contact_id].blank?) || Contact.new + @contact.first_name = search_first_name || '' + @contact.last_name = search_last_name || '' + @contact.middle_name = search_middle_name || '' + respond_to do |format| + format.html { render :partial => 'duplicates', :layout => false if request.xhr? } + end + end + + def merge + @duplicate.notes << @contact.notes + @duplicate.deals << @contact.deals + @duplicate.related_deals << @contact.related_deals + @duplicate.issues << @contact.issues + @duplicate.projects << @contact.projects + @duplicate.email = (@duplicate.emails | @contact.emails).join(', ') + @duplicate.phone = (@duplicate.phones | @contact.phones).join(', ') + + call_hook(:controller_contacts_duplicates_merge, { :params => params, :duplicate => @duplicate, :contact => @contact }) + @duplicate.tag_list = @duplicate.tag_list | @contact.tag_list + begin + Contact.transaction do + @duplicate.save! + @duplicate.reload + @contact.reload + @contact.destroy + flash[:notice] = l(:notice_successful_merged) + redirect_to :controller => 'contacts', :action => 'show', :project_id => @project, :id => @duplicate + end + rescue + redirect_to :action => 'duplicates', :contact_id => @contact, :project_id => @project + end + end + + def search + @contacts = [] + q = (params[:q] || params[:term]).to_s.strip + if q.present? + scope = Contact.where({}) + scope = scope.limit(params[:limit] || 10) + scope = scope.companies if params[:is_company] + scope = scope.where(["#{Contact.table_name}.id <> ?", params[:contact_id].to_i]) if params[:contact_id] + @contacts = scope.visible.by_project(@project).live_search(q).to_a.sort!{|x, y| x.name <=> y.name } + else + @contacts = @contact.duplicates + end + render :layout => false, :partial => 'list' + end + + private + + def find_duplicate + @duplicate = Contact.find(params[:duplicate_id]) + render_403 unless @duplicate.editable? + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_contact + @contact = Contact.find(params[:contact_id]) + rescue ActiveRecord::RecordNotFound + render_404 if !request.xhr? + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_issues_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_issues_controller.rb new file mode 100644 index 0000000..ecad632 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_issues_controller.rb @@ -0,0 +1,109 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsIssuesController < ApplicationController + unloadable + + before_action :find_contact, :only => [:create_issue, :delete] + before_action :find_issue, :except => [:create_issue] + before_action :find_project_by_project_id, :only => [:create_issue] + before_action :authorize_global, :only => [:close] + before_action :authorize + + helper :contacts + + def create_issue + deny_access unless User.current.allowed_to?(:manage_contact_issue_relations, @project) || User.current.allowed_to?(:add_issues, @project) + issue = Issue.new + issue.project = @project + issue.author = User.current + issue.status = IssueStatus.default if ActiveRecord::VERSION::MAJOR < 4 + issue.start_date ||= Date.today + issue.contacts << @contact + issue.safe_attributes = params[:issue] if params[:issue] + + if issue.save + flash[:notice] = l(:notice_successful_add) + else + flash[:error] = issue.errors.full_messages.join('
').html_safe + end + redirect_to :back + end + + def create + contact_ids = [] + if params[:contacts_issue].present? + contact_ids << (params[:contacts_issue][:contact_ids] || params[:contacts_issue][:contact_id]) + else + contact_ids << params[:contact_id] + end + contact_ids.flatten.compact.uniq.each do |contact_id| + ContactsIssue.create(:issue_id => @issue.id, :contact_id => contact_id) + end + respond_to do |format| + format.html { redirect_to_referer_or { render :text => 'Added.', :layout => true } } + format.js + end + end + + def new + end + + def delete + @issue.contacts.delete(@contact) + respond_to do |format| + format.html { redirect_to :back } + format.js + end + end + + def close + @issue.init_journal(User.current) + @issue.status = IssueStatus.where(:is_closed => true).first + @issue.save + respond_to do |format| + format.js + format.html { redirect_to :back } + end + end + + def autocomplete_for_contact + q = params[:q].to_s + scope = Contact.where({}) + q.split(' ').collect { |search_string| scope = scope.live_search(search_string) } unless q.blank? + @contacts = scope.visible.includes(:avatar).order(Contact.fields_for_order_statement).by_project(params[:cross_project_contacts] == '1' ? nil : @project).limit(100) + @contacts -= @issue.contacts if @issue + render :layout => false + end + + private + + def find_contact + @contact = Contact.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_issue + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_mailer_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_mailer_controller.rb new file mode 100644 index 0000000..05d3af8 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_mailer_controller.rb @@ -0,0 +1,38 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsMailerController < ActionController::Base + before_action :check_credential + helper :contacts + # Submits an incoming email to ContactsMailer + def index + options = params.dup + email = options.delete(:email) + head ContactsMailer.receive(email, options) ? :created : :unprocessable_entity + end + + private + + def check_credential + User.current = nil + unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key + render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403 + end + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_projects_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_projects_controller.rb new file mode 100644 index 0000000..42f8e5a --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_projects_controller.rb @@ -0,0 +1,87 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsProjectsController < ApplicationController + unloadable + + before_action :find_optional_project, :find_contact + before_action :find_related_project, :only => [:destroy, :create] + before_action :check_count, :only => :destroy + + accept_api_auth :create, :destroy + + helper :contacts + + def new + @show_form = "true" + respond_to do |format| + format.html { redirect_to :back } + format.js + end + rescue ::ActionController::RedirectBackError + render :text => 'Project added.', :layout => true + end + + def create + @contact.projects << @related_project + if @contact.save + respond_to do |format| + format.html { redirect_to :back } + format.js { render :action => "new" } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { redirect_to :back } + format.js { render :action => "new" } + format.api { render_validation_errors(@contact) } + end + end + end + + def destroy + @contact.projects.delete(@related_project) + respond_to do |format| + format.html { redirect_to :back } + format.js {render :action => "new"} + format.api { render_api_ok } + end + end + + private + + def find_related_project + @related_project = Project.find((params[:project] && params[:project][:id]) || params[:id]) + raise Unauthorized unless User.current.allowed_to?(:edit_contacts, @related_project) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def check_count + deny_access if @contact.projects.size <= 1 + end + + def find_contact + @contact = Contact.find(params[:contact_id]) + raise Unauthorized unless @contact.editable? + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_settings_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_settings_controller.rb new file mode 100644 index 0000000..09ffb91 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_settings_controller.rb @@ -0,0 +1,34 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsSettingsController < ApplicationController + unloadable + before_action :find_project_by_project_id, :authorize + + def save + settings = params[:contacts_settings] + settings = settings.to_unsafe_hash if settings.class.to_s == 'ActionController::Parameters' + if settings && settings.is_a?(Hash) + settings.map do |k, v| + ContactsSetting[k, @project.id] = v + end + end + redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => params[:tab] + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_tags_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_tags_controller.rb new file mode 100644 index 0000000..a49283f --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_tags_controller.rb @@ -0,0 +1,98 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsTagsController < ApplicationController + unloadable + before_action :require_admin, :except => [:index] + before_action :find_tag, :only => [:edit, :update] + before_action :bulk_find_tags, :only => [:context_menu, :merge, :destroy] + + accept_api_auth :index + + def index + @tags = Contact.all_tag_counts(:order => :name) + respond_to do |format| + format.api + end + end + + def edit + end + + def destroy + @tags.each do |tag| + begin + tag.reload.destroy + Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + tag.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save} + rescue ::ActiveRecord::RecordNotFound # raised by #reload if tag no longer exists + # nothing to do, tag was already deleted (eg. by a parent) + end + end + redirect_back_or_default(:controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags") + end + + def update + old_name = @tag.name + @tag.name = params[:tag][:name] + if @tag.save + Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + old_name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save} + flash[:notice] = l(:notice_successful_update) + respond_to do |format| + format.html { redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags" } + end + else + respond_to do |format| + format.html { render :action => "edit"} + end + end + end + + def context_menu + @tag = @tags.first if (@tags.size == 1) + @back = back_url + render :layout => false + end + + def merge + if request.post? && params[:tag] && params[:tag][:name] + RedmineCrm::Tagging.transaction do + tag = RedmineCrm::Tag.where(:name => params[:tag][:name]).first || RedmineCrm::Tag.create(params[:tag]) + RedmineCrm::Tagging.where(:tag_id => @tags.map(&:id)).update_all(:tag_id => tag.id) + @tags.select{|t| t.id != tag.id}.each do |t| + t.destroy + Contact.where("#{Contact.table_name}.cached_tag_list LIKE ?", '%' + t.name + '%').includes(:tags).each{|c| c.tag_list = c.all_tags_list; c.save} + end + redirect_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags" + end + end + end + + private + + def bulk_find_tags + @tags = RedmineCrm::Tag.where(:id => params[:id] ? [params[:id]] : params[:ids]) + raise ActiveRecord::RecordNotFound if @tags.empty? + end + + def find_tag + @tag = RedmineCrm::Tag.find(params[:id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/plugins/redmine_contacts/app/controllers/contacts_vcf_controller.rb b/plugins/redmine_contacts/app/controllers/contacts_vcf_controller.rb new file mode 100644 index 0000000..ecea174 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/contacts_vcf_controller.rb @@ -0,0 +1,97 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsVcfController < ApplicationController + unloadable + + before_action :find_project_by_project_id, :authorize + + def load + begin + vcard = Vcard::Vcard.decode(params[:contact_vcf]).first + contact = {} + fill_name(vcard, contact) + contact[:phone] = vcard.telephones.join(', ') + contact[:email] = vcard.emails.join(', ') + contact[:website] = vcard.url.uri if vcard.url + contact[:birthday] = vcard.birthday + fill_background(vcard, contact) + fill_title(vcard, contact) + fill_address(vcard, contact) if vcard['ADR'] + fill_company(vcard, contact) if vcard.org + + respond_to do |format| + format.html { redirect_to :controller => 'contacts', :action => 'new', :project_id => @project, :contact => contact } + end + + rescue Exception => e + flash[:error] = e.message + respond_to do |format| + format.html { redirect_to :back } + end + end + end + + private + + def fill_name(vcard, contact) + vcard_charset = get_field_encoding(vcard, 'N') + contact[:first_name] = encode(vcard_charset, vcard.name.given) + contact[:middle_name] = encode(vcard_charset, vcard.name.additional) + contact[:last_name] = encode(vcard_charset, vcard.name.family) + end + + def fill_address(vcard, contact) + vcard_charset = get_field_encoding(vcard, 'ADR') + contact[:address_attributes] = {} + contact[:address_attributes][:street1] = encode(vcard_charset, vcard.address.street) + contact[:address_attributes][:city] = encode(vcard_charset, vcard.address.locality) + contact[:address_attributes][:postcode] = encode(vcard_charset, vcard.address.postalcode) + contact[:address_attributes][:region] = encode(vcard_charset, vcard.address.region) + end + + def fill_background(vcard, contact) + vcard_charset = get_field_encoding(vcard, 'NOTE') + contact[:background] = encode(vcard_charset, vcard.note) + end + + def fill_company(vcard, contact) + vcard_charset = get_field_encoding(vcard, 'ORG') + contact[:company] = encode(vcard_charset, vcard.org.first) + end + + def fill_title(vcard, contact) + vcard_charset = get_field_encoding(vcard, 'TITLE') + contact[:job_title] = encode(vcard_charset, vcard.title) + end + + def get_field_encoding(vcard, field_name) + vcard.fields.find { |field| field.name == field_name }.try(:pvalue, 'CHARSET') + end + + def encode(vcard_charset, field) + return field if vcard_charset.nil? + if RUBY_VERSION < '1.9' + Iconv.conv('UTF-8', vcard_charset, field) + else + field.force_encoding(vcard_charset).encode('UTF-8') + end + end + +end diff --git a/plugins/redmine_contacts/app/controllers/crm_queries_controller.rb b/plugins/redmine_contacts/app/controllers/crm_queries_controller.rb new file mode 100644 index 0000000..cd454cf --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/crm_queries_controller.rb @@ -0,0 +1,133 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CrmQueriesController < ApplicationController + before_action :find_query_class + before_action :find_query, :except => [:new, :create, :index] + before_action :find_optional_project, :only => [:new, :create] + before_action :set_menu_item + + accept_api_auth :index + + helper :queries + include QueriesHelper + + def index + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = per_page_option + end + @query_count = @query_class.visible.count + @query_pages = Paginator.new @query_count, @limit, params['page'] + @queries = @query_class.visible. + order("#{Query.table_name}.name"). + limit(@limit). + offset(@offset). + all + respond_to do |format| + format.api + end + end + + def new + @query = @query_class.new + @query.user = User.current + @query.project = @project + @query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin? + @query.build_from_params(params) + end + + def create + @query = @query_class.new(params_hash[:query]) + @query.user = User.current + @query.project = params_hash[:query_is_for_all] ? nil : @project + @query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin? + @query.build_from_params(params_hash) + @query.column_names = nil if params_hash[:default_columns] + + if @query.save + flash[:notice] = l(:notice_successful_create) + redirect_to_list(:query_id => @query) + else + render :action => 'new', :layout => !request.xhr? + end + end + + def edit + end + + def update + @query.attributes = params_hash[:query] + @query.project = nil if params_hash[:query_is_for_all] + @query.visibility = @query_class::VISIBILITY_PRIVATE unless User.current.allowed_to?("manage_public_#{@object_type}s_queries".to_sym, @project) || User.current.admin? + @query.build_from_params(params_hash) + @query.column_names = nil if params_hash[:default_columns] + + if @query.save + flash[:notice] = l(:notice_successful_update) + redirect_to_list(:query_id => @query) + else + render :action => 'edit' + end + end + + def destroy + @query.destroy + redirect_to_list(:set_filter => 1) + end + +private + def find_query_class + raise NameError if params[:object_type].blank? + @query_class = Object.const_get("#{params[:object_type].to_s.camelcase}Query") + @object_type = params[:object_type] + return false unless @query_class.is_a?(Query) + rescue NameError + render_404 + end + + def find_query + @query = @query_class.find(params[:id]) + @project = @query.project + render_403 unless @query.editable_by?(User.current) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_optional_project + @project = Project.find(params[:project_id]) if params[:project_id] + render_403 unless User.current.allowed_to?("save_#{@object_type}s_queries".to_sym, @project, :global => true) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def redirect_to_list(options) + redirect_to url_for({:controller => "#{@object_type}s", :action => "index", :project_id => @project}.merge(options)) + end + + def set_menu_item + menu_items[:project_tabs][:actions][action_name.to_sym] = "#{@object_type}s" + end + + def params_hash + @params_hash ||= params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash.symbolize_keys : params + end +end diff --git a/plugins/redmine_contacts/app/controllers/deal_categories_controller.rb b/plugins/redmine_contacts/app/controllers/deal_categories_controller.rb new file mode 100644 index 0000000..2dffc41 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deal_categories_controller.rb @@ -0,0 +1,109 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealCategoriesController < ApplicationController + unloadable + menu_item :settings + model_object DealCategory + before_action :find_model_object, :except => [:new, :index, :create] + before_action :find_project_from_association, :except => [:new, :index, :create] + before_action :find_project_by_project_id, :only => [:new, :index, :create] + before_action :authorize + accept_api_auth :index, :update, :create, :destroy + + def index + @categories = @project.deal_categories + respond_to do |format| + format.api + end + end + + def create + @category = @project.deal_categories.build + @category.safe_attributes = params[:category] + if @category.save + flash[:notice] = l(:notice_successful_create) + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api { render_api_ok } + end + + else + respond_to do |format| + format.html { render :action => 'new' } + format.api { render_validation_errors(@category) } + end + end + end + + def new + @category = @project.deal_categories.build(params[:category]) + end + + def edit + end + + def update + @category.safe_attributes = params[:category] + if @category.save + # @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts] + flash[:notice] = l(:notice_successful_update) + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@category) } + end + end + end + + def destroy + @deal_count = @category.deals.size + if @deal_count == 0 || params[:todo] || api_request? + reassign_to = nil + if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?) + reassign_to = @project.deal_categories.find_by_id(params[:reassign_to_id]) + end + @category.destroy(reassign_to) + respond_to do |format| + format.html { redirect_to_settings_in_projects } + format.api { render_api_ok } + end + return + end + @categories = @project.deal_categories - [@category] + end + + private + + def redirect_to_settings_in_projects + redirect_to settings_project_path(@project, :tab => 'deals') + end + + # Wrap ApplicationController's find_model_object method to set + # @category instead of just @deal_category + def find_model_object + super + @category = @object + @project = @category.project + end +end diff --git a/plugins/redmine_contacts/app/controllers/deal_contacts_controller.rb b/plugins/redmine_contacts/app/controllers/deal_contacts_controller.rb new file mode 100644 index 0000000..9774a12 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deal_contacts_controller.rb @@ -0,0 +1,81 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealContactsController < ApplicationController + unloadable + + before_action :find_project_by_project_id, :authorize + before_action :find_contact, :only => :delete + before_action :find_deal + + helper :deals + helper :contacts + + def search + @contacts = contacts.limit(10) - @deal.all_contacts + end + + def autocomplete + @contacts = contacts.live_search(params[:q]).limit(100) - @deal.all_contacts + render :layout => false + end + + def add + if params[:contact_id] && request.post? + find_contact + unless @deal.all_contacts.include?(@contact) + @deal.related_contacts << @contact + @deal.save + end + end + + respond_to do |format| + format.html do + redirect_to :back + end + format.js + end + end + + def delete + @deal.related_contacts.delete(@contact) + respond_to do |format| + format.html { redirect_to :back } + format.js + end + end + + private + + def contacts + Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project) + end + + def find_contact + @contact = Contact.find(params[:contact_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_deal + @deal = Deal.find(params[:deal_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/plugins/redmine_contacts/app/controllers/deal_imports_controller.rb b/plugins/redmine_contacts/app/controllers/deal_imports_controller.rb new file mode 100644 index 0000000..f3f8c62 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deal_imports_controller.rb @@ -0,0 +1,36 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + + +class DealImportsController < ImporterBaseController + menu_item :deals + helper :deals + + def klass + DealImport + end + + def importer_klass + DealKernelImport + end + + def instance_index + project_deals_path(:project_id => @project.id) + end +end diff --git a/plugins/redmine_contacts/app/controllers/deal_statuses_controller.rb b/plugins/redmine_contacts/app/controllers/deal_statuses_controller.rb new file mode 100644 index 0000000..b0121db --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deal_statuses_controller.rb @@ -0,0 +1,95 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealStatusesController < ApplicationController + unloadable + + layout 'admin' + before_action :require_admin, :except => :assing_to_project + before_action :find_project_by_project_id, :authorize, :only => :assing_to_project + + accept_api_auth :index + + def index + @deal_statuses = DealStatus.order(:position) + + respond_to do |format| + format.api + format.html { render :action => 'index', :layout => false if request.xhr? } + end + end + + def new + @deal_status = DealStatus.new + end + + def create + @deal_status = DealStatus.new + @deal_status.safe_attributes = params[:deal_status] + if @deal_status.save + flash[:notice] = l(:notice_successful_create) + redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses' + else + render :action => 'new' + end + end + + def edit + @deal_status = DealStatus.find(params[:id]) + end + + def update + @deal_status = DealStatus.find(params[:id]) + @deal_status.safe_attributes = params[:deal_status] + @deal_status.insert_at(@deal_status.position) if @deal_status.position_changed? + if @deal_status.save + respond_to do |format| + format.html do + flash[:notice] = l(:notice_successful_update) + redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses' + end + format.js { head 200 } + end + else + respond_to do |format| + format.html do + render :action => 'edit' + end + format.js { head 422 } + end + end + end + + def destroy + DealStatus.find(params[:id]).destroy + redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses' + rescue + flash[:error] = l(:error_unable_delete_deal_status) + redirect_to :action => 'plugin', :id => 'redmine_contacts', :controller => 'settings', :tab => 'deal_statuses' + end + + def assing_to_project + if request.put? + @project.deal_statuses = !params[:deal_statuses].blank? ? DealStatus.find(params[:deal_statuses]) : [] + @project.save + flash[:notice] = l(:notice_successful_update) + end + redirect_to :controller => 'projects', :action => 'settings', :tab => 'deals', :id => @project + end +end diff --git a/plugins/redmine_contacts/app/controllers/deals_controller.rb b/plugins/redmine_contacts/app/controllers/deals_controller.rb new file mode 100644 index 0000000..cede1b2 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deals_controller.rb @@ -0,0 +1,333 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealsController < ApplicationController + unloadable + + PRICE_TYPE_PULLDOWN = [l(:label_price_fixed_bid), l(:label_price_per_hour)] + + before_action :find_deal, :only => [:show, :edit, :update, :destroy] + before_action :find_project, :only => [:new, :create, :update_form] + before_action :bulk_find_deals, :only => [:bulk_update, :bulk_edit, :bulk_destroy, :context_menu] + before_action :authorize, :except => [:index] + before_action :find_optional_project, :only => [:index] + before_action :update_deal_from_params, :only => [:edit, :update] + before_action :build_new_deal_from_params, :only => [:new, :update_form] + before_action :find_deal_attachments, :only => :show + skip_before_filter :authorize, :only => :add_product_line if RedmineContacts.products_plugin_installed? + + accept_api_auth :index, :show, :create, :update, :destroy + + helper :attachments + helper :timelog + helper :watchers + helper :custom_fields + helper :context_menus + helper :sort + helper :crm_queries + helper :notes + helper :queries + helper :calendars + include QueriesHelper + include CrmQueriesHelper + include WatchersHelper + include DealsHelper + include SortHelper + if RedmineContacts.products_plugin_installed? + include ProductsHelper + helper :products + end + + def index + retrieve_crm_query('deal') + sort_init(@query.sort_criteria.empty? ? [['created_on', 'desc']] : @query.sort_criteria) + sort_update(@query.sortable_columns) + @query.sort_criteria = sort_criteria.to_a + + if @query.valid? + case params[:format] + when 'csv', 'pdf' + @limit = Setting.issues_export_limit.to_i + when 'atom' + @limit = Setting.feeds_limit.to_i + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = per_page_option + end + + @deals_count = @query.object_count + @deals_scope = @query.objects_scope + @deal_amount = @query.deal_amount + @deal_weighted_amount = @query.weighted_amount + @deals_pages = Paginator.new @deals_count, @limit, params['page'] + @offset ||= @deals_pages.offset + @deal_count_by_group = @query.object_count_by_group + @deals = @query.results_scope( + :include => [{ :contact => [:avatar, :projects, :address] }, :author], + :search => params[:search], + :order => sort_clause, + :limit => @limit, + :offset => @offset + ) + + if deals_list_style == 'crm_calendars/crm_calendar' + retrieve_crm_calendar(:start_date_field => 'due_date') + @calendar.events = @query.results_scope( + :include => [:contact], + :search => params[:search], + :conditions => ['due_date BETWEEN ? AND ?', @calendar.startdt, @calendar.enddt] + ) + end + + respond_to do |format| + format.html { request.xhr? ? render(:partial => deals_list_style, :layout => false) : last_notes } + format.api + format.atom { render_feed(@deals, :title => "#{@project || Setting.app_title}: #{l(:label_order_plural)}") } + format.csv { send_data(deals_to_csv(@deals), :type => 'text/csv; header=present', :filename => 'deals.csv') } + format.pdf { send_data(deals_to_pdf(@deals, @project, @query), :type => 'application/pdf', :filename => 'deals.pdf') } + end + else + respond_to do |format| + format.html { render(:template => 'deals/index', :layout => !request.xhr?) } + format.any(:atom, :csv, :pdf) { render(:nothing => true) } + format.api { render_validation_errors(@query) } + end + end + rescue ActiveRecord::RecordNotFound + render_404 + end + + def show + @note = DealNote.new(:created_on => Time.now) + respond_to do |format| + format.html do + @deal_issues = @deal.issues.visible + @deal.viewed + @deal_events = (@deal.deal_processes.where("#{DealProcess.table_name}.old_value IS NOT NULL").includes([:to, :from, :author]) | @deal.notes.includes([:attachments, :author])).map{|o| {:date => o.is_a?(DealProcess) ? o.created_at : o.created_on, :author => o.author, :object => o} } + @deal_events.sort! { |x, y| y[:date] <=> x[:date] } + end + format.api + end + end + + def new + end + + def create + @deal = Deal.new + @deal.safe_attributes = params[:deal] + @deal.project = @project + @deal.author ||= User.current + @deal.price = parsed_price(params[:deal][:price]) + @deal.init_deal_process(User.current) + if @deal.save + flash[:notice] = l(:notice_successful_create) + respond_to do |format| + format.html { redirect_to(params[:continue] ? { :action => 'new' } : { :action => 'show', :id => @deal }) } + format.api { render :action => 'show', :status => :created, :location => deal_url(@deal) } + end + + else + respond_to do |format| + format.html { render :action => 'new' } + format.api { render_validation_errors(@deal) } + end + end + end + + def update + @deal.init_deal_process(User.current) + @deal.safe_attributes = params[:deal] + if @deal.save + # @deal.contacts = [Contact.find(params[:contacts])] if params[:contacts] + retrieve_crm_query('deal') + @deals_scope = @query.objects_scope + flash[:notice] = l(:notice_successful_update) + respond_to do |format| + format.html { redirect_back_or_default(:action => 'show', :id => @deal) } + format.api { render_api_ok } + format.js { render :update_total } + end + else + respond_to do |format| + format.html { render :action => 'edit' } + format.api { render_validation_errors(@deal) } + format.js { render "alert('Error!')" } + end + end + end + + def edit + respond_to do |format| + format.html {} + format.xml {} + end + end + + def destroy + if @deal.destroy + flash[:notice] = l(:notice_successful_delete) + respond_to do |format| + format.html { redirect_to :action => 'index', :project_id => params[:project_id] } + format.api { render_api_ok } + end + else + flash[:error] = l(:notice_unsuccessful_save) + end + end + + def context_menu + @deal = @deals.first if @deals.size == 1 + @can = { :edit => User.current.allowed_to?(:edit_deals, @projects), + :delete => User.current.allowed_to?(:delete_deals, @projects) } + + @back = back_url + render :layout => false + end + + def bulk_destroy + @deals.each do |deal| + begin + deal.reload.destroy + rescue ::ActiveRecord::RecordNotFound # raised by #reload if deal no longer exists + # nothing to do, deal was already deleted (eg. by a parent) + end + end + respond_to do |format| + format.html { redirect_back_or_default(:action => 'index', :project_id => params[:project_id]) } + format.api { head :ok } + end + end + + def bulk_edit + @available_statuses = @projects.map(&:deal_statuses).inject { |memo, w| memo & w } + @custom_fields = DealCustomField.order(:name) + @available_categories = @projects.map(&:deal_categories).inject { |memo, w| memo & w } + @assignables = @projects.map(&:assignable_users).inject { |memo, a| memo & a } + end + + def bulk_update + unsaved_deal_ids = [] + @deals.each do |deal| + deal.reload + deal.init_deal_process(User.current) + deal.safe_attributes = parse_params_for_bulk_deal_attributes(params) + unless deal.save + # Keep unsaved deal ids to display them in flash error + unsaved_deal_ids << deal.id + end + if params[:note] && !params[:note][:content].blank? + note = DealNote.new + note.safe_attributes = params[:note] + note.author = User.current + deal.notes << note + end + end + set_flash_from_bulk_contact_save(@deals, unsaved_deal_ids) + redirect_back_or_default(:controller => 'deals', :action => 'index', :project_id => @project) + end + + private + + def last_notes(count = 5) + # TODO: ИÑправить говнокод Ñтот и выделить вÑе в плагин acts-as-noteble + scope = DealNote.where({}) + scope = scope.where("#{Deal.table_name}.project_id = ?", @project.id) if @project + + @last_notes = scope.visible.order("#{DealNote.table_name}.created_on DESC").limit(count) + end + + def build_new_deal_from_params + if params[:id].blank? + @deal = Deal.new + @deal.assigned_to_id = User.current.id + @deal.name = params[:name] if params[:name] + @deal.contact = Contact.find(params[:contact_id]) if params[:contact_id] + if params[:copy_from] + begin + @copy_from = Deal.visible.find(params[:copy_from]) + @deal.copy_from(@copy_from) + rescue ActiveRecord::RecordNotFound + render_404 + return + end + end + else + @deal = Deal.visible.find(params[:id]) + end + + @deal.project = @project + @deal.author ||= User.current + @deal.safe_attributes = params[:deal] + + @available_watchers = (@deal.project.users.sort + @deal.watcher_users).uniq + end + + def update_deal_from_params + end + + def update_form + end + + def find_deal_attachments + @deal_attachments = Attachment.where(:container_type => 'Note', :container_id => @deal.notes.map(&:id)).order(:created_on) + end + + def bulk_find_deals + @deals = Deal.where(:id => (params[:id] || params[:ids])).includes([:project, :contact]) + raise ActiveRecord::RecordNotFound if @deals.empty? + if @deals.detect { |deal| !deal.visible? } + deny_access + return + end + @projects = @deals.collect(&:project).compact.uniq + @project = @projects.first if @projects.size == 1 + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_deal + @deal = Deal.where(:id => params[:id]).includes([:project, :status, :category]).first + @project = @deal.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_project(project_id = nil) + project_id ||= (params[:deal] && params[:deal][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def parse_params_for_bulk_deal_attributes(params) + attributes = (params[:deal] || {}).reject { |_k, v| v.blank? } + attributes.keys.each { |k| attributes[k] = '' if attributes[k] == 'none' } + attributes[:custom_field_values].reject! { |_k, v| v.blank? } if attributes[:custom_field_values] + attributes + end + + def parsed_price(price) + return unless price + price.gsub!(ContactsSetting.thousands_delimiter, '') + price.gsub!(ContactsSetting.decimal_separator, '.') + price.to_f + end +end diff --git a/plugins/redmine_contacts/app/controllers/deals_tasks_controller.rb b/plugins/redmine_contacts/app/controllers/deals_tasks_controller.rb new file mode 100644 index 0000000..c2f69a3 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/deals_tasks_controller.rb @@ -0,0 +1,74 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealsTasksController < ApplicationController + unloadable + + before_action :find_project_by_project_id, :authorize + before_action :find_deal, :except => [:close] + before_action :find_issue, :except => [:new] + + def new + issue = Issue.new + issue.subject = params[:task_subject] + issue.project = @project + issue.tracker_id = params[:task_tracker] + issue.author = User.current + issue.due_date = params[:due_date] + issue.assigned_to_id = params[:assigned_to] + issue.description = params[:task_description] + issue.status = IssueStatus.default + if issue.save + flash[:notice] = l(:notice_successful_add) + @deal.issues << issue + @deal.save + redirect_to :back + return + else + redirect_to :back + end + end + + def close + @issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true }) + @issue.save + respond_to do |format| + format.js do + render :update do |page| + page["issue_#{params[:issue_id]}"].visual_effect :fade + end + end + format.html {redirect_to :back } + end + end + + private + + def find_deal + @deal = Deal.find(params[:deal_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_issue + @issue = Issue.find(params[:issue_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/plugins/redmine_contacts/app/controllers/importer_base_controller.rb b/plugins/redmine_contacts/app/controllers/importer_base_controller.rb new file mode 100644 index 0000000..08c3c91 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/importer_base_controller.rb @@ -0,0 +1,149 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ImporterBaseController < ApplicationController + unloadable + if Redmine::VERSION.to_s >= '3.2' + helper :imports + before_action :find_import, :only => [:show, :settings, :mapping, :run] + end + + before_action :find_project_by_project_id, :authorize + + def new + @importer = klass.new + if Redmine::VERSION.to_s >= '3.2' + render 'importers/kernel_new' + else + render 'importers/new' + end + end + + def create + if Redmine::VERSION.to_s >= '3.2' + @import = importer_klass.new + @import.user = User.current + @import.project = @project + @import.file = params[:file] + @import.set_default_settings + + if @import.save + redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project + else + render 'importers/kernel_new' + end + else + @importer = klass.new(params[klass.to_s.underscore.to_sym]) + @importer.project = @project + if @importer.file && @importer.save + redirect_to instance_index + else + render 'importers/new' + end + end + end + + def show + render 'importers/show' + end + + def settings + if request.post? && @import.parse_file + return redirect_to :controller => klass.name.tableize, :action => 'mapping', :id => @import, :project_id => @project + end + render 'importers/settings' + + rescue CSV::MalformedCSVError => e + flash.now[:error] = l(:error_invalid_csv_file_or_settings) + render 'importers/settings' + rescue ArgumentError, Encoding::InvalidByteSequenceError => e + flash.now[:error] = l(:error_invalid_file_encoding, :encoding => ERB::Util.h(@import.settings['encoding'])) + render 'importers/settings' + rescue SystemCallError => e + flash.now[:error] = l(:error_can_not_read_import_file) + render 'importers/settings' + end + + def mapping + mapping_object = klass.new.klass.new + @attributes = mapping_object.safe_attribute_names + @custom_fields = mapping_object.custom_field_values.map(&:custom_field) + + if request.post? + respond_to do |format| + format.html do + if params[:previous] + redirect_to :controller => klass.name.tableize, :action => 'settings', :id => @import, :project_id => @project + else + redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project + end + end + end + else + render 'importers/mapping' + end + end + + def run + if request.post? + @current = @import.run( + :max_items => max_items_per_request, + :max_time => 10.seconds + ) + respond_to do |format| + format.html do + if @import.finished? + redirect_to :controller => klass.name.tableize, :action => 'show', :id => @import, :project_id => @project + else + redirect_to :controller => klass.name.tableize, :action => 'run', :id => @import, :project_id => @project + end + end + format.js { render 'importers/run' } + end + else + render 'importers/run' + end + end + + private + + def find_import + @import = Import.where(:user_id => User.current.id, :filename => params[:id]).first + if @import.nil? + render_404 + return + elsif @import.finished? && action_name != 'show' + redirect_to new_project_contact_import_path(@import) + return + end + update_from_params if request.post? + end + + def update_from_params + if params[:import_settings].present? + @import.settings ||= {} + @import.settings.merge!(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash['import_settings'] : params['import_settings']) + @import.save! + end + end + + def max_items_per_request + 5 + end +end diff --git a/plugins/redmine_contacts/app/controllers/notes_controller.rb b/plugins/redmine_contacts/app/controllers/notes_controller.rb new file mode 100644 index 0000000..b827564 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/notes_controller.rb @@ -0,0 +1,130 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class NotesController < ApplicationController + unloadable + default_search_scope :notes + # before_filter :find_model_object + before_action :find_note, :only => [:show, :edit, :update, :destroy] + before_action :find_project, :only => :create + before_action :find_note_source, :only => :create + before_action :find_optional_project, :only => :show + + accept_api_auth :show, :create, :update, :destroy + + helper :attachments + helper :custom_fields + + def show + (render_403; return false) unless @note.visible? + respond_to do |format| + format.html + format.api + end + end + + def new + find_note_source + @note = Note.new + @note.source = @note_source + end + + def edit + (render_403; return false) unless @note.editable_by?(User.current, @project) + end + + def update + @note.safe_attributes = params[:note] + if @note.save + @note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time] + attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads]))) + render_attachment_warning_if_needed(@note) + flash[:notice] = l(:notice_successful_update) + respond_to do |format| + format.html { redirect_back_or_default({ :action => 'show', :project_id => @note.source.project, :id => @note }) } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :action => 'edit', :project_id => params[:project_id], :id => @note } + format.api { render_validation_errors(@note) } + end + end + end + + def create + @note = Note.new + @note.safe_attributes = params[:note] + @note.source = @note_source + @note.note_time = params[:note][:note_time] if params[:note] && params[:note][:note_time] + @note.author = User.current + if @note.save + attachments = Attachment.attach_files(@note, (params[:attachments] || (params[:note] && params[:note][:uploads]))) + render_attachment_warning_if_needed(@note) + + flash[:notice] = l(:notice_successful_create) + respond_to do |format| + format.js + format.html { redirect_to :back } + format.api { render :action => 'show', :status => :created, :location => note_url(@note) } + end + else + respond_to do |format| + format.html { redirect_to :back } + format.api { render_validation_errors(@note) } + end + end + end + + def destroy + (render_403; return false) unless @note.destroyable_by?(User.current, @project) + @note.destroy + respond_to do |format| + format.js + format.html { redirect_to :action => 'show', :project_id => @project, :id => @note.source } + format.api { render_api_ok } + end + + # redirect_to :action => 'show', :project_id => @project, :id => @contact + end + + private + + def find_project(project_id = nil) + project_id ||= (params[:note] && params[:note][:project_id]) || params[:project_id] + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_note + @note = Note.find(params[:id]) + @project ||= @note.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_note_source + note_source_type = (params[:note] && params[:note][:source_type]) || params[:source_type] + note_source_id = (params[:note] && params[:note][:source_id]) || params[:source_id] + + klass = Object.const_get(note_source_type.camelcase) + @note_source = klass.find(note_source_id) + end +end diff --git a/plugins/redmine_contacts/app/controllers/tasks_controller.rb b/plugins/redmine_contacts/app/controllers/tasks_controller.rb new file mode 100644 index 0000000..b244e18 --- /dev/null +++ b/plugins/redmine_contacts/app/controllers/tasks_controller.rb @@ -0,0 +1,110 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class TasksController < ApplicationController + unloadable + + before_action :find_project_by_project_id, :authorize, :except => [:index] + before_action :find_optional_project, :only => :index + before_action :find_taskable, :except => [:index, :add, :close] + before_action :find_issue, :except => [:index, :new] + + def new + issue = Issue.new + issue.subject = params[:task_subject] + issue.project = @project + issue.tracker_id = params[:task_tracker] + issue.author = User.current + issue.due_date = params[:due_date] + issue.assigned_to_id = params[:assigned_to] + issue.description = params[:task_description] + issue.status = IssueStatus.default + if issue.save + flash[:notice] = l(:notice_successful_add) + @taskable.issues << issue + @taskable.save + redirect_to :back + return + else + redirect_to :back + end + end + + def add + @show_form = 'true' + + if params[:source_id] && params[:source_type] && request.post? + find_taskable + @taskable.issues << @issue + @taskable.save + end + + taskable_name = @taskable.class.name.underscore + + respond_to do |format| + format.html { redirect_to :back } + format.js do + render :update do |page| + page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s" + end + end + end + end + + def delete + @issue.taskables.delete(@taskable) + taskable_name = @taskable.class.name.underscore + respond_to do |format| + format.html { redirect_to :back } + format.js do + render :update do |page| + page.replace_html "issue_#{taskable_name}s", :partial => "issues/#{taskable_name}s" + end + end + end + end + + def close + @issue.status = IssueStatus.find(:first, :conditions => { :is_closed => true }) + @issue.save + respond_to do |format| + format.js do + render :update do |page| + page["issue_#{params[:issue_id]}"].visual_effect :fade + end + end + format.html { redirect_to :back } + end + end + + private + + def find_taskable + klass = Object.const_get(params[:source_type].camelcase) + @taskable = klass.find(params[:source_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_issue + @issue = Issue.find(params[:issue_id]) + rescue ActiveRecord::RecordNotFound + render_404 + end +end diff --git a/plugins/redmine_contacts/app/helpers/contacts_helper.rb b/plugins/redmine_contacts/app/helpers/contacts_helper.rb new file mode 100644 index 0000000..fe8a41c --- /dev/null +++ b/plugins/redmine_contacts/app/helpers/contacts_helper.rb @@ -0,0 +1,269 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module ContactsHelper + + def contact_tabs(contact) + contact_tabs = [] + contact_tabs << {:name => 'notes', :partial => 'contacts/notes', :label => l(:label_crm_note_plural)} if contact.visible? + contact_tabs << {:name => 'contacts', :partial => 'company_contacts', :label => l(:label_contact_plural) + (contact.company_contacts.visible.count > 0 ? " (#{contact.company_contacts.count})" : "")} if contact.is_company? + contact_tabs << {:name => 'deals', :partial => 'deals/related_deals', :label => l(:label_deal_plural) + (contact.all_visible_deals.size > 0 ? " (#{contact.all_visible_deals.size})" : "") } if User.current.allowed_to?(:add_deals, @project) + contact_tabs + end + + def settings_contacts_tabs + ret = [ + {:name => 'general', :partial => 'settings/contacts/contacts_general', :label => :label_general}, + {:name => 'money', :partial => 'settings/contacts/money', :label => :label_crm_money_settings}, + {:name => 'tags', :partial => 'settings/contacts/contacts_tags', :label => :label_crm_tags_plural}, + {:name => 'deal_statuses', :partial => 'settings/contacts/contacts_deal_statuses', :label => :label_crm_deal_status_plural}, + ] + ret.push({:name => 'hidden', :partial => 'settings/contacts/contacts_hidden', :label => :label_crm_contacts_hidden}) if params[:hidden] + ret + end + + def collection_for_visibility_select + [[l(:label_crm_contacts_visibility_project), Contact::VISIBILITY_PROJECT], + [l(:label_crm_contacts_visibility_public), Contact::VISIBILITY_PUBLIC], + [l(:label_crm_contacts_visibility_private), Contact::VISIBILITY_PRIVATE]] + end + + def contact_list_styles_for_select + list_styles = [[l(:label_crm_list_excerpt), "list_excerpt"]] + list_styles += [[l(:label_crm_list_list), "list"], + [l(:label_crm_list_cards), "list_cards"]] + end + + def contacts_list_style + list_styles = contact_list_styles_for_select.map(&:last) + if params[:contacts_list_style].blank? + list_style = list_styles.include?(session[:contacts_list_style]) ? session[:contacts_list_style] : RedmineContacts.default_list_style + else + list_style = list_styles.include?(params[:contacts_list_style]) ? params[:contacts_list_style] : RedmineContacts.default_list_style + end + session[:contacts_list_style] = list_style + end + + def authorized_for_permission?(permission, project, global = false) + User.current.allowed_to?(permission, project, :global => global) + end + + def render_contact_projects_hierarchy(projects) + s = '' + project_tree(projects) do |project, level| + s << "
    " + name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') + s << "
  • " + name_prefix + link_to_project(project) + + s += ' ' + link_to(image_tag('delete.png'), + contact_contacts_project_path(@contact, :id => project.id, :project_id => @project.id), + :remote => true, + :method => :delete, + :style => "vertical-align: middle", + :class => "delete", + :title => l(:button_delete)) if (projects.size > 1 && User.current.allowed_to?(:edit_contacts, project)) + s << "
  • " + + s << "
" + end + s.html_safe + end + + def contact_to_vcard(contact) + return false unless ContactsSetting.vcard? + + card = Vcard::Vcard::Maker.make2 do |maker| + + maker.add_name do |name| + name.prefix = '' + name.given = contact.first_name.to_s + name.family = contact.last_name.to_s + name.additional = contact.middle_name.to_s + end + + maker.add_addr do |addr| + addr.preferred = true + addr.street = contact.street1.to_s.gsub("\r\n"," ").gsub("\n"," ") + addr.locality = contact.city.to_s + addr.region = contact.region.to_s + addr.postalcode = contact.postcode.to_s + addr.country = contact.country.to_s + addr.location = 'business' + end + + maker.title = contact.job_title.to_s + maker.org = contact.company.to_s + maker.birthday = contact.birthday.to_date unless contact.birthday.blank? + maker.add_note(contact.background.to_s.gsub("\r\n"," ").gsub("\n", ' ')) + + maker.add_url(contact.website.to_s) + + contact.phones.each { |phone| maker.add_tel(phone) } + contact.emails.each { |email| maker.add_email(email) } + end + avatar = contact.attachments.find_by_description('avatar') + card = card.encode.sub("END:VCARD", "PHOTO;BASE64:" + "\n " + [File.open(avatar.diskfile).read].pack('m').to_s.gsub(/[ \n]/, '').scan(/.{1,76}/).join("\n ") + "\nEND:VCARD") if avatar && avatar.readable? + + card.to_s + + end + def contacts_to_vcard(contacts) + return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true) + contacts.map{|c| contact_to_vcard(c) }.join("\r\n") + end + + def contacts_to_xls(contacts) + return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true) + require 'spreadsheet' + + Spreadsheet.client_encoding = 'UTF-8' + book = Spreadsheet::Workbook.new + sheet = book.create_worksheet + headers = [ "#", + l(:field_is_company), + l(:field_contact_first_name), + l(:field_contact_middle_name), + l(:field_contact_last_name), + l(:field_contact_job_title), + l(:field_contact_company), + l(:field_contact_phone), + l(:field_contact_email), + l(:label_crm_address), + l(:label_crm_city), + l(:label_crm_postcode), + l(:label_crm_region), + l(:label_crm_country), + l(:field_contact_skype), + l(:field_contact_website), + l(:field_birthday), + l(:field_contact_tag_names), + l(:label_crm_assigned_to), + l(:field_contact_background), + l(:field_created_on), + l(:field_updated_on) + ] + custom_fields = ContactCustomField.order('LOWER(name)') + custom_fields.each { |f| headers << f.name } + idx = 0 + row = sheet.row(idx) + row.replace headers + + contacts.each do |contact| + idx += 1 + row = sheet.row(idx) + fields = [contact.id, + contact.is_company ? 1 : 0, + contact.first_name, + contact.middle_name, + contact.last_name, + contact.job_title, + contact.company, + contact.phone, + contact.email, + contact.address.to_s.gsub("\r\n"," ").gsub("\n", ' '), + contact.city, + contact.postcode, + contact.region, + contact.country, + contact.skype_name, + contact.website, + format_date(contact.birthday), + contact.tag_list.to_s, + contact.assigned_to ? contact.assigned_to.name : "", + contact.background.to_s.gsub("\r\n"," ").gsub("\n", ' '), + format_date(contact.created_on), + format_date(contact.updated_on) + ] + contact.custom_field_values.sort_by{|v| v.custom_field.name.downcase}.each {|custom_value| fields << RedmineContacts::CSVUtils.csv_custom_value(custom_value) } + row.replace fields + end + + xls_stream = StringIO.new('') + book.write(xls_stream) + + return xls_stream.string + end + + def mail_macro(contact, message) + message = message.gsub(/%%NAME%%/, contact.first_name) + message = message.gsub(/%%FULL_NAME%%/, contact.name) + message = message.gsub(/%%COMPANY%%/, contact.company) if contact.company + message = message.gsub(/%%LAST_NAME%%/, contact.last_name) if contact.last_name + message = message.gsub(/%%MIDDLE_NAME%%/, contact.middle_name) if contact.middle_name + message = message.gsub(/%%DATE%%/, format_date(Date.today.to_s)) + + contact.custom_field_values.each do |value| + message = message.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s) + end + message + end + + def set_flash_from_bulk_contact_save(contacts, unsaved_contact_ids) + if unsaved_contact_ids.empty? + flash[:notice] = l(:notice_successful_update) unless contacts.empty? + else + flash[:error] = l(:notice_failed_to_save_contacts, + :count => unsaved_contact_ids.size, + :total => contacts.size, + :ids => '#' + unsaved_contact_ids.join(', #')) + end + end + + def render_contact_tabs(tabs) + if tabs.any? + render :partial => 'common/contact_tabs', :locals => {:tabs => tabs} + else + content_tag 'p', l(:label_no_data), :class => "nodata" + end + end + + def importer_link + project_contact_imports_path + end + + def importer_show_link(importer, project) + project_contact_import_path(:id => importer, :project_id => project) + end + + def importer_settings_link(importer, project) + settings_project_contact_import_path(:id => importer, :project => project) + end + + def importer_run_link(importer, project) + run_project_contact_import_path(:id => importer, :project_id => project, :format => 'js') + end + + def importer_link_to_object(contact) + link_to "#{contact.first_name} #{contact.last_name}", contact_path(contact) + end + + def _project_contacts_path(project, *args) + if project + project_contacts_path(project, *args) + else + contacts_path(*args) + end + end + def deals_link_to_remove_fields(name, f, options={}) + f.hidden_field(:_destroy) + link_to_function(name, "remove_order_fields(this); tooglePriceField()", options) + end + +end diff --git a/plugins/redmine_contacts/app/helpers/contacts_money_helper.rb b/plugins/redmine_contacts/app/helpers/contacts_money_helper.rb new file mode 100644 index 0000000..ac073c7 --- /dev/null +++ b/plugins/redmine_contacts/app/helpers/contacts_money_helper.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module ContactsMoneyHelper +# Will be depricated +end diff --git a/plugins/redmine_contacts/app/helpers/crm_queries_helper.rb b/plugins/redmine_contacts/app/helpers/crm_queries_helper.rb new file mode 100644 index 0000000..93e59f9 --- /dev/null +++ b/plugins/redmine_contacts/app/helpers/crm_queries_helper.rb @@ -0,0 +1,94 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module CrmQueriesHelper + + def retrieve_crm_query(object_type) + query_class = Object.const_get("#{object_type.camelcase}Query") + if !params[:query_id].blank? + cond = "project_id IS NULL" + cond << " OR project_id = #{@project.id}" if @project + @query = query_class.where(cond).find(params[:query_id]) + raise ::Unauthorized unless @query.visible? + @query.project = @project + session["#{object_type}_query".to_sym] = {:id => @query.id, :project_id => @query.project_id} + sort_clear + elsif api_request? || params[:set_filter] || session["#{object_type}_query".to_sym].nil? || session["#{object_type}_query".to_sym][:project_id] != (@project ? @project.id : nil) + # Give it a name, required to be valid + @query = query_class.new(:name => "_") + @query.project = @project + @query.build_from_params(params) + session["#{object_type}_query".to_sym] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names} + else + # retrieve from session + @query = query_class.find(session["#{object_type}_query".to_sym][:id]) if session["#{object_type}_query".to_sym][:id] + @query ||= query_class.new(:name => "_", :filters => session["#{object_type}_query".to_sym][:filters], :group_by => session["#{object_type}_query".to_sym][:group_by], :column_names => session["#{object_type}_query".to_sym][:column_names]) + @query.project = @project + end + end + + + def retrieve_crm_calendar(options = {}) + if params[:year] and params[:year].to_i > 1900 + @year = params[:year].to_i + if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13 + @month = params[:month].to_i + end + end + @year ||= Date.today.year + @month ||= Date.today.month + + @calendar = RedmineContacts::Helpers::CrmCalendar.new(Date.civil(@year, @month, 1), options) + end + + def sidebar_crm_queries(query_class) + unless @sidebar_queries + @sidebar_queries = query_class.visible. + where(@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id]). + order("#{query_class.table_name}.name ASC") + end + @sidebar_queries + end + + def crm_query_links(title, queries, object_type) + # links to #index on contacts/show + return '' unless queries.any? + url_params = controller_name == "#{object_type}s" ? {:controller => "#{object_type}s", :action => 'index', :project_id => @project} : params + content_tag('h3', title) + "\n" + + content_tag('ul', + queries.collect {|query| + css = 'query' + css << ' selected' if query == @query + content_tag('li', link_to(query.name, url_params.merge(:query_id => query), :class => css)) + }.join("\n").html_safe, + :class => 'queries' + ) + "\n" + end + + def render_sidebar_crm_queries(object_type) + query_class = Object.const_get("#{object_type.camelcase}Query") + out = ''.html_safe + out << crm_query_links(l(:label_my_queries), sidebar_crm_queries(query_class).select(&:is_private?), object_type) + out << crm_query_links(l(:label_query_plural), sidebar_crm_queries(query_class).reject(&:is_private?), object_type) + out + end + +end diff --git a/plugins/redmine_contacts/app/helpers/deals_helper.rb b/plugins/redmine_contacts/app/helpers/deals_helper.rb new file mode 100644 index 0000000..2ba997a --- /dev/null +++ b/plugins/redmine_contacts/app/helpers/deals_helper.rb @@ -0,0 +1,185 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module DealsHelper + include ContactsHelper + def collection_for_status_select + deal_statuses.collect{|s| [s.name, s.id.to_s]} + end + + def deal_status_options_for_select(select="") + options_for_select(collection_for_status_select, select) + end + + def deal_statuses + (!@project.blank? ? @project.deal_statuses : DealStatus.order("#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position")) || [] + end + + def deal_status_url(status_id, options={}) + {:controller => 'deals', + :action => 'index', + :set_filter => 1, + :project_id => @project, + :fields => [:status_id], + :values => {:status_id => [status_id]}, + :operators => {:status_id => '='}}.merge(options) + end + + def pipeline_status_tag(deal_status, count, index) + total = @processor.scope.count + width ||= 20 if deal_status.is_won? + width ||= 40 if deal_status.is_lost? + width ||= (100 - 20) * (count.to_f / total.to_f) + 20 + width_style = index == 0 ? "" : "width: #{width}%" + status_tag = content_tag(:span, deal_status.name) + content_tag(:span, status_tag, :class => "tag-label-color", :style => "background-color:#{deal_status.color_name};color:white; #{width_style}") + end + + def remove_contractor_link(contact) + link_to(image_tag('delete.png'), + {:controller => "deal_contacts", :action => 'delete', :project_id => @project, :deal_id => @deal, :contact_id => contact}, + :remote => true, + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => "delete", :title => l(:button_delete)) if User.current.allowed_to?(:edit_deals, @project) + end + + def link_to_deal(deal) + link_to deal.name, deal_path(deal) + end + + def deal_list_styles_for_select + [[l(:label_crm_list_excerpt), "list_excerpt"], + [l(:label_crm_list_list), "list"], + [l(:label_crm_list_board), "list_board"], + [l(:label_calendar), "crm_calendars/crm_calendar"], + [l(:label_crm_pipeline), "list_pipeline"]] + end + + def deals_list_style + list_styles = deal_list_styles_for_select.map(&:last) + if params[:deals_list_style].blank? + list_style = list_styles.include?(session[:deals_list_style]) ? session[:deals_list_style] : RedmineContacts.default_list_style.gsub("list_cards", "list_board") + else + list_style = list_styles.include?(params[:deals_list_style]) ? params[:deals_list_style] : RedmineContacts.default_list_style.gsub("list_cards", "list_board") + end + session[:deals_list_style] = list_style + end + + def retrieve_deals_query + if params[:status_id] || !params[:period].blank? || !params[:category_id].blank? || !params[:assigned_to_id].blank? + session[:deals_query] = {:project_id => (@project ? @project.id : nil), + :status_id => params[:status_id], + :category_id => params[:category_id], + :period => params[:period], + :assigned_to_id => params[:assigned_to_id]} + else + if api_request? || params[:set_filter] || session[:deals_query].nil? || session[:deals_query][:project_id] != (@project ? @project.id : nil) + session[:deals_query] = {} + else + params.merge!(session[:deals_query]) + end + end + end + + def pipeline_prices(scope) + prices_collection_by_currency(scope.group_by(&:currency).map{|k,v| [k, v.inject(0) { |sum, x| sum + x.price.to_f } ] }).join(' / ').html_safe + end + + def deals_to_csv(deals) + return "" unless User.current.allowed_to?(:export_contacts, @project, :global => true) + decimal_separator = l(:general_csv_decimal_separator) + encoding = 'utf-8' + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + # csv header fields + headers = [ "#", + l(:field_name, :locale => :en), + l(:field_background, :locale => :en), + l(:field_currency, :locale => :en), + l(:field_price, :locale => :en), + l(:label_crm_probability, :locale => :en), + l(:label_crm_expected_revenue, :locale => :en), + l(:field_due_date, :locale => :en), + l(:field_author, :locale => :en), + l(:field_assigned_to, :locale => :en), + l(:field_status, :locale => :en), + l(:field_contact, :locale => :en), + l(:field_category, :locale => :en), + l(:field_created_on, :locale => :en), + l(:field_updated_on, :locale => :en) + ] + + custom_fields = DealCustomField.order(:name) + custom_fields.each {|f| headers << f.name} + csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + # csv lines + deals.each do |deal| + fields = [deal.id, + deal.name, + deal.background, + deal.currency, + deal.price, + deal.probability, + deal.expected_revenue, + format_date(deal.due_date), + deal.author, + deal.assigned_to, + deal.status, + deal.contact, + deal.category, + format_date(deal.created_on), + format_date(deal.updated_on) + ] + deal.custom_field_values.sort_by{|v| v.custom_field.name}.each {|custom_value| fields << RedmineContacts::CSVUtils.csv_custom_value(custom_value) } + csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + end + end + export + end + + def importer_link + project_deal_imports_path + end + + def importer_show_link(importer, project) + project_deal_import_path(:id => importer, :project_id => project) + end + + def importer_settings_link(importer, project) + settings_project_deal_import_path(:id => importer, :project => project) + end + + def importer_run_link(importer, project) + run_project_deal_import_path(:id => importer, :project_id => project, :format => 'js') + end + + def importer_link_to_object(deal) + link_to deal.name, deal_path(deal) + end + + def _project_deals_path(project, *args) + if project + project_deals_path(project, *args) + else + deals_path(*args) + end + end +end diff --git a/plugins/redmine_contacts/app/helpers/notes_helper.rb b/plugins/redmine_contacts/app/helpers/notes_helper.rb new file mode 100644 index 0000000..b299155 --- /dev/null +++ b/plugins/redmine_contacts/app/helpers/notes_helper.rb @@ -0,0 +1,132 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module NotesHelper + include ContactsHelper + + def collection_for_note_types_select + note_types = [[l(:label_crm_note), '']] + [:label_crm_note_type_email, :label_crm_note_type_call, :label_crm_note_type_meeting].each_with_index.collect{|type, i| [l(type), i]} + context = {:note_types => note_types} + call_hook(:helper_notes_note_type_label, context) + context[:note_types] + end + + def authoring_note(created, author, options={}) + return "#{l(options[:label] || :label_crm_added_by)} #{link_to_user(author).to_s}".html_safe if created.blank? + if RedmineContacts.settings[:note_authoring_time] + ('' + l(options[:label] || :label_crm_added_by) + ' ' + + link_to_user(author).to_s + ', ' + + format_time(created).to_s + '').html_safe + else + authoring(created, author, options={}) + end + end + + def add_note_url(note_source, project=nil) + {:controller => 'notes', :action => 'create', :source_id => note_source, :source_type => note_source.class.name, :project_id => project} + end + + def contacts_thumbnails(obj, options={}) + return false if !obj || !obj.respond_to?(:attachments) + options[:size] = options[:size].to_s || "100" + size = options[:size] + options[:size] = options[:size] + "x" + options[:size] + # options[:max_width] = size + # options[:max_heght] = size + max_file_size = options[:max_file_size] || 300.kilobytes + options[:class] = "thumbnail" + + s = "" + # TODO: Regexp does not work + images = obj.attachments.select{|att| att.thumbnailable?} + images = images.select{|att| att.filename.match(options[:regexp])} if options[:regexp] + images.each do |att_file| + attachment_url = url_for :controller => 'attachments', :action => 'download', :id => att_file, :filename => att_file.filename + contacts_thumbnail_url = url_for(:controller => 'attachments', + :action => 'contacts_thumbnail', + :id => att_file, + :size => size) + + image_url = Redmine::Thumbnail.convert_available? ? contacts_thumbnail_url : attachment_url + s << link_to(image_tag(image_url, options), attachment_url, {:title => att_file.filename}) if (att_file.filesize < max_file_size || Redmine::Thumbnail.convert_available?) + end + s.html_safe + end + + def auto_contacts_thumbnails(obj) + s = "" + max_file_size = Setting.plugin_redmine_contacts[:max_contacts_thumbnail_file_size].to_i.kilobytes if !Setting.plugin_redmine_contacts[:max_contacts_thumbnail_file_size].blank? + s << contacts_thumbnails(obj, {:size => 100, :max_file_size => max_file_size}) if Setting.plugin_redmine_contacts[:auto_contacts_thumbnails] + s = content_tag(:p, s.html_safe, :class => "thumbnail") if !s.blank? + s.html_safe + end + + def note_content(note) + s = '' + if note.content.length > Note.cut_length + if ActiveRecord::VERSION::MAJOR >= 4 + s << truncate(note.content, :length => Note.cut_length) { link_to "#{l(:label_crm_note_read_more)}", note_path(:id => note, :project_id => @project) } + else + s << textilizable(truncate(note.content, :length => Note.cut_length, + :omission => "... \"#{l(:label_crm_note_read_more)}\":#{url_for(:controller => 'notes', + :action => 'show', + :project_id => @project, + :id => note)}")) + end + else + s << textilizable(note, :content) + end + s.html_safe + end + + def notes_to_csv(notes) + decimal_separator = l(:general_csv_decimal_separator) + encoding = l(:general_csv_encoding) + export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv| + # csv header fields + headers = [ "#", + l(:field_type, :locale => :en), + l(:label_date, :locale => :en), + l(:field_author, :locale => :en), + l(:field_content, :locale => :en) + ] + # Export project custom fields if project is given + # otherwise export custom fields marked as "For all projects" + custom_fields = NoteCustomField.order(:name) + custom_fields.each {|f| headers << f.name} + # Description in the last column + csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + # csv lines + notes.each do |note| + fields = [note.id, + note.type_id, + format_time(note.created_on), + note.author.name, + note.content + ] + custom_fields.each {|f| fields << RedmineContacts::CSVUtils.csv_custom_value(note.custom_value_for(f)) } + csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } + end + end + export + end + +end diff --git a/plugins/redmine_contacts/app/models/address.rb b/plugins/redmine_contacts/app/models/address.rb new file mode 100644 index 0000000..8484ed3 --- /dev/null +++ b/plugins/redmine_contacts/app/models/address.rb @@ -0,0 +1,81 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class Address < ActiveRecord::Base + include Redmine::SafeAttributes + + attr_reader :country + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'street1', 'street2', 'region', 'city', 'country_code', 'postcode', + 'full_address', 'address_type', 'addressable' + + belongs_to :addressable, :polymorphic => true + + scope :business, lambda { where(:address_type => 'business') } + scope :billing, lambda { where(:address_type => 'billing') } + scope :shipping, lambda { where(:address_type => 'shipping') } + + before_save :populate_full_address + + def country + @country ||= l(:label_crm_countries)[country_code.to_sym].to_s unless country_code.blank? + end + + def blank? + %w(street1 street2 city region postcode country_code).all? { |attr| self.send(attr).blank? } + end + + #---------------------------------------------------------------------------- + # Ensure blank address records don't get created. If we have a new record and + # address is empty then return true otherwise return false so that _destroy + # is processed (if applicable) and the record is removed. + # Intended to be called as follows: + # accepts_nested_attributes_for :business_address, :allow_destroy => true, :reject_if => proc {|attributes| Address.reject_address(attributes)} + def self.reject_address(attributes) + exists = attributes['id'].present? + empty = %w(street1 street2 city region postcode country_code full_address).map { |name| attributes[name].blank? }.all? + attributes[:_destroy] = 1 if exists && empty + !exists && empty + end + + def to_s + %w(street1 street2 city postcode region country).map { |attr| send(attr) }.select { |a| !a.blank? }.join(', ') + end + + def post_address + address_template = ContactsSetting.post_address_format + address_template = address_template.gsub('%street1%', street1.to_s) + address_template = address_template.gsub('%street2%', street2.to_s) + address_template = address_template.gsub('%city%', city.to_s) + address_template = address_template.gsub('%town%', city.to_s) + address_template = address_template.gsub('%postcode%', postcode.to_s) + address_template = address_template.gsub('%zip%', postcode.to_s) + address_template = address_template.gsub('%region%', region.to_s) + address_template = address_template.gsub('%state%', region.to_s) + address_template = address_template.gsub('%country%', country.to_s) + address_template.gsub(/\r\n?/, "\n").gsub(/^$\n/, '').gsub(/^[, ]+|[, ]+$|[,]{2,}/,'').gsub(/\s{2,}/, ' ').strip + end + + private + + def populate_full_address + self.full_address = self.to_s + end +end diff --git a/plugins/redmine_contacts/app/models/contact.rb b/plugins/redmine_contacts/app/models/contact.rb new file mode 100755 index 0000000..dae5d07 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact.rb @@ -0,0 +1,519 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class Contact < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + CONTACT_FORMATS = { + :firstname_lastname => { + :string => '#{first_name} #{last_name}', + :order => %w(first_name middle_name last_name id), + :setting_order => 1 + }, + :lastname_firstname_middlename => { + :string => '#{last_name} #{first_name} #{middle_name}', + :order => %w(last_name first_name middle_name id), + :setting_order => 1 + }, + :firstname_middlename_lastname => { + :string => '#{first_name} #{middle_name} #{last_name}', + :order => %w(first_name middle_name last_name id), + :setting_order => 1 + }, + :firstname_lastinitial => { + :string => '#{first_name} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?} #{last_name.to_s.chars.first + \'.\' unless last_name.blank?}', + :order => %w(first_name middle_name last_name id), + :setting_order => 2 + }, + :firstinitial_lastname => { + :string => '#{first_name.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?} #{last_name}', + :order => %w(first_name middle_name last_name id), + :setting_order => 2 + }, + :lastname_firstinitial => { + :string => '#{last_name} #{first_name.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{middle_name.to_s.chars.first + \'.\' unless middle_name.blank?}', + :order => %w(last_name first_name middle_name id), + :setting_order => 2 + }, + :firstname => { + :string => '#{first_name}', + :order => %w(first_name middle_name id), + :setting_order => 3 + }, + :lastname_firstname => { + :string => '#{last_name} #{first_name}', + :order => %w(last_name first_name middle_name id), + :setting_order => 4 + }, + :lastname_coma_firstname => { + :string => '#{last_name.to_s + \',\' unless last_name.blank?} #{first_name}', + :order => %w(last_name first_name middle_name id), + :setting_order => 5 + }, + :lastname => { + :string => '#{last_name}', + :order => %w(last_name id), + :setting_order => 6 + } + } + + VISIBILITY_PROJECT = 0 + VISIBILITY_PUBLIC = 1 + VISIBILITY_PRIVATE = 2 + + delegate :street1, :street2, :city, :country, :country_code, :postcode, :region, :post_address, :to => :address, :allow_nil => true + + has_many :notes, :as => :source, :class_name => 'ContactNote', :dependent => :delete_all + has_many :addresses, :dependent => :destroy, :as => :addressable, :class_name => 'Address' + belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + + if ActiveRecord::VERSION::MAJOR >= 4 + has_one :avatar, lambda { where("#{Attachment.table_name}.description = 'avatar'") }, :class_name => 'Attachment', :as => :container, :dependent => :destroy + has_one :address, lambda { where(:address_type => 'business') }, :dependent => :destroy, :as => :addressable, :class_name => 'Address' + has_many :deals, lambda { order("#{Deal.table_name}.status_id") } + has_and_belongs_to_many :related_deals, lambda { order("#{Deal.table_name}.status_id") }, :uniq => true, :class_name => 'Deal' + has_and_belongs_to_many :projects, :uniq => true + has_and_belongs_to_many :issues, lambda { order("#{Issue.table_name}.due_date") }, :uniq => true + else + has_one :avatar, :conditions => "#{Attachment.table_name}.description = 'avatar'", :class_name => 'Attachment', :as => :container, :dependent => :destroy + has_one :address, :conditions => { :address_type => 'business' }, :dependent => :destroy, :as => :addressable, :class_name => 'Address' + has_many :deals, :order => "#{Deal.table_name}.status_id" + has_and_belongs_to_many :related_deals, :order => "#{Deal.table_name}.status_id", :class_name => 'Deal', :uniq => true + has_and_belongs_to_many :projects, :uniq => true + has_and_belongs_to_many :issues, :order => "#{Issue.table_name}.due_date", :uniq => true + end + + attr_accessor :phones + attr_accessor :emails + acts_as_customizable + acts_as_viewable + rcrm_acts_as_taggable + acts_as_watchable + acts_as_attachable :view_permission => :view_contacts, + :delete_permission => :edit_contacts + + acts_as_event :datetime => :created_on, + :url => lambda { |o| { :controller => 'contacts', :action => 'show', :id => o } }, + :type => 'icon icon-contact', + :title => lambda { |o| o.name }, + :description => lambda { |o| [o.info, o.company, o.email, o.address, o.background].join(' ') } + + if ActiveRecord::VERSION::MAJOR >= 4 + acts_as_activity_provider :type => 'contacts', + :permission => :view_contacts, + :author_key => :author_id, + :scope => joins(:projects) + + acts_as_searchable :columns => ["#{table_name}.first_name", + "#{table_name}.middle_name", + "#{table_name}.last_name", + "#{table_name}.company", + "#{table_name}.email", + "#{Address.table_name}.full_address", + "#{table_name}.background", + "#{ContactNote.table_name}.content"], + :project_key => "#{Project.table_name}.id", + :scope => includes([:address, :notes]), + :date_column => "created_on" + else + acts_as_activity_provider :type => 'contacts', + :permission => :view_contacts, + :author_key => :author_id, + :find_options => { :include => :projects } + + acts_as_searchable :columns => ["#{table_name}.first_name", + "#{table_name}.middle_name", + "#{table_name}.last_name", + "#{table_name}.company", + "#{table_name}.email", + "#{Address.table_name}.full_address", + "#{table_name}.background", + "#{ContactNote.table_name}.content"], + :project_key => "#{Project.table_name}.id", + :include => [:projects, :address, :notes], + # sort by id so that limited eager loading doesn't break with postgresql + :order_column => "#{table_name}.id" + end + + accepts_nested_attributes_for :address, :allow_destroy => true, :update_only => true, :reject_if => proc { |attributes| Address.reject_address(attributes) } + + scope :visible, lambda { |*args| eager_load(:projects).where(Contact.visible_condition(args.shift || User.current, *args)) } + scope :deletable, lambda { |*args| eager_load(:projects).where(Contact.deletable_condition(args.shift || User.current, *args)).readonly(false) } + scope :editable, lambda { |*args| eager_load(:projects).where(Contact.editable_condition(args.shift || User.current, *args)).readonly(false) } + scope :by_project, lambda { |prj| joins(:projects).where("#{Project.table_name}.id = ?", prj) unless prj.blank? } + scope :like_by, lambda { |field, search| {:conditions => ["LOWER(#{Contact.table_name}.#{field}) LIKE ?", search.downcase + "%"] }} + scope :companies, lambda { where(:is_company => true) } + scope :people, lambda { where(:is_company => false) } + scope :order_by_name, lambda { order(Contact.fields_for_order_statement) } + scope :order_by_creation, lambda { order("#{Contact.table_name}.created_on DESC") } + + scope :by_full_name, lambda { |search| where("LOWER(CONCAT(#{Contact.table_name}.first_name,' ',#{Contact.table_name}.last_name)) = ? ", search.downcase) } + scope :by_name, lambda { |search| where("(LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.middle_name) LIKE LOWER(:p))", + { :p => '%' + search.downcase + '%' }) } + + scope :live_search, lambda {|search| where("(LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.middle_name) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.company) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.email) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.phone) LIKE LOWER(:p) OR + LOWER(#{Contact.table_name}.job_title) LIKE LOWER(:p))", + { :p => '%' + search.downcase + '%' }) } + + validates_presence_of :first_name, :project + validate :emails_format + # validates_uniqueness_of :first_name, :scope => [:last_name, :company, :email] + + before_validation :strip_email + after_create :send_notification + before_save :update_company_contacts + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'is_company', + 'first_name', + 'last_name', + 'middle_name', + 'company', + 'website', + 'skype_name', + 'birthday', + 'job_title', + 'background', + 'author_id', + 'assigned_to_id', + 'phone', + 'email', + 'tag_list', + 'project_ids', + 'visibility', + 'custom_field_values', + 'custom_fields', + 'watcher_user_ids', + 'address_attributes' + + def self.visible_condition(user, options = {}) + user.reload + user_ids = [user.id] + user.groups.map(&:id) + + projects_allowed_to_view_contacts = Project.where(Project.allowed_to_condition(user, :view_contacts)).pluck(:id) + allowed_to_view_condition = projects_allowed_to_view_contacts.empty? ? "(1=0)" : "#{Project.table_name}.id IN (#{projects_allowed_to_view_contacts.join(',')})" + projects_allowed_to_view_private = Project.where(Project.allowed_to_condition(user, :view_private_contacts)).pluck(:id) + allowed_to_view_private_condition = projects_allowed_to_view_private.empty? ? "(1=0)" : "#{Project.table_name}.id IN (#{projects_allowed_to_view_private.join(',')})" + + cond = "(#{Project.table_name}.id <> -1 ) AND (" + if user.admin? + cond << "(#{table_name}.visibility = 1) OR (#{allowed_to_view_condition}) " + else + cond << " (#{table_name}.visibility = 1) OR" if user.allowed_to_globally?(:view_contacts, {}) + cond << " (#{allowed_to_view_condition} AND #{table_name}.visibility <> 2) " + + if user.logged? + cond << " OR (#{allowed_to_view_private_condition} " + + " OR (#{allowed_to_view_condition} " + + " AND (#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}) )))" + end + end + cond << ')' + end + + def self.editable_condition(user, options = {}) + visible_condition(user, options) + " AND (#{Project.allowed_to_condition(user, :edit_contacts)})" + end + + def self.deletable_condition(user, options = {}) + visible_condition(user, options) + " AND (#{Project.allowed_to_condition(user, :delete_contacts)})" + end + def all_deals + @all_deals ||= (deals + related_deals).uniq.sort! { |x, y| x.status_id <=> y.status_id } + end + + def all_visible_deals(usr = User.current) + @all_deals ||= (deals.visible(usr) + related_deals.visible(usr)).uniq.sort! { |x, y| x.status_id <=> y.status_id } + if is_company? + company_contacts.each { |contact| @all_deals += contact.deals } + end + @all_deals.uniq.sort! { |x, y| x.status_id <=> y.status_id } + end + + def self.available_tags(options = {}) + limit = options[:limit] + + scope = RedmineCrm::Tag.where({}) + scope = scope.where("#{Project.table_name}.id = ?", options[:project]) if options[:project] + scope = scope.where(Contact.visible_condition(options[:user] || User.current)) + scope = scope.where("LOWER(#{RedmineCrm::Tag.table_name}.name) LIKE ?", "%#{options[:name_like].downcase}%") if options[:name_like] + + joins = [] + joins << "JOIN #{RedmineCrm::Tagging.table_name} ON #{RedmineCrm::Tagging.table_name}.tag_id = #{RedmineCrm::Tag.table_name}.id " + joins << "JOIN #{Contact.table_name} ON #{Contact.table_name}.id = #{RedmineCrm::Tagging.table_name}.taggable_id AND #{RedmineCrm::Tagging.table_name}.taggable_type = '#{Contact.name}' " + joins << Contact.projects_joins + + scope = scope.select("#{RedmineCrm::Tag.table_name}.*, COUNT(DISTINCT #{RedmineCrm::Tagging.table_name}.taggable_id) AS count") + scope = scope.joins(joins.flatten) + scope = scope.group("#{RedmineCrm::Tag.table_name}.id, #{RedmineCrm::Tag.table_name}.name HAVING COUNT(*) > 0") + scope = scope.limit(limit) if limit + scope = scope.order("#{RedmineCrm::Tag.table_name}.name") + scope + end + + def duplicates(limit = 10) + scope = Contact.where({}) + + cond = "((1=1) " + cond << "AND LOWER(#{Contact.table_name}.first_name) LIKE LOWER('#{first_name.strip}') " unless first_name.blank? + cond << "AND (LOWER(#{Contact.table_name}.middle_name) LIKE LOWER('#{middle_name.strip}') OR middle_name LIKE '') " unless middle_name.blank? + cond << "AND LOWER(#{Contact.table_name}.last_name) LIKE LOWER('#{last_name.strip}') " unless last_name.blank? + cond << " OR LOWER(#{Contact.table_name}.email) LIKE LOWER('#{primary_email.strip}') " unless primary_email.blank? + cond << ")" + cond << " AND #{Contact.table_name}.id <> #{id}" unless new_record? + scope = scope.where(cond) + @duplicates ||= (first_name.blank? && last_name.blank? && middle_name.blank?) ? [] : scope.visible.limit(limit) + end + + def company_contacts + @contacts ||= Contact.order_by_name.includes(:avatar).where(["#{Contact.table_name}.is_company = ? AND #{Contact.table_name}.company = ? AND #{Contact.table_name}.id <> ?", false, first_name, id]) + end + + alias_method :employees, :company_contacts + + def redmine_user + if ActiveRecord::VERSION::MAJOR >= 4 + @redmine_user ||= User.joins(:email_address).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", emails).first unless email.blank? + else + @redmine_user ||= User.where(:mail => emails).first unless email.blank? + end + end + + def contact_company + @contact_company ||= Contact.where(:first_name => company, :is_company => true). + where("#{Contact.table_name}.id <> #{id.to_i}").first unless company.blank? + end + + def notes_attachments + @contact_attachments ||= Attachment.where(:container_type => 'Note', :container_id => notes.map(&:id)).order(:created_on) + end + + # usr for mailer + def visible?(usr = nil) + usr ||= User.current + if is_public? + usr.allowed_to_globally?(:view_contacts, {}) + else + allowed_to?(usr || User.current, :view_contacts) + end + end + + def editable?(usr = nil) + allowed_to?(usr || User.current, :edit_contacts) + end + + def deletable?(usr = nil) + allowed_to?(usr || User.current, :delete_contacts) + end + + def allowed_to?(user, action, options = {}) + if is_private? + (projects.map { |p| user.allowed_to?(action, p) }.compact.any? && (author == user || user.is_or_belongs_to?(assigned_to))) || + (projects.map { |p| user.allowed_to?(:view_private_contacts, p) }.compact.any? && projects.map { |p| user.allowed_to?(action, p) }.compact.any?) + else + projects.map { |p| user.allowed_to?(action, p) }.compact.any? + end + end + + def is_public? + visibility == VISIBILITY_PUBLIC + end + + def is_private? + visibility == VISIBILITY_PRIVATE + end + + def send_mail_allowed?(usr = nil) + usr ||= User.current + @send_mail_allowed ||= 0 < projects.visible(usr).where(Project.allowed_to_condition(usr, :send_contacts_mail)).count + end + + def self.projects_joins + joins = [] + joins << ["JOIN contacts_projects ON contacts_projects.contact_id = #{table_name}.id"] + joins << ["JOIN #{Project.table_name} ON contacts_projects.project_id = #{Project.table_name}.id"] + end + + def project(current_project=nil) + return @project if @project + visible_projects = Project.visible.where(:id => projects.pluck(:id)) + if current_project && visible_projects.include?(current_project) + @project = current_project + else + @project = visible_projects.where(Project.allowed_to_condition(User.current, :view_contacts)).first + end + + @project ||= projects.first + end + + def project=(project) + projects << project + end + + def self.find_by_emails(emails) + cond = '(1 = 0)' + emails = emails.map(&:downcase) + emails.each do |mail| + cond << " OR (LOWER(#{Contact.table_name}.email) LIKE LOWER('%#{mail.gsub("'", "").gsub("\"", "")}%'))" + end + contacts = Contact.where(cond) + contacts.select { |c| (c.emails.map(&:downcase) & emails).any? } + end + + def self.name_formatter(formatter = nil) + CONTACT_FORMATS[formatter || ContactsSetting.contact_name_format.to_sym] + end + + # Returns an array of fields names than can be used to make an order statement for users + # according to how user names are displayed + # Examples: + # + # Contact.fields_for_order_statement => ['contacts.first_name', 'contacts.first_name', 'contacts.id'] + # Contact.fields_for_order_statement('customers') => ['customers.last_name', 'customers.id'] + def self.fields_for_order_statement(table = nil) + table ||= table_name + name_formatter[:order].map { |field| "#{table}.#{field}" } + end + + # Return contacts's full name for display + def name(formatter = nil) + unless is_company? + f = self.class.name_formatter(formatter) + if formatter + eval('"' + f[:string] + '"') + else + @name ||= eval('"' + f[:string] + '"') + end + else + first_name + end + end + + def name_with_company + return name if company.blank? + [name, ' ', '(', company, ')'].join + end + + def info + job_title + end + + def phones + @phones || phone ? phone.split(/, */) : [] + end + + def emails + @emails || email ? email.split(/, */).map { |m| m.strip } : [] + end + + def primary_email + emails.first + end + + def age + return nil if birthday.blank? + now = Time.now + # how many years? + # has their birthday occured this year yet? + # subtract 1 if so, 0 if not + now.year - birthday.year - (birthday.to_time.change(:year => now.year) > now ? 1 : 0) + end + + def website_address + website.match("^https?://") ? website : website.gsub(/^/, "http://") unless website.blank? + end + + def to_s + name + end + + def notified_users + notified = [] + # Author and assignee are always notified unless they have been + # locked or don't want to be notified + notified << author if author + if assigned_to + notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) + end + + notified += project.notified_users + + if !is_company && !contact_company.blank? + notified += contact_company.notified_users + end + + notified = notified.select { |u| u.active? } + notified.uniq! + # Remove users that can not view the issue + notified.reject! { |user| !visible?(user) } + notified + end + + # Returns the mail adresses of users that should be notified + def recipients + notified_users.collect(&:mail) + end + + def all_watcher_recepients + notified = watcher_recipients + if !is_company && !contact_company.blank? + notified += contact_company.watcher_recipients + end + notified + end + + private + + def assign_phone + if @phones + self.phone = @phones.uniq.map { |s| s.strip.delete(',').squeeze(' ') }.join(', ') + end + end + + def send_notification + Mailer.crm_contact_add(self).deliver if Setting.notified_events.include?('crm_contact_added') + end + + def strip_email + return unless email + self.email = email.tr(' ', '') + end + + def emails_format + return unless email + validate_result = email.split(',').all? { |email| email.match(/\A[^@]+@[^@]+\z/) } + errors.add(:email, I18n.t(:text_crm_string_incorrect_format)) unless validate_result + end + + def update_company_contacts + return unless is_company + return unless first_name_changed? + Contact.where(["#{Contact.table_name}.is_company = ? AND #{Contact.table_name}.company = ?", false, first_name_was]). + update_all(:company => first_name) + end +end diff --git a/plugins/redmine_contacts/app/models/contact_custom_field.rb b/plugins/redmine_contacts/app/models/contact_custom_field.rb new file mode 100644 index 0000000..6db0f52 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact_custom_field.rb @@ -0,0 +1,26 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactCustomField < CustomField + unloadable + + def type_name + :label_contact_plural + end +end diff --git a/plugins/redmine_contacts/app/models/contact_import.rb b/plugins/redmine_contacts/app/models/contact_import.rb new file mode 100644 index 0000000..8781974 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact_import.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactImport + extend ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + include CSVImportable + + attr_accessor :file, :project, :tag_list, :quotes_type + + def klass + Contact + end + + def build_from_fcsv_row(row) + ret = Hash[row.to_hash.collect { |k, v| [k.underscore.tr(' ', '_'), force_utf8(v)] if k }].delete_if { |k, _v| !klass.column_names.include?(k) } + ret[:birthday] = row['birthday'].to_date if row['birthday'] + ActiveRecord::VERSION::MAJOR >= 4 ? ret[:tag_list] = [row['tags'], tag_list] : ret[:tag_list] = [row['tags'], tag_list].join(',') + ret[:assigned_to_id] = User.find_by_login(row['responsible']).try(:id) unless row['responsible'].blank? + unless row['address'].blank? && row['city'].blank? && row['street1'].blank? && row['street2'].blank? && row['region'].blank? && row['postcode'].blank? && row['country_code'].blank? + ret[:address_attributes] = {} + ret[:address_attributes][:street1] = row['address'] unless row['address'].blank? + ret[:address_attributes][:street2] = row['street2'] unless row['street2'].blank? + ret[:address_attributes][:city] = row['city'] unless row['city'].blank? + ret[:address_attributes][:postcode] = row['postcode'] unless row['postcode'].blank? + ret[:address_attributes][:postcode] = row['zip'] unless row['zip'].blank? + ret[:address_attributes][:region] = row['region'] unless row['region'].blank? + ret[:address_attributes][:country_code] = row['country code'] unless row['country code'].blank? + ret[:address_attributes][:country] = row['country'] unless row['country'].blank? + ret[:address_attributes][:region] = row['state'] unless row['state'].blank? && !row["region"].blank? + end + ret + end +end diff --git a/plugins/redmine_contacts/app/models/contact_kernel_import.rb b/plugins/redmine_contacts/app/models/contact_kernel_import.rb new file mode 100644 index 0000000..606b5ee --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact_kernel_import.rb @@ -0,0 +1,130 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactKernelImport < Import + + def klass + Contact + end + + def saved_objects + object_ids = saved_items.pluck(:obj_id) + Contact.where(:id => object_ids).order(:id) + end + + def project=(project) + settings['project'] = project.id + end + + def project + settings['project'] + end + + private + + def build_object(row, _item = nil) + contact = Contact.new + contact.project = Project.find(settings['project']) + contact.author = user + + attributes = {} + if is_company = row_value(row, 'is_company') + attributes['is_company'] = '1' if yes?(is_company) + end + if first_name = row_value(row, 'first_name') + attributes['first_name'] = first_name + end + if middle_name = row_value(row, 'middle_name') + attributes['middle_name'] = middle_name + end + if last_name = row_value(row, 'last_name') + attributes['last_name'] = last_name + end + if job_title = row_value(row, 'job_title') + attributes['job_title'] = job_title + end + if company = row_value(row, 'company') + attributes['company'] = company + end + if phone = row_value(row, 'phone') + attributes['phone'] = phone + end + if email = row_value(row, 'email') + attributes['email'] = email + end + + address_attributes = {} + if address_street = row_value(row, 'address_street') + address_attributes['street1'] = address_street + end + if address_country_code = row_value(row, 'address_country_code') + address_attributes['country_code'] = address_country_code + end + if address_zip = row_value(row, 'address_zip') + address_attributes['postcode'] = address_zip + end + if address_state = row_value(row, 'address_state') + address_attributes['region'] = address_state + end + if address_city = row_value(row, 'address_city') + address_attributes['city'] = address_city + end + attributes['address_attributes'] = address_attributes + + if skype_name = row_value(row, 'skype_name') + attributes['skype_name'] = skype_name + end + if website = row_value(row, 'website') + attributes['website'] = website + end + if birthday = row_value(row, 'birthday') + attributes['birthday'] = birthday + end + if tag_list = row_value(row, 'tag_list') + attributes['tag_list'] = tag_list + end + if background = row_value(row, 'background') + attributes['background'] = background + end + + attributes['custom_field_values'] = contact.custom_field_values.inject({}) do |h, v| + value = case v.custom_field.field_format + when 'date' + row_date(row, "cf_#{v.custom_field.id}") + when 'list' + row_value(row, "cf_#{v.custom_field.id}").try(:split, ',') + else + row_value(row, "cf_#{v.custom_field.id}") + end + if value + h[v.custom_field.id.to_s] = + if value.is_a?(Array) + value.map { |val| v.custom_field.value_from_keyword(val.strip, contact) }.compact.flatten + else + v.custom_field.value_from_keyword(value, contact) + end + end + h + end + + contact.send :safe_attributes=, attributes, user + contact + end + +end diff --git a/plugins/redmine_contacts/app/models/contact_note.rb b/plugins/redmine_contacts/app/models/contact_note.rb new file mode 100644 index 0000000..00076ed --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact_note.rb @@ -0,0 +1,50 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactNote < Note + unloadable + include Redmine::SafeAttributes + + belongs_to :contact, :foreign_key => :source_id + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'subject', 'type_id', 'content', 'source', 'author_id' + + if ActiveRecord::VERSION::MAJOR >= 4 + if ActiveRecord::Base.connection.table_exists?('notes') + acts_as_activity_provider :type => 'contacts', + :permission => :view_contacts, + :author_key => :author_id, + :scope => eager_load(:contact => :projects).where(:source_type => 'Contact') + end + else + acts_as_activity_provider :type => 'contacts', + :permission => :view_contacts, + :author_key => :author_id, + :find_options => { :include => [:contact => :projects], :conditions => { :source_type => 'Contact' } } + end + + scope :visible, + lambda { |*args| joins([:contact => :projects]). + where(Contact.visible_condition(args.shift || User.current, *args) + + " AND (#{ContactNote.table_name}.source_type = 'Contact')") } + + acts_as_attachable :view_permission => :view_contacts, + :delete_permission => :edit_contacts +end diff --git a/plugins/redmine_contacts/app/models/contact_query.rb b/plugins/redmine_contacts/app/models/contact_query.rb new file mode 100644 index 0000000..d112cb0 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contact_query.rb @@ -0,0 +1,240 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactQuery < Query + include CrmQuery + + class QueryMultipleValuesColumn < QueryColumn + def value_object(object) + value = super + value.respond_to?(:to_a) ? value.to_a : value + end + end + + self.queried_class = Contact + self.view_permission = :view_contacts if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? + + self.available_columns = [ + QueryColumn.new(:id, :sortable => "#{Contact.table_name}.id", :default_order => 'desc', :caption => '#'), + QueryColumn.new(:name, :sortable => lambda {Contact.fields_for_order_statement}, :caption => :field_contact_full_name), + QueryColumn.new(:first_name, :sortable => "#{Contact.table_name}.first_name"), + QueryColumn.new(:last_name, :sortable => "#{Contact.table_name}.last_name"), + QueryColumn.new(:middle_name, :sortable => "#{Contact.table_name}.middle_name", :caption => :field_contact_middle_name), + QueryColumn.new(:job_title, :sortable => "#{Contact.table_name}.job_title", :caption => :field_contact_job_title, :groupable => true), + QueryColumn.new(:company, :sortable => "#{Contact.table_name}.company", :groupable => "#{Contact.table_name}.company", :caption => :field_contact_company), + QueryColumn.new(:phones, :sortable => "#{Contact.table_name}.phone", :caption => :field_contact_phone), + QueryColumn.new(:emails, :sortable => "#{Contact.table_name}.email", :caption => :field_contact_email), + QueryColumn.new(:address, :sortable => "#{Address.table_name}.full_address", :caption => :label_crm_address), + QueryColumn.new(:street1, :sortable => "#{Address.table_name}.street1", :caption => :label_crm_street1), + QueryColumn.new(:street2, :sortable => "#{Address.table_name}.street2", :caption => :label_crm_street2), + QueryColumn.new(:city, :sortable => "#{Address.table_name}.city", :groupable => "#{Address.table_name}.city", :caption => :label_crm_city), + QueryColumn.new(:region, :sortable => "#{Address.table_name}.region", :caption => :label_crm_region), + QueryColumn.new(:postcode, :sortable => "#{Address.table_name}.postcode", :caption => :label_crm_postcode), + QueryColumn.new(:country, :sortable => "#{Address.table_name}.country_code", :groupable => "#{Address.table_name}.country_code", :caption => :label_crm_country), + QueryMultipleValuesColumn.new(:tags, :caption => :label_crm_tags_plural), + QueryColumn.new(:created_on, :sortable => "#{Contact.table_name}.created_on"), + QueryColumn.new(:updated_on, :sortable => "#{Contact.table_name}.updated_on"), + QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), + QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}) + ] + + + def initialize(attributes=nil, *args) + super attributes + self.filters ||= {} + end + + def initialize_available_filters + add_available_filter "ids", :type => :integer, :label => :label_contact if Redmine::VERSION.to_s >= '3.3' + add_available_filter "first_name", :type => :string, :order => 0 + add_available_filter "last_name", :type => :string, :order => 1 + add_available_filter "middle_name", :type => :string, :order => 2 + add_available_filter "job_title", :type => :string, :order => 3 + add_available_filter "company", :type => :string, :order => 4 + add_available_filter "phone", :type => :text, :order => 5 + add_available_filter "email", :type => :text, :order => 6 + add_available_filter "full_address", :type => :text, :order => 7, :name => l(:label_crm_address) + add_available_filter "street1", :type => :text, :order => 8, :name => l(:label_crm_street1) + add_available_filter "street2", :type => :text, :order => 8, :name => l(:label_crm_street2) + add_available_filter "city", :type => :text, :order => 8, :name => l(:label_crm_city) + add_available_filter "region", :type => :text, :order => 9, :name => l(:label_crm_region) + add_available_filter "postcode", :type => :text, :order => 10, :name => l(:label_crm_postcode) + add_available_filter "country", :type => :list_optional, :values => l(:label_crm_countries).map{|k, v| [v, k]}, :order => 11, :name => l(:label_crm_country) + add_available_filter "is_company", :type => :list, :values => [[l(:general_text_yes), ActiveRecord::Base.connection.quoted_true.gsub(/'/, '')], [l(:general_text_no), ActiveRecord::Base.connection.quoted_false.gsub(/'/, '')]], :order => 12 + add_available_filter "last_note", :type => :date_past, :order => 13 + add_available_filter "has_deals", :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 14, :name => l(:label_crm_has_deals) + add_available_filter "updated_on", :type => :date_past, :order => 20 + add_available_filter "created_on", :type => :date, :order => 21 + add_available_filter "tags", :type => :list, :values => Contact.available_tags(project.blank? ? {} : {:project => project.id}).collect{ |t| [t.name, t.name] }, :order => 12 + initialize_author_filter + initialize_assignee_filter + + add_available_filter("has_open_issues", + :type => :list_optional, :values => users_values, :label => :label_crm_has_open_issues + ) unless users_values.empty? + + add_custom_fields_filters(ContactCustomField.where(:is_filter => true)) + add_associations_custom_fields_filters :author, :assigned_to + end + + def available_columns + return @available_columns if @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += CustomField.where(:type => 'ContactCustomField').all.map {|cf| QueryCustomFieldColumn.new(cf) } + @available_columns + end + + def default_columns_names + @default_columns_names ||= [:id, :name, :job_title, :company, :phone, :email, :address] + end + + def sql_for_tags_field(field, operator, value) + compare = operator_for('tags').eql?('=') ? 'IN' : 'NOT IN' + ids_list = Contact.tagged_with(value).collect{|contact| contact.id }.push(0).join(',') + "( #{Contact.table_name}.id #{compare} (#{ids_list}) ) " + end + + def sql_for_project_field(field, operator, value) + '(' + sql_for_field(field, operator, value, Project.table_name, "id", false) + ')' + end + + def sql_for_country_field(field, operator, value) + if operator == '*' # Any group + contact_countries = l(:label_crm_countries).map{|k, v| k.to_s} + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" + contact_countries = l(:label_crm_countries).map{|k, v| k.to_s} + operator = '!' # Override the operator since we want to find by assigned_to + else + contact_countries = value + end + '(' + sql_for_field("address_id", operator, contact_countries, Address.table_name, "country_code", false) + ')' + end + + def sql_for_city_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "city") + end + + def sql_for_street1_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "street1") + end + + def sql_for_street2_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "street2") + end + + def sql_for_full_address_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "full_address") + end + + def sql_for_region_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "region") + end + + def sql_for_postcode_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "postcode") + end + + def sql_for_has_deals_field(field, operator, value) + db_table = Deal.table_name + if operator == "!" + "#{Contact.table_name}.id IN ( + SELECT #{db_table}.contact_id FROM #{db_table} + GROUP BY #{db_table}.contact_id + HAVING COUNT(#{db_table}.id) = 0)" + else operator == "=" + "#{Contact.table_name}.id IN ( + SELECT #{db_table}.contact_id FROM #{db_table} + GROUP BY #{db_table}.contact_id + HAVING COUNT(#{db_table}.id) > 0)" + end + end + + def sql_for_has_open_issues_field(field, operator, value) + db_table = ContactNote.table_name + if operator == "!*" + "#{Contact.table_name}.id IN ( + SELECT #{Contact.table_name}.id FROM #{Contact.table_name} + LEFT JOIN contacts_issues ON contacts_issues.contact_id = #{Contact.table_name}.id + LEFT JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id + LEFT JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id + WHERE (#{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false}) OR (#{IssueStatus.table_name}.is_closed IS NULL) + GROUP BY #{Contact.table_name}.id + HAVING COUNT(#{Issue.table_name}.id) = 0)" + elsif operator == "*" + "#{Contact.table_name}.id IN ( + SELECT contacts_issues.contact_id FROM contacts_issues + INNER JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id + INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id + WHERE #{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false} + GROUP BY contacts_issues.contact_id + HAVING COUNT(#{Issue.table_name}.id) > 0)" + else + "#{Contact.table_name}.id IN ( + SELECT contacts_issues.contact_id FROM contacts_issues + INNER JOIN #{Issue.table_name} ON contacts_issues.issue_id = #{Issue.table_name}.id + INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id + WHERE #{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false} + AND #{sql_for_field("assigned_to_id", operator, value, Issue.table_name, 'assigned_to_id')} + GROUP BY contacts_issues.contact_id)" + end + end + + def sql_for_last_note_field(field, operator, value) + db_table = ContactNote.table_name + if operator == "!*" + "#{Contact.table_name}.id IN ( + SELECT #{Contact.table_name}.id FROM #{Contact.table_name} + LEFT JOIN #{db_table} ON #{db_table}.source_id = #{Contact.table_name}.id and #{db_table}.source_type = 'Contact' + GROUP BY #{Contact.table_name}.id + HAVING COUNT(#{db_table}.id) = 0)" + elsif operator == "*" + "#{Contact.table_name}.id IN ( + SELECT #{Contact.table_name}.id FROM #{Contact.table_name} + INNER JOIN #{db_table} ON #{db_table}.source_id = #{Contact.table_name}.id and #{db_table}.source_type = 'Contact' + GROUP BY #{Contact.table_name}.id + HAVING COUNT(#{db_table}.id) > 0)" + else + "#{Contact.table_name}.id IN ( + SELECT #{db_table}.source_id + FROM #{db_table} + WHERE #{db_table}.source_type='Contact' + AND #{db_table}.id IN + (SELECT MAX(#{db_table}.id) + FROM #{db_table} + WHERE #{db_table}.source_type='Contact' + GROUP BY #{db_table}.source_id) + AND #{sql_for_field(field, operator, value, db_table, 'created_on')} + )" + end + end + + def objects_scope(options={}) + scope = Contact.visible + options[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } unless options[:search].blank? + scope = scope.includes((query_includes + (options[:include] || [])).uniq). + where(statement). + where(options[:conditions]) + scope + end + + def query_includes + [:address, :projects, :assigned_to] + end +end diff --git a/plugins/redmine_contacts/app/models/contacts_issue.rb b/plugins/redmine_contacts/app/models/contacts_issue.rb new file mode 100644 index 0000000..d271488 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contacts_issue.rb @@ -0,0 +1,36 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsIssue < ActiveRecord::Base + include Redmine::SafeAttributes + validates_presence_of :contact_id, :issue_id + validates_uniqueness_of :contact_id, :scope => [:issue_id] + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'issue_id', 'contact_id' + # after_create :send_mails + # after_save :send_mails + + private + + def send_mails + Mailer.deliver_contacts_issue_connected(Contact.find(contact_id), Issue.find(issue_id)) + true + end +end diff --git a/plugins/redmine_contacts/app/models/contacts_mailer.rb b/plugins/redmine_contacts/app/models/contacts_mailer.rb new file mode 100644 index 0000000..c880c18 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contacts_mailer.rb @@ -0,0 +1,262 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsMailer < ActionMailer::Base + include Redmine::I18n + + class UnauthorizedAction < StandardError; end + class MissingInformation < StandardError; end + + helper :application + + attr_reader :email, :user + + def self.default_url_options + h = Setting.host_name + h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank? + { :host => h, :protocol => Setting.protocol } + end + + def bulk_mail(contact, params = {}) + raise l(:error_empty_email) if (contact.emails.empty? || params[:message].blank?) + + @contact = contact + @params = params + + params[:attachments].each_value do |mail_attachment| + if file = mail_attachment['file'] + file.rewind if file + attachments[file.original_filename] = file.binread + file.rewind if file + elsif token = mail_attachment['token'] + if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ + attachment_id, attachment_digest = $1, $2 + if a = Attachment.where(:id => attachment_id, :digest => attachment_digest).first + attachments[a.filename] = File.binread(a.diskfile) + end + end + end + end unless params[:attachments].blank? + + mail(:from => params[:from] || User.current.mail, + :to => contact.emails.first, + :cc => params[:cc], + :bcc => params[:bcc], + :subject => params[:subject]) do |format| + format.text + format.html + end + + end + + def self.receive(email, options={}) + @@contacts_mailer_options = options.dup + super email + end + + # Processes incoming emails + # Returns the created object (eg. an issue, a message) or false + def receive(email) + # debugger + @email = email + sender_email = email.from.to_a.first.to_s.strip + # Ignore emails received from the application emission address to avoid hell cycles + if sender_email.downcase == Setting.mail_from.to_s.strip.downcase + logger.info "ContactsMailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info + return false + end + @user = User.find_by_mail(sender_email) if sender_email.present? + if @user.nil? || (@user && !@user.active?) + logger.info "ContactsMailHandler: user not found [#{sender_email}]" if logger && logger.info + end + dispatch + end + + def dispatch + deal_id = email.to.to_s.match(/.+\+d([0-9]*)/).to_a[1] + deal_id ||= email.bcc.to_s.match(/.+\+d([0-9]*)/).to_a[1] + deal_id ||= email.cc.to_s.match(/.+\+d([0-9]*)/).to_a[1] + + if deal_id + deal = Deal.find_by_id(deal_id) + if deal + return [*receive_deal_note(deal_id)] + end + end + + contacts = [] + + if contacts.blank? + contact_id = email.to.to_s.match(/.+\+c([0-9]*)/).to_a[1] + contact_id ||= email.bcc.to_s.match(/.+\+c([0-9]*)/).to_a[1] + contact_id ||= email.cc.to_s.match(/.+\+c([0-9]*)/).to_a[1] + contacts = Contact.where(:id => contact_id) + end + + if contacts.blank? + contacts = Contact.find_by_emails(email.to.to_a) + end + + if contacts.blank? + from_key_words = get_keyword_locales(:label_crm_mail_from) + @plain_text_body = plain_text_body.gsub(/^>\s*/, '').gsub('> ','').gsub('"', '"') + full_address = plain_text_body.match(/^(#{from_key_words.join('|')})[ \s]*:[ \s]*(.+)\s*$/).to_a[2] + + email_address = full_address.match(/[\w,\.,\-,\+]+@.+\.\w{2,}/) if full_address + contacts = Contact.find_by_emails([email_address.to_s.strip]) if email_address + end + + if contacts.blank? + return false + end + + raise MissingInformation if contacts.blank? + + result = [] + contacts.each do |contact| + result << receive_contact_note(contact.id) + end + result + + rescue ActiveRecord::RecordInvalid => e + # TODO: send a email to the user + logger.error e.message if logger + false + rescue MissingInformation => e + logger.error "ContactsMailHandler: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "ContactsMailHandler: unauthorized attempt from #{user}" if logger + false + end + + # Receives a reply to a forum message + def receive_contact_note(contact_id) + contact = Contact.find_by_id(contact_id) + note = nil + # logger.error "ContactsMailHandler: receive_contact_note user: #{user}, + # contact: #{contact.name}, + # editable: #{contact.editable?(self.user)}, + # current: #{User.current}" + raise UnauthorizedAction unless contact.editable?(self.user) + if contact + note = ContactNote.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, + :type_id => Note.note_types[:email], + :content => plain_text_body, + :created_on => email.date) + note.author = self.user + contact.notes << note + add_attachments(note) + logger.info note + note.save + contact.save + end + note + end + + def receive_deal_note(deal_id) + deal = Deal.find_by_id(deal_id) + note = nil + # logger.error "ContactsMailHandler: receive_contact_note user: #{user}, + # contact: #{contact.name}, + # editable: #{contact.editable?(self.user)}, + # current: #{User.current}" + raise UnauthorizedAction unless deal.editable?(self.user) + if deal + note = DealNote.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip, + :type_id => Note.note_types[:email], + :content => plain_text_body, + :created_on => email.date) + note.author = self.user + deal.notes << note + add_attachments(note) + logger.info note + note.save + deal.save + end + note + end + + private + + # Destructively extracts the value for +attr+ in +text+ + # Returns nil if no matching keyword found + def extract_keyword!(text, attr, format=nil) + keys = [attr.to_s.humanize] + if attr.is_a?(Symbol) + keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present? + keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present? + end + keys.reject! {|k| k.blank?} + keys.collect! {|k| Regexp.escape(k)} + format ||= '.+' + text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '') # /^(От:)[ \t]*:[ \t]*(.+)\s*$/i + $2 && $2.strip + end + + def add_attachments(obj) + if email.attachments && email.attachments.any? + email.attachments.each do |attachment| + obj.attachments << Attachment.create(:container => obj, + :file => attachment.decoded, + :filename => attachment.filename, + :author => user, + :content_type => attachment.mime_type) + end + end + end + + # Returns the text/plain part of the email + # If not found (eg. HTML-only email), returns the body with tags removed + def plain_text_body + + return @plain_text_body unless @plain_text_body.nil? + + part = email.text_part || email.html_part || email + @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset) + + # strip html tags and remove doctype directive + @plain_text_body = ActionController::Base.helpers.strip_tags(@plain_text_body.strip) unless email.text_part + @plain_text_body.sub! %r{^ lc)}.uniq + end + + # Appends a Redmine header field (name is prepended with 'X-Redmine-') + def redmine_headers(h) + h.each { |k,v| headers["X-Redmine-#{k}"] = v } + end + + def initialize_defaults(method_name) + super + # Common headers + headers 'X-Mailer' => 'Redmine Contacts', + 'X-Redmine-Host' => Setting.host_name, + 'X-Redmine-Site' => Setting.app_title + end + + def logger + Rails.logger + end + +end diff --git a/plugins/redmine_contacts/app/models/contacts_setting.rb b/plugins/redmine_contacts/app/models/contacts_setting.rb new file mode 100644 index 0000000..d205d11 --- /dev/null +++ b/plugins/redmine_contacts/app/models/contacts_setting.rb @@ -0,0 +1,171 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsSetting < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + TAX_TYPE_EXCLUSIVE = 1 + TAX_TYPE_INCLUSIVE = 2 + + belongs_to :project + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'name', 'value', 'project_id' + + cattr_accessor :settings + acts_as_attachable + + # Hash used to cache setting values + @contacts_cached_settings = {} + @contacts_cached_cleared_on = Time.now + + validates_uniqueness_of :name, :scope => [:project_id] + + # Returns the value of the setting named name + def self.[](name, project_id) + project_id = project_id.id if project_id.is_a?(Project) + v = @contacts_cached_settings[hk(name, project_id)] + v ? v : (@contacts_cached_settings[hk(name, project_id)] = find_or_default(name, project_id).value) + end + + def self.[]=(name, project_id, v) + project_id = project_id.id if project_id.is_a?(Project) + setting = find_or_default(name, project_id) + setting.value = (v ? v : '') + @contacts_cached_settings[hk(name, project_id)] = nil + setting.save + setting.value + end + + # Checks if settings have changed since the values were read + # and clears the cache hash if it's the case + # Called once per request + def self.check_cache + settings_updated_on = ContactsSetting.maximum(:updated_on) + if settings_updated_on && @contacts_cached_cleared_on <= settings_updated_on + clear_cache + end + end + + # Clears the settings cache + def self.clear_cache + @contacts_cached_settings.clear + @contacts_cached_cleared_on = Time.now + logger.info 'Contacts settings cache cleared.' if logger + end + + def self.contact_name_format + Setting.plugin_redmine_contacts['name_format'] || :firstname_lastname + end + + def self.vcard? + Object.const_defined?(:Vcard) + end + + def self.spreadsheet? + Object.const_defined?(:Spreadsheet) + end + + def self.monochrome_tags? + !!Setting.plugin_redmine_contacts['monochrome_tags'] + end + + def self.contacts_show_in_top_menu? + !!Setting.plugin_redmine_contacts['contacts_show_in_top_menu'] + end + + def self.contacts_show_in_app_menu? + !!Setting.plugin_redmine_contacts['contacts_show_in_app_menu'] + end + + def self.default_country + Setting.plugin_redmine_contacts['default_country'] + end + + def self.cross_project_contacts? + Setting.plugin_redmine_contacts['cross_project_contacts'].to_i > 0 + end + + # Finance + + def self.default_currency + Setting.plugin_redmine_contacts['default_currency'] || 'USD' + end + + def self.major_currencies + currencies = Setting.plugin_redmine_contacts['major_currencies'].to_s.split(',').select { |c| !c.blank? }.map(&:strip) + currencies = %w(USD EUR GBP RUB CHF) if currencies.blank? + currencies.compact.uniq + end + + def self.default_tax + Setting.plugin_redmine_contacts['default_tax'].to_f + end + + def self.tax_type + ((['1', '2'] & [Setting.plugin_redmine_contacts['tax_type'].to_s]).first || TAX_TYPE_EXCLUSIVE).to_i + end + + def self.tax_exclusive? + ContactsSetting.tax_type == TAX_TYPE_EXCLUSIVE + end + + def self.thousands_delimiter + ([' ', ',', '.'] & [Setting.plugin_redmine_contacts['thousands_delimiter']]).first || ' ' + end + + def self.decimal_separator + ([',', '.'] & [Setting.plugin_redmine_contacts['decimal_separator']]).first || '.' + end + + def self.disable_taxes? + !!Setting.plugin_redmine_contacts['disable_taxes'] + end + + def self.post_address_format + unless Setting.plugin_redmine_contacts['post_address_format'].blank? + Setting.plugin_redmine_contacts['post_address_format'].to_s.strip + else + "%street1%\n%street2%\n%city%, %postcode%\n%region%\n%country%" + end + end + def self.deals_show_in_top_menu? + !!Setting.plugin_redmine_contacts['deals_show_in_top_menu'] + end + + def self.deals_show_in_app_menu? + !!Setting.plugin_redmine_contacts['deals_show_in_app_menu'] + end + + private + + def self.hk(name, project_id) + "#{name}-#{project_id.to_s}" + end + + # Returns the Setting instance for the setting named name + # (record found in database or new record with default value) + def self.find_or_default(name, project_id) + name = name.to_s + setting = find_by_name_and_project_id(name, project_id) + setting ||= new(:name => name, :value => '', :project_id => project_id) + end + +end diff --git a/plugins/redmine_contacts/app/models/crm_query.rb b/plugins/redmine_contacts/app/models/crm_query.rb new file mode 100644 index 0000000..070c410 --- /dev/null +++ b/plugins/redmine_contacts/app/models/crm_query.rb @@ -0,0 +1,244 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module CrmQuery + + def self.included(base) + base.send :include, InstanceMethods + base.extend ClassMethods + end + + module ClassMethods + def visible(*args) + user = args.shift || User.current + base = Project.allowed_to_condition(user, "view_#{queried_class.name.pluralize.downcase}".to_sym, *args) + if Redmine::VERSION.to_s < '2.4' + user_id = user.logged? ? user.id : 0 + return includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id) + end + + scope = eager_load(:project).where("#{table_name}.project_id IS NULL OR (#{base})") + if user.admin? + scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", Query::VISIBILITY_PRIVATE, user.id) + elsif user.memberships.any? + scope.where("#{table_name}.visibility = ?" + + " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" + + "SELECT DISTINCT q.id FROM #{table_name} q" + + " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" + + " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" + + " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" + + " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" + + " OR #{table_name}.user_id = ?", + Query::VISIBILITY_PUBLIC, Query::VISIBILITY_ROLES, user.id, user.id) + elsif user.logged? + scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", Query::VISIBILITY_PUBLIC, user.id) + else + scope.where("#{table_name}.visibility = ?", Query::VISIBILITY_PUBLIC) + end + end + end + + module InstanceMethods + def visible?(user=User.current) + return true if user.admin? + return false unless project.nil? || user.allowed_to?("view_#{queried_class.name.pluralize.downcase}".to_sym, project) + case visibility + when Query::VISIBILITY_PUBLIC + true + when Query::VISIBILITY_ROLES + if project + (user.roles_for_project(project) & roles).any? + else + Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any? + end + else + user == self.user + end + end + + def is_private? + visibility == Query::VISIBILITY_PRIVATE + end + + def is_public? + !is_private? + end + + def initialize_project_filter(position=nil) + if project.blank? + project_values = [] + if User.current.logged? && User.current.memberships.any? + project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] + end + project_values += all_projects_values + add_available_filter("project_id", :order => position, + :type => :list, :values => project_values + ) unless project_values.empty? + end + end + + def initialize_author_filter(position=nil) + add_available_filter("author_id", :order => position, + :type => :list_optional, :values => users_values + ) unless users_values.empty? + end + + def initialize_assignee_filter(position=nil) + add_available_filter("assigned_to_id", :order => position, + :type => :list_optional, :values => users_values + ) unless users_values.empty? + end + + def initialize_contact_country_filter(position=nil) + contact_countries = l(:label_crm_countries).map{|k, v| [v, k]} + add_available_filter("contact_country", :order => position, + :type => :list_optional, :values => contact_countries, :label => :label_crm_contact_country + ) unless contact_countries.empty? + end + + def initialize_contact_city_filter(position=nil) + add_available_filter("contact_city", :order => position, + :type => :string, :label => :label_crm_contact_city + ) + end + + def sql_for_contact_country_field(field, operator, value) + if operator == '*' # Any group + contact_countries = l(:label_crm_countries).map{|k, v| k.to_s} + operator = '=' # Override the operator since we want to find by assigned_to + elsif operator == "!*" + contact_countries = l(:label_crm_countries).map{|k, v| k.to_s} + operator = '!' # Override the operator since we want to find by assigned_to + else + contact_countries = value + end + '(' + sql_for_field("address_id", operator, contact_countries, Address.table_name, "country_code", false) + ')' + end + + def sql_for_contact_city_field(field, operator, value) + sql_for_field(field, operator, value, Address.table_name, "city") + end + + def sql_for_ids_field(field, operator, value) + if operator == "*" + "1=1" + elsif operator == "=" + ids = value.first.to_s.scan(/\d+/).map(&:to_i).join(",") + if ids.present? + "#{self.queried_class.table_name}.id IN (#{ids})" + else + "1=0" + end + elsif operator == ">=" + id = value.first.to_s.scan(/\d+/).map(&:to_i).first + if id.present? + "#{self.queried_class.table_name}.id >= (#{id})" + else + "1=0" + end + elsif operator == "<=" + id = value.first.to_s.scan(/\d+/).map(&:to_i).first + if id.present? + "#{self.queried_class.table_name}.id <= (#{id})" + else + "1=0" + end + elsif operator == "><" + if value.is_a? Array + "#{self.queried_class.table_name}.id BETWEEN #{value.first} AND #{value.last}" + else + "1=0" + end + else + "1=0" + end + end if Redmine::VERSION.to_s >= '3.3' + + + def principals + return @principals if @principals + @principals = [] + if project + @principals += project.principals.sort + unless project.leaf? + subprojects = project.descendants.visible.all + @principals += Principal.member_of(subprojects) + end + else + if all_projects.any? + @principals += Principal.member_of(all_projects) + end + end + @principals.uniq! + @principals.sort! + end + + def users_values + return @users_values if @users_values + users = principals.select {|p| p.is_a?(User)} + @users_values = [] + @users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + @users_values += users.collect{|s| [s.name, s.id.to_s] } + @users_values + end + + def object_count + objects_scope.count + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def object_count_by_group + r = nil + if grouped? + begin + # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value + r = objects_scope. + joins(joins_for_order_statement(group_by_statement)). + group(group_by_statement).count + rescue ActiveRecord::RecordNotFound + r = {nil => object_count} + end + c = group_by_column + if c.is_a?(QueryCustomFieldColumn) + r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} + end + end + r + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def objects_scope(options={}) + raise NotImplementedError.new("You must implement #{name}.") + end + + def results_scope(options={}) + order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?) + + objects_scope(options). + order(order_option). + joins(joins_for_order_statement(order_option.join(','))). + limit(options[:limit]). + offset(options[:offset]) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + end +end diff --git a/plugins/redmine_contacts/app/models/deal.rb b/plugins/redmine_contacts/app/models/deal.rb new file mode 100644 index 0000000..a0f54c5 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal.rb @@ -0,0 +1,306 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class Deal < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + belongs_to :project + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id' + belongs_to :category, :class_name => 'DealCategory', :foreign_key => 'category_id' + belongs_to :contact + belongs_to :status, :class_name => 'DealStatus', :foreign_key => 'status_id' + has_many :deals, :class_name => 'Deal', :foreign_key => 'reference_id' + has_many :notes, :as => :source, :class_name => 'DealNote', :dependent => :delete_all + has_many :deal_processes, :dependent => :delete_all + has_many :deals_issues, :dependent => :destroy + has_many :issues, :through => :deals_issues + + if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2' + has_many :lines, :class_name => 'ProductLine', :as => :container, :dependent => :delete_all + has_many :products, :through => :lines, :uniq => true, :select => "#{Product.table_name}.*, #{ProductLine.table_name}.position" + + accepts_nested_attributes_for :lines, :allow_destroy => true + safe_attributes 'lines_attributes' + acts_as_priceable :amount, :tax_amount, :subtotal, :total + + before_validation :assign_lines + before_save :calculate_price + end + + if ActiveRecord::VERSION::MAJOR >= 4 + has_and_belongs_to_many :related_contacts, lambda { order("#{Contact.table_name}.last_name, #{Contact.table_name}.first_name") }, :uniq => true, :class_name => 'Contact' + else + has_and_belongs_to_many :related_contacts, :order => "#{Contact.table_name}.last_name, #{Contact.table_name}.first_name", :uniq => true, :class_name => 'Contact' + end + + scope :visible, lambda {|*args| + joins(:project).where(Project.allowed_to_condition(args.shift || User.current, :view_deals, *args)) + } + scope :by_project, lambda { |project_id| where(:project_id => project_id) unless project_id.blank? } + scope :deletable, lambda { |*args| joins(:project).where(Project.allowed_to_condition(args.first || User.current, :delete_deals)) } + + scope :live_search, lambda { |search| where("(#{Deal.table_name}.name LIKE ?)", "%#{search}%") } + scope :live_search_with_contact, ->(search) do + conditions = [] + values = {} + search.split(' ').each_with_index { |word, index| + key = :"v#{index}" + conditions << "LOWER(#{Deal.table_name}.name) LIKE LOWER(:#{key})" + conditions << "LOWER(#{Contact.table_name}.first_name) LIKE LOWER(:#{key})" + conditions << "LOWER(#{Contact.table_name}.last_name) LIKE LOWER(:#{key})" + conditions << "LOWER(#{Contact.table_name}.company) LIKE LOWER(:#{key})" + conditions << "LOWER(#{Contact.table_name}.email) LIKE LOWER(:#{key})" + values[key] = "%#{word}%" + } + sql = conditions.join(' OR ') + joins(:contact).where(sql, values) + end + + scope :open, lambda { joins(:status).where("(#{DealStatus.table_name}.status_type = ? OR #{DealStatus.table_name}.status_type IS NULL)", DealStatus::OPEN_STATUS) } + scope :closed, lambda { joins(:status).where("#{DealStatus.table_name}.status_type <> ?", DealStatus::OPEN_STATUS) } + scope :won, lambda { joins(:status).where("#{DealStatus.table_name}.status_type = ?", DealStatus::WON_STATUS) } + scope :lost, lambda { joins(:status).where("#{DealStatus.table_name}.status_type = ?", DealStatus::LOST_STATUS) } + scope :was_in_status, lambda { |status_id| joins(:deal_processes).where(["#{DealProcess.table_name}.old_value = ? OR #{DealProcess.table_name}.value = ?", status_id, status_id]).uniq } + scope :with_status, lambda { |status_id| where(:status_id => status_id) } + + acts_as_priceable :price, :expected_revenue + acts_as_customizable + acts_as_viewable + acts_as_watchable + acts_as_attachable :view_permission => :view_deals, + :delete_permission => :edit_deals + + acts_as_event :datetime => :created_on, + :url => Proc.new { |o| { :controller => 'deals', :action => 'show', :id => o } }, + :type => 'icon icon-add-deal', + :title => Proc.new { |o| o.name }, + :description => Proc.new { |o| [o.price_to_s, o.contact ? o.contact.name : nil, o.background].join(' ').strip } + + if ActiveRecord::VERSION::MAJOR >= 4 + acts_as_activity_provider :type => 'deals', + :permission => :view_deals, + :author_key => :author_id, + :scope => joins(:project) + + acts_as_searchable :columns => ["#{table_name}.name", + "#{table_name}.background", + "#{DealNote.table_name}.content"], + :scope => includes([:project, :notes]), + :date_column => :created_on + else + acts_as_activity_provider :type => 'deals', + :permission => :view_deals, + :author_key => :author_id, + :find_options => { :include => :project } + + acts_as_searchable :columns => ["#{table_name}.name", + "#{table_name}.background", + "#{DealNote.table_name}.content"], + :include => [:project, :notes], + :order_column => "#{table_name}.id" + end + + validates_presence_of :name, :project, :status + validates_numericality_of :price, :allow_nil => true + + after_update :create_deal_process + after_create :send_notification + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'name', + 'background', + 'currency', + 'price', + 'price_type', + 'duration', + 'project_id', + 'author_id', + 'assigned_to_id', + 'status_id', + 'contact_id', + 'category_id', + 'probability', + 'due_date', + 'custom_field_values', + 'custom_fields', + 'watcher_user_ids', + :if => lambda { |deal, user| deal.new_record? || user.allowed_to?(:edit_deals, deal.project) } + + def initialize(attributes = nil, *args) + super + return unless new_record? + # set default values for new records only + self.status_id = DealStatus.default.try(:id) + self.currency ||= ContactsSetting.default_currency + end + + def avatar + end + + def expected_revenue + probability ? (probability.to_f / 100) * price.to_f : price + end + + def full_name + result = '' + result << contact.name + ': ' unless contact.blank? + result << name + end + + def all_contacts + @all_contacts ||= ([contact] + related_contacts).uniq + end + + def self.available_users(prj = nil) + cond = '(1=1)' + cond << " AND #{Deal.table_name}.project_id = #{prj.id}" if prj + User.active.select("DISTINCT #{User.table_name}.*"). + joins("JOIN #{Deal.table_name} ON #{Deal.table_name}.assigned_to_id = #{User.table_name}.id"). + where(cond). + order("#{User.table_name}.lastname, #{User.table_name}.firstname") + end + + def open? + status.blank? || status.is_open? + end + + def init_deal_process(author) + @current_deal_process ||= DealProcess.new(:deal => self, :author => (author || User.current)) + @deal_status_before_change = new_record? ? nil : status_id + @current_deal_process + end + + def create_deal_process + if @current_deal_process && @deal_status_before_change && !(@deal_status_before_change == status_id) + @current_deal_process.old_value = @deal_status_before_change + @current_deal_process.value = status_id + @current_deal_process.save + init_deal_process @current_deal_process.author + end + end + + def visible?(usr = nil) + (usr || User.current).allowed_to?(:view_deals, project) + end + + def editable?(usr = nil) + (usr || User.current).allowed_to?(:edit_deals, project) + end + + def destroyable?(usr = nil) + (usr || User.current).allowed_to?(:delete_deals, project) + end + + # Returns an array of projects that user can move deal to + def self.allowed_target_projects(user = User.current) + Project.where(Project.allowed_to_condition(user, :add_deals)) + end + + # Returns the mail adresses of users that should be notified + def recipients + notified = [] + # Author and assignee are always notified unless they have been + # locked or don't want to be notified + notified << author if author + if assigned_to + notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) + end + + notified += project.notified_users + notified = notified.select { |u| u.active? } + notified.uniq! + # Remove users that can not view the contact + notified.reject! { |user| !visible?(user) } + notified.collect(&:mail) + end + + def status_was + if status_id_changed? && status_id_was.present? + @status_was ||= DealStatus.find_by_id(status_id_was) + end + end + + def copy_from(arg) + deal = arg.is_a?(Deal) ? arg : Deal.visible.find(arg) + self.attributes = deal.attributes.dup.except('id', 'created_at', 'updated_at') + self.custom_field_values = deal.custom_field_values.inject({}) { |h, v| h[v.custom_field_id] = v.value ; h } + if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2' + deal.lines.each do |line| + lines.build(line.attributes) + end + end + self + end + + def contact_country + try(:contact).try(:address).try(:country) + end + + def contact_city + try(:contact).try(:address).try(:city) + end + + if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2' + def has_taxes? + !lines.map(&:tax).all? { |t| t == 0 || t.blank? } + end + + def has_discounts? + !lines.map(&:discount).all? { |t| t == 0 || t.blank? } + end + + def tax_amount + lines.select { |l| !l.marked_for_destruction? }.inject(0) { |sum, l| sum + l.tax_amount }.to_f + end + + def subtotal + lines.select { |l| !l.marked_for_destruction? }.inject(0) { |sum, l| sum + l.total }.to_f + end + + def total_units + lines.inject(0) { |sum, l| sum + (l.product.blank? ? 0 : l.quantity) } + end + + def calculate_price + return true if lines.select { |l| !l.marked_for_destruction? }.empty? + self.price = subtotal + (ContactsSetting.tax_exclusive? ? tax_amount : 0) + end + end + + def info + result = '' + result = status.name if status + result = result + ' - ' + price_to_s unless price.blank? + result.html_safe + end + private + + def send_notification + Mailer.crm_deal_add(self).deliver if Setting.notified_events.include?('crm_deal_added') + end + + if Redmine::Plugin.load && Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2' + def assign_lines + return unless new_record? + lines.each { |l| l.container = self } + end + end +end diff --git a/plugins/redmine_contacts/app/models/deal_category.rb b/plugins/redmine_contacts/app/models/deal_category.rb new file mode 100644 index 0000000..79a79e2 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_category.rb @@ -0,0 +1,52 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealCategory < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'name' + + belongs_to :project + has_many :deals, :class_name => 'Deal', :foreign_key => 'category_id', :dependent => :nullify + validates_presence_of :name, :project + validates_uniqueness_of :name, :scope => [:project_id] + validates_length_of :name, :maximum => 30 + + alias :destroy_without_reassign :destroy + + # Destroy the category + # If a category is specified, issues are reassigned to this category + def destroy(reassign_to = nil) + if reassign_to && reassign_to.is_a?(DealCategory) && reassign_to.project == self.project + if ActiveRecord::VERSION::MAJOR >= 4 + Deal.where(:category_id => id).update_all(:category_id => reassign_to.id) + else + Deal.update_all("category_id = #{reassign_to.id}", "category_id = #{id}") + end + end + destroy_without_reassign + end + + def <=>(category) + name <=> category.name + end + + def to_s; name end +end diff --git a/plugins/redmine_contacts/app/models/deal_custom_field.rb b/plugins/redmine_contacts/app/models/deal_custom_field.rb new file mode 100644 index 0000000..3dfea75 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_custom_field.rb @@ -0,0 +1,26 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealCustomField < CustomField + unloadable + + def type_name + :label_deal_plural + end +end diff --git a/plugins/redmine_contacts/app/models/deal_import.rb b/plugins/redmine_contacts/app/models/deal_import.rb new file mode 100644 index 0000000..e15f24d --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_import.rb @@ -0,0 +1,46 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + + +class DealImport + extend ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + include CSVImportable + + attr_accessor :file, :project, :quotes_type + + def klass + Deal + end + + def build_from_fcsv_row(row) + ret = Hash[row.to_hash.collect { |k, v| [k.underscore.tr(' ', '_'), force_utf8(v)] if k }].delete_if { |k, _v| !klass.column_names.include?(k) } + ret[:due_date] = row['due date'].to_date if row['due date'] + ret[:status_id] = DealStatus.where(:name => row['status']).first.try(:id) if row['status'] + ret[:category_id] = DealCategory.where(:name => row['category']).first.try(:id) if row['category'] + ret[:assigned_to_id] = User.find_by_login(row['assignee']).try(:id) unless row['assignee'].blank? + ret[:price] = row['sum'].to_f if row['sum'] + if row['contact'].to_s.match(/^\#(\d+):/) + ret[:contact_id] = Contact.find_by_id($1).try(:id) + end + ret + end + +end diff --git a/plugins/redmine_contacts/app/models/deal_kernel_import.rb b/plugins/redmine_contacts/app/models/deal_kernel_import.rb new file mode 100644 index 0000000..5bc1aef --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_kernel_import.rb @@ -0,0 +1,101 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealKernelImport < Import + + def klass + Deal + end + + def saved_objects + object_ids = saved_items.pluck(:obj_id) + Deal.where(:id => object_ids).order(:id) + end + + def project=(project) + settings['project'] = project.id + end + + def project + settings['project'] + end + + private + + def build_object(row, _item = nil) + deal = Deal.new + deal.project = Project.find(settings['project']) + deal.author = user + + attributes = {} + if name = row_value(row, 'name') + attributes['name'] = name + end + if background = row_value(row, 'background') + attributes['background'] = background + end + if currency = row_value(row, 'currency') + attributes['currency'] = currency + end + if price = row_value(row, 'price') + attributes['price'] = price.to_f + end + if probability = row_value(row, 'probability') + attributes['probability'] = probability.to_i + end + if status = row_value(row, 'status') + attributes['status_id'] = DealStatus.where('name = ?', status).first.try(:id) + end + if contact = row_value(row, 'contact') + attributes['contact_id'] = Contact.by_full_name(contact).first.try(:id) + end + if assigned_to = row_value(row, 'assigned_to') + attributes['assigned_to_id'] = User.where("LOWER(CONCAT(#{User.table_name}.firstname,' ',#{User.table_name}.lastname)) = ? ", assigned_to.mb_chars.downcase.to_s) + .first + .try(:id) + end + if category = row_value(row, 'category') + attributes['category_id'] = DealCategory.where(:name => category).first.try(:id) + end + + attributes['custom_field_values'] = deal.custom_field_values.inject({}) do |h, v| + value = case v.custom_field.field_format + when 'date' + row_date(row, "cf_#{v.custom_field.id}") + when 'list' + row_value(row, "cf_#{v.custom_field.id}").try(:split, ',') + else + row_value(row, "cf_#{v.custom_field.id}") + end + if value + h[v.custom_field.id.to_s] = + if value.is_a?(Array) + value.map { |val| v.custom_field.value_from_keyword(val.strip, contact) }.compact.flatten + else + v.custom_field.value_from_keyword(value, contact) + end + end + h + end + + deal.send :safe_attributes=, attributes, user + deal + end + +end diff --git a/plugins/redmine_contacts/app/models/deal_note.rb b/plugins/redmine_contacts/app/models/deal_note.rb new file mode 100644 index 0000000..75676b6 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_note.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealNote < Note + unloadable + include Redmine::SafeAttributes + belongs_to :deal, :foreign_key => :source_id + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'subject', 'type_id', 'content' + if ActiveRecord::VERSION::MAJOR >= 4 + if ActiveRecord::Base.connection.table_exists?('notes') + acts_as_activity_provider :type => 'deals', + :permission => :view_deals, + :author_key => :author_id, + :scope => joins(:deal => :project).where(:source_type => 'Deal') + end + else + acts_as_activity_provider :type => 'deals', + :permission => :view_deals, + :author_key => :author_id, + :find_options => { :joins => [:deal => :project], + :conditions => { :source_type => 'Deal' } } + end + + scope :visible, lambda {|*args| joins(:deal => :project). + where(Project.allowed_to_condition(args.first || User.current, :view_deals) + + " AND (#{DealNote.table_name}.source_type = 'Deal')") } + acts_as_attachable :view_permission => :view_deals, + :delete_permission => :edit_deals + + def custom_field_values + Note.new.custom_field_values + end +end diff --git a/plugins/redmine_contacts/app/models/deal_process.rb b/plugins/redmine_contacts/app/models/deal_process.rb new file mode 100644 index 0000000..1536fe3 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_process.rb @@ -0,0 +1,44 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealProcess < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'deal', 'author' + + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :deal + belongs_to :from, :class_name => 'DealStatus', :foreign_key => 'old_value' + belongs_to :to, :class_name => 'DealStatus', :foreign_key => 'value' + scope :visible, lambda { |*args| joins(:deal => :project).where(Project.allowed_to_condition(args.first || User.current, :view_deals)) } + + after_create :send_notification + + def recipients + (deal.recipients + [author.mail]).uniq + end + + private + + def send_notification + Mailer.crm_deal_updated(self).deliver if Setting.notified_events.include?('crm_deal_updated') + end +end diff --git a/plugins/redmine_contacts/app/models/deal_query.rb b/plugins/redmine_contacts/app/models/deal_query.rb new file mode 100644 index 0000000..5ad2ad6 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_query.rb @@ -0,0 +1,178 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealQuery < Query + include CrmQuery + include RedmineCrm::MoneyHelper + + self.queried_class = Deal + self.view_permission = :view_deals if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? + + self.available_columns = [ + QueryColumn.new(:name, :sortable => "#{Deal.table_name}.name", :caption => :field_deal_name), + QueryColumn.new(:price, :sortable => ["#{Deal.table_name}.currency", "#{Deal.table_name}.price"], :default_order => 'desc', :caption => :field_price), + QueryColumn.new(:status, :sortable => "#{Deal.table_name}.status_id", :groupable => true, :caption => :field_contact_status), + QueryColumn.new(:currency, :sortable => "#{Deal.table_name}.currency", :groupable => true, :caption => :field_currency), + QueryColumn.new(:contact, :sortable => lambda { Contact.fields_for_order_statement }, :groupable => true, :caption => :label_contact), + QueryColumn.new(:category, :sortable => "#{Deal.table_name}.category_id", :groupable => true), + QueryColumn.new(:probability, :sortable => "#{Deal.table_name}.probability", :groupable => "#{Deal.table_name}.probability", :caption => :label_crm_probability), + QueryColumn.new(:expected_revenue, :sortable => ["#{Deal.table_name}.currency", "#{Deal.table_name}.price * (#{Deal.table_name}.probability / 100)"], :caption => :label_crm_expected_revenue), + QueryColumn.new(:contact_city, :caption => :label_crm_contact_city, :groupable => "#{Address.table_name}.city", :sortable => "#{Address.table_name}.city"), + QueryColumn.new(:contact_country, :caption => :label_crm_contact_country, :groupable => "#{Address.table_name}.country_code", :sortable => "#{Address.table_name}.country_code"), + QueryColumn.new(:due_date, :sortable => "#{Deal.table_name}.due_date"), + QueryColumn.new(:due_date, :sortable => "#{Deal.table_name}.due_date"), + QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), + QueryColumn.new(:created_on, :sortable => "#{Deal.table_name}.created_on"), + QueryColumn.new(:updated_on, :sortable => "#{Deal.table_name}.updated_on"), + QueryColumn.new(:assigned_to, :sortable => lambda { User.fields_for_order_statement }, :groupable => true), + QueryColumn.new(:author, :sortable => lambda { User.fields_for_order_statement('authors') }), + QueryColumn.new(:background) + ] + + def initialize(attributes = nil, *args) + super attributes + self.filters ||= { 'status_id' => { :operator => 'o', :values => [''] } } + end + + def initialize_available_filters + add_available_filter 'ids', :type => :integer, :label => :label_deal if Redmine::VERSION.to_s >= '3.3' + add_available_filter 'price', :type => :float, :label => :field_price + add_available_filter 'currency', :type => :list, + :label => :field_currency, + :values => collection_for_currencies_select(ContactsSetting.default_currency, ContactsSetting.major_currencies) + add_available_filter 'background', :type => :text, :label => :field_background + add_available_filter 'due_date', :type => :date, :order => 20 + add_available_filter 'updated_on', :type => :date_past, :order => 20 + add_available_filter 'created_on', :type => :date, :order => 21 + add_available_filter 'probability', :type => :float, :label => :label_crm_probability + + deal_statuses = (project.blank? ? DealStatus.order("#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position") : project.deal_statuses) || [] + add_available_filter('status_id', + :type => :list_status, :values => deal_statuses.map { |a| [a.name, a.id.to_s] }, :label => :field_contact_status, :order => 1 + ) unless deal_statuses.empty? + + initialize_project_filter + initialize_author_filter + initialize_assignee_filter + initialize_contact_country_filter + initialize_contact_city_filter + + add_custom_fields_filters(DealCustomField.where(:is_filter => true)) + add_associations_custom_fields_filters :contact, :notes, :author, :assigned_to + if RedmineContacts.products_plugin_installed? + products = Product.visible.all + add_available_filter('products', :type => :list_optional, + :values => products.map { |a| [a.name, a.id.to_s] }, :label => :label_product_plural + ) unless products.empty? + + product_categories = [] + ProductCategory.category_tree(ProductCategory.order(:lft)) do |product_category, level| + name_prefix = (level > 0 ? '-' * 2 * level + ' ' : '').html_safe + product_categories << [(name_prefix + product_category.name).html_safe, product_category.id.to_s] + end + add_available_filter('product_category_id', :type => :list, + :label => :label_products_category_filter, + :values => product_categories + ) if product_categories.any? + add_associations_custom_fields_filters :products, :lines + end + end + + def available_columns + return @available_columns if @available_columns + @available_columns = self.class.available_columns.dup + @available_columns += CustomField.where(:type => 'DealCustomField').all.map { |cf| QueryCustomFieldColumn.new(cf) } + @available_columns += CustomField.where(:type => 'ContactCustomField').all.map { |cf| QueryAssociationCustomFieldColumn.new(:contact, cf) } + @available_columns << QueryColumn.new(:products, :caption => :label_product_plural) if RedmineContacts.products_plugin_installed? + @available_columns + end + + def default_columns_names + @default_columns_names ||= [:id, :name, :contact, :price] + end + if RedmineContacts.products_plugin_installed? + def sql_for_products_field(_field, operator, value) + if operator == '*' + products = Product.visible.all + operator = '=' + elsif operator == '!*' + products = Product.visible.all + operator = '!' + else + products = Product.visible.where(:id => value) + end + products ||= [] + + order_products = products.map(&:id).uniq.compact.sort.collect(&:to_s) + '(' + sql_for_field('product_id', operator, order_products, ProductLine.table_name, 'product_id', false) + ')' + end + + def sql_for_product_category_id_field(field, operator, value) + category_ids = value + category_ids += ProductCategory.where(:id => value).map(&:descendants).flatten.collect { |c| c.id.to_s }.uniq + sql_for_field(field, operator, category_ids, Product.table_name, 'category_id') + end + end + + def sql_for_status_id_field(field, operator, value) + sql = '' + case operator + when "o" + sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{DealStatus.table_name} WHERE status_type = #{DealStatus::OPEN_STATUS})" if field == "status_id" + when "c" + sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{DealStatus.table_name} WHERE status_type IN (#{DealStatus::WON_STATUS}, #{DealStatus::LOST_STATUS}))" if field == "status_id" + else + sql_for_field(field, operator, value, queried_table_name, field) + end + end + + def deal_amount + @deal_amount ||= objects_scope.group("#{Deal.table_name}.currency").sum(:price) + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def weighted_amount + @weighted_amount ||= objects_scope.open.group("#{Deal.table_name}.currency").sum("#{Deal.table_name}.price * #{Deal.table_name}.probability / 100") + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def objects_scope(options={}) + scope = Deal.visible + options[:search].split(' ').collect{ |search_string| scope = scope.live_search(search_string) } unless options[:search].blank? + scope = scope.includes((query_includes + (options[:include] || [])).uniq). + where(statement). + where(options[:conditions]) + scope + end + + def query_includes + includes = [:status, :project] + includes << { :contact => :address } if self.filters['contact_country'] || + self.filters['contact_city'] || + [:contact_country, :contact_city].include?(group_by_column.try(:name)) + includes << :assigned_to if self.filters['assigned_to_id'] || (group_by_column && [:assigned_to].include?(group_by_column.name)) + if RedmineContacts.products_plugin_installed? + includes << :products if filters['products'] + includes << :products if filters['product_category_id'] + end + includes + end +end diff --git a/plugins/redmine_contacts/app/models/deal_status.rb b/plugins/redmine_contacts/app/models/deal_status.rb new file mode 100644 index 0000000..4235234 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deal_status.rb @@ -0,0 +1,116 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealStatus < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + OPEN_STATUS = 0 + WON_STATUS = 1 + LOST_STATUS = 2 + + before_destroy :check_integrity + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'name', 'is_default', 'status_type', 'move_to', 'color_name', 'position' + + has_and_belongs_to_many :projects + has_many :deals, :foreign_key => 'status_id', :dependent => :nullify + has_many :deal_processes_from, :class_name => 'DealProcess',:foreign_key => 'old_value', :dependent => :delete_all + has_many :deal_processes_to, :class_name => 'DealProcess', :foreign_key => 'value', :dependent => :delete_all + rcrm_acts_as_list :scope => 'status_type = #{status_type}' + + scope :open, lambda { where(:status_type => DealStatus::OPEN_STATUS) } + scope :won, lambda { where(:status_type => DealStatus::WON_STATUS) } + scope :lost, lambda { where(:status_type => DealStatus::LOST_STATUS) } + scope :closed, lambda { where("#{DealStatus.table_name}.status_type <> #{DealStatus::OPEN_STATUS}") } + + after_save :update_default + + validates_presence_of :name + validates_uniqueness_of :name + validates_length_of :name, :maximum => 30 + + def update_default + DealStatus.where('id <> ?', id).update_all(:is_default => false) if is_default? + end + + # Returns the default status for new Deals + def self.default + where(:is_default => true).first + end + + def is_open? + status_type == OPEN_STATUS + end + + def is_won? + status_type == WON_STATUS + end + + def is_lost? + status_type == LOST_STATUS + end + + def is_closed? + !is_open? + end + + def status_type_name + case status_type + when OPEN_STATUS then l(:label_open_issues) + when WON_STATUS then l(:label_crm_deal_status_won) + when LOST_STATUS then l(:label_crm_deal_status_lost) + else '' + end + end + + def new_status_allowed_to?(status, roles, tracker) + if status && roles && tracker + !workflows.where(:new_status_id => status.id).where(:role_id => roles.collect(&:id)).where(:tracker_id => tracker.id).first.nil? + else + false + end + end + + def color_name + return '#' + "%06x" % color unless color.nil? + end + + def color_name=(clr) + self.color = clr.from(1).hex + end + + def <=>(status) + position <=> status.position + end + + def to_s; name end + + private + + def check_integrity + raise "Can't delete status" if Deal.where(:status_id => id).any? + end + + # Deletes associated workflows + def delete_workflows + Workflow.delete_all(['old_status_id = :id OR new_status_id = :id', { :id => id }]) + end +end diff --git a/plugins/redmine_contacts/app/models/deals_issue.rb b/plugins/redmine_contacts/app/models/deals_issue.rb new file mode 100644 index 0000000..b520f47 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deals_issue.rb @@ -0,0 +1,33 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealsIssue < ActiveRecord::Base + include Redmine::SafeAttributes + belongs_to :issue + belongs_to :deal + + validate :validate_deals_issue + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'issue_id', 'deal_id', 'issue', 'deal' + + def validate_deals_issue + errors.add :deal_id, :invalid if deal_id && !deal + end +end diff --git a/plugins/redmine_contacts/app/models/deals_pipeline_processor.rb b/plugins/redmine_contacts/app/models/deals_pipeline_processor.rb new file mode 100644 index 0000000..78d4dd4 --- /dev/null +++ b/plugins/redmine_contacts/app/models/deals_pipeline_processor.rb @@ -0,0 +1,79 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealsPipelineProcessor + attr_reader :scope + + def initialize(scope) + @scope = scope + end + + def count + @scope.count + end + + def deals_for_status(status) + if status.is_open? + open_deals_for_status(status) + closed_deals_for_status(status) + else + @scope.where(:status_id => status.id) + end + end + + def closed_deals_for_status(status) + deal_status_ids = DealStatus.open.where('position >= ?', status.position).pluck(:id) + first_condition = [] + second_condition = [] + if lost_status_ids.present? + first_condition << "dp.value IN (#{lost_status_ids.join(',')})" + second_condition << "dp2.value IN (#{lost_status_ids.join(',')})" + end + if won_status_ids.present? + first_condition << "dp.old_value IN (#{won_status_ids.join(',')})" + second_condition << "dp2.old_value IN (#{won_status_ids.join(',')})" + end + first_sql = first_condition.present? ? "NOT (#{first_condition.join(' AND ')})" : '1=1' + second_sql = second_condition.present? ? "NOT (#{second_condition.join(' AND ')})" : '1=1' + ret = @scope.closed.joins("LEFT OUTER JOIN #{DealProcess.table_name} dp on dp.deal_id = deals.id AND #{first_sql}"). + joins("LEFT OUTER JOIN #{DealProcess.table_name} dp2 ON (deals.id = dp2.deal_id AND (dp.created_at < dp2.created_at OR dp.created_at = dp2.created_at AND dp.id < dp2.id)) AND #{second_sql}"). + joins("LEFT OUTER JOIN #{DealStatus.table_name} ds ON (ds.id = deals.status_id)"). + where(['ds.status_type IN (?)', [DealStatus::WON_STATUS, DealStatus::LOST_STATUS] ]). + where("dp2.id IS NULL") + if status.is_open? + ret.where(["(dp.old_value IN (?) OR (#{Deal.table_name}.status_id IN (?)))", deal_status_ids, won_status_ids]) + else + ret.where(["dp.old_value IN (?)", deal_status_ids]) + end + end + + def open_deals_for_status(status) + deal_status_ids = DealStatus.open.where('position >= ?', status.position).pluck(:id) + @scope.open.joins("LEFT OUTER JOIN #{DealStatus.table_name} ds ON (ds.id = deals.status_id)"). + where(['ds.status_type NOT IN (?)', [DealStatus::WON_STATUS, DealStatus::LOST_STATUS] ]). + where(["#{Deal.table_name}.status_id IN (?)", deal_status_ids]) + end + + def won_status_ids + @won_status_ids ||= DealStatus.won.pluck(:id) + end + + def lost_status_ids + @lost_status_ids ||= DealStatus.lost.pluck(:id) + end +end diff --git a/plugins/redmine_contacts/app/models/note.rb b/plugins/redmine_contacts/app/models/note.rb new file mode 100644 index 0000000..3d3be34 --- /dev/null +++ b/plugins/redmine_contacts/app/models/note.rb @@ -0,0 +1,95 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class Note < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' + belongs_to :source, :polymorphic => true, :touch => true + + # added as a quick fix to allow eager loading of the polymorphic association for multiprojects + + validates_presence_of :source, :author, :content + + acts_as_customizable + acts_as_attachable + + acts_as_event :title => Proc.new {|o| "#{l(:label_crm_note_for)}: #{o.source.name}"}, + :type => "icon issue-note icon-issue-note", + :group => :source, + :url => Proc.new {|o| {:controller => 'notes', :action => 'show', :id => o.id }}, + :description => Proc.new {|o| o.content} + + after_create :send_notification + + cattr_accessor :note_types + @@note_types = {:email => 0, :call => 1, :meeting => 2} + cattr_accessor :cut_length + @@cut_length = 1000 + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'subject', 'type_id', 'author_id', 'note_time', 'content', 'created_on', 'custom_field_values' + + def self.note_types + @@note_types + end + + def note_time + self.created_on.to_s(:time) unless self.created_on.blank? + end + + def note_time=(val) + if !self.created_on.blank? && val.to_s.gsub(/\s/, "").match(/^(\d{1,2}):(\d{1,2})$/) + self.created_on = self.created_on.change({:hour => $1.to_i % 24, :min => $2.to_i % 60}) + end + end + + def visible?(usr=nil) + self.source.visible?(usr) + end + + def project + self.source.respond_to?(:project) ? self.source.project : nil + end + + def editable_by?(usr, prj=nil) + prj ||= @project || self.project + usr && (usr.allowed_to?(:delete_notes, prj) || (self.author == usr && usr.allowed_to?(:delete_own_notes, prj))) + # usr && usr.logged? && (usr.allowed_to?(:edit_notes, project) || (self.author == usr && usr.allowed_to?(:edit_own_notes, project))) + end + + def destroyable_by?(usr, prj=nil) + prj ||= @project || self.project + usr && (usr.allowed_to?(:delete_notes, prj) || (self.author == usr && usr.allowed_to?(:delete_own_notes, prj))) + end + + def created_on + return nil if super.blank? + zone = User.current.time_zone + zone ? super.in_time_zone(zone) : (super.utc? ? super.localtime : super) + end + +private + + def send_notification + Mailer.crm_note_add(self).deliver if Setting.notified_events.include?('crm_note_added') + end + +end diff --git a/plugins/redmine_contacts/app/models/note_custom_field.rb b/plugins/redmine_contacts/app/models/note_custom_field.rb new file mode 100644 index 0000000..def6a37 --- /dev/null +++ b/plugins/redmine_contacts/app/models/note_custom_field.rb @@ -0,0 +1,26 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class NoteCustomField < CustomField + unloadable + + def type_name + :label_crm_note_plural + end +end diff --git a/plugins/redmine_contacts/app/models/recently_viewed.rb b/plugins/redmine_contacts/app/models/recently_viewed.rb new file mode 100644 index 0000000..aa890be --- /dev/null +++ b/plugins/redmine_contacts/app/models/recently_viewed.rb @@ -0,0 +1,45 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class RecentlyViewed < ActiveRecord::Base + unloadable + include Redmine::SafeAttributes + + attr_protected :id if ActiveRecord::VERSION::MAJOR <= 4 + safe_attributes 'viewer' + + RECENTLY_VIEWED_LIMIT = 5 + + belongs_to :viewer, :class_name => 'User', :foreign_key => 'viewer_id' + belongs_to :viewed, :polymorphic => true + + validates_presence_of :viewed, :viewer + + # after_save :increment_views_count + def self.last(limit=RECENTLY_VIEWED_LIMIT, usr=nil) + RecentlyViewed.where("#{RecentlyViewed.table_name}.viewer_id" => usr || User.current).order("#{RecentlyViewed.table_name}.updated_at DESC").limit(limit).collect{|v| v.viewed}.select(&:visible?).compact + end + + private + + def increment_views_count + self.increment!(:views_count) + end + +end diff --git a/plugins/redmine_contacts/app/models/task.rb b/plugins/redmine_contacts/app/models/task.rb new file mode 100644 index 0000000..6681f94 --- /dev/null +++ b/plugins/redmine_contacts/app/models/task.rb @@ -0,0 +1,32 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class Task < ActiveRecord::Base + validates_presence_of :source_id, :issue_id, :source_type + validates_uniqueness_of :source_id, :scope => [:issue_id, :source_type] + + after_save :send_mails + + private + + def send_mails + Mailer.deliver_contacts_issue_connected(Contact.find(contact_id), Issue.find(issue_id)) + true + end +end diff --git a/plugins/redmine_contacts/app/views/auto_completes/_companies.html.erb b/plugins/redmine_contacts/app/views/auto_completes/_companies.html.erb new file mode 100644 index 0000000..43e3d21 --- /dev/null +++ b/plugins/redmine_contacts/app/views/auto_completes/_companies.html.erb @@ -0,0 +1,10 @@ +<%= raw @companies.map {|company| { + 'id' => company.id, + 'name' => company.name, + 'avatar' => avatar_to(company, :size => 16), + 'email' => company.primary_email, + 'label' => company.name, + 'value' => company.name + } + }.to_json +%> diff --git a/plugins/redmine_contacts/app/views/auto_completes/_contacts.html.erb b/plugins/redmine_contacts/app/views/auto_completes/_contacts.html.erb new file mode 100644 index 0000000..67c4319 --- /dev/null +++ b/plugins/redmine_contacts/app/views/auto_completes/_contacts.html.erb @@ -0,0 +1,11 @@ +<%= raw @contacts.map {|contact| { + 'id' => contact.id, + 'text' => contact.name_with_company, + 'name' => contact.name, + 'avatar' => avatar_to(contact, :size => 16), + 'company' => contact.is_company ? "" : contact.company.to_s, + 'email' => contact.primary_email, + 'value' => contact.id + } + }.to_json +%> diff --git a/plugins/redmine_contacts/app/views/auto_completes/_crm_tag_list.html.erb b/plugins/redmine_contacts/app/views/auto_completes/_crm_tag_list.html.erb new file mode 100644 index 0000000..f0aef7f --- /dev/null +++ b/plugins/redmine_contacts/app/views/auto_completes/_crm_tag_list.html.erb @@ -0,0 +1,4 @@ +<%= raw @tags.collect {|tag| + tag.name + }.to_json +%> diff --git a/plugins/redmine_contacts/app/views/auto_completes/_deals.html.erb b/plugins/redmine_contacts/app/views/auto_completes/_deals.html.erb new file mode 100644 index 0000000..28721be --- /dev/null +++ b/plugins/redmine_contacts/app/views/auto_completes/_deals.html.erb @@ -0,0 +1,9 @@ +<%= raw @deals.map {|deal| { + 'id' => deal.id, + 'label' => "#{deal.full_name} (#{deal.info})", + 'text' => "#{deal.name} (#{deal.info})", + 'avatar' => avatar_to(deal, :size => 16), + 'value' => deal.id + } + }.to_json +%> diff --git a/plugins/redmine_contacts/app/views/common/_address_form.html.erb b/plugins/redmine_contacts/app/views/common/_address_form.html.erb new file mode 100644 index 0000000..df3e6bf --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_address_form.html.erb @@ -0,0 +1,8 @@ +

+ +<%= f.text_field :street1, :no_label => true, :placeholder => l(:label_crm_street1), :style => "width:90%;" -%>

+

<%= f.text_field :street2, :no_label => true, :placeholder => l(:label_crm_street2) -%>

+

<%= f.text_field :city, :no_label => true, :placeholder => l(:label_crm_city) -%>

+

<%= f.text_field :region, :no_label => true, :placeholder => l(:label_crm_region) -%>

+

<%= f.text_field :postcode, :no_label => true, :placeholder => l(:label_crm_postcode), :size => 12 -%>

+

<%= f.select :country_code, countries_options_for_select(f.object.country_code), :no_label => true, :placeholder => l(:label_crm_country), :include_blank => true -%>

diff --git a/plugins/redmine_contacts/app/views/common/_contact_data.html.erb b/plugins/redmine_contacts/app/views/common/_contact_data.html.erb new file mode 100644 index 0000000..e7f5ecd --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_contact_data.html.erb @@ -0,0 +1,17 @@ +<% actions ||= "" %> + + + + + <% if !actions.blank? %> + + <% end %> + +
<%= link_to avatar_to(contact_data, :size => "32"), note_source_url(contact_data), :id => "avatar" %> +

+ <%= link_to contact_data.name, note_source_url(contact_data) %> +

+ <%= contact_data.info %> +
+ <%= actions %> +
diff --git a/plugins/redmine_contacts/app/views/common/_contact_tabs.html.erb b/plugins/redmine_contacts/app/views/common/_contact_tabs.html.erb new file mode 100644 index 0000000..27cc2a1 --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_contact_tabs.html.erb @@ -0,0 +1,29 @@ +<% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %> + +
+
    + <% tabs.each do |tab| -%> +
  • <%= link_to tab[:label], tabs_contact_path(@contact, :tab => tab[:name]), + :id => "tab-#{tab[:name]}", + :class => (tab[:name] != selected_tab ? 'tab-header' : 'selected tab-header'), + :data => { :name => tab[:name], :partial => tab[:partial], :project_id => @project}, + :onclick => "showContactTab('#{tab[:name]}'); this.blur(); return false;" %>
  • + <% end -%> +
+ +
+ +<% tabs.each do |tab| %> + <% selected = tab[:name] == selected_tab %> +
' id='tab-placeholder-<%= tab[:name] %>' style='<%= "display: block" if selected %>'> + <%= render(:partial => tab[:partial]) if selected %> +
+<% end %> + diff --git a/plugins/redmine_contacts/app/views/common/_contacts_select2_data.html.erb b/plugins/redmine_contacts/app/views/common/_contacts_select2_data.html.erb new file mode 100644 index 0000000..823b5a8 --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_contacts_select2_data.html.erb @@ -0,0 +1,9 @@ + diff --git a/plugins/redmine_contacts/app/views/common/_notes_attachments.html.erb b/plugins/redmine_contacts/app/views/common/_notes_attachments.html.erb new file mode 100644 index 0000000..87c1388 --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_notes_attachments.html.erb @@ -0,0 +1,4 @@ +<% if notes_attachments.any? %> +

<%= l(:label_attachment_plural) %>

+<%= render :partial => 'attachments/links', :locals => {:attachments => notes_attachments, :options => {}} %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/common/_recently_viewed.html.erb b/plugins/redmine_contacts/app/views/common/_recently_viewed.html.erb new file mode 100644 index 0000000..abb2759 --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_recently_viewed.html.erb @@ -0,0 +1,4 @@ +

<%= l(:label_crm_recently_viewed) %>

+
+ <%= render :partial => 'common/contact_data', :collection => RecentlyViewed.includes(:viewed).last(5).map(&:viewed).select{|v| !v.blank? && v.visible?} %> +
diff --git a/plugins/redmine_contacts/app/views/common/_responsible_user.html.erb b/plugins/redmine_contacts/app/views/common/_responsible_user.html.erb new file mode 100644 index 0000000..06473ee --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_responsible_user.html.erb @@ -0,0 +1,10 @@ +<% if responsible_user.assigned_to %> +

<%= l(:label_crm_assigned_to) %>

+
+
    +
  • + <%= (avatar(responsible_user.assigned_to, :size => "16").to_s + link_to_user(responsible_user.assigned_to, :class => 'user').to_s).html_safe %> +
  • +
+
+<% end %> diff --git a/plugins/redmine_contacts/app/views/common/_sidebar.html.erb b/plugins/redmine_contacts/app/views/common/_sidebar.html.erb new file mode 100644 index 0000000..7d037bb --- /dev/null +++ b/plugins/redmine_contacts/app/views/common/_sidebar.html.erb @@ -0,0 +1,15 @@ +<%= call_hook(:view_contacts_sidebar_top) %> + +

<%= l(:label_crm_module_plural) %>

+<% if User.current.allowed_to?(:view_contacts, @project, :global => true) %> +<%= link_to l(:label_contact_plural), { :controller => 'contacts', :action => 'index', :project_id => @project, :set_filter => 1} %> + | + <% end %> +<% if User.current.allowed_to?(:view_deals, @project, :global => true) %> +<%= link_to l(:label_deal_plural), { :controller => 'deals', :action => 'index', :project_id => @project, :set_filter => 1} %> + | +<% end %> + +<%= link_to l(:label_crm_note_plural), { :controller => 'contacts', :action => 'contacts_notes', :project_id => @project} %> + +<%= call_hook(:view_contacts_sidebar_bottom) %> diff --git a/plugins/redmine_contacts/app/views/contacts/_attributes.html.erb b/plugins/redmine_contacts/app/views/contacts/_attributes.html.erb new file mode 100644 index 0000000..918851c --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_attributes.html.erb @@ -0,0 +1,74 @@ +
+
+ <%- if ContactsSetting.vcard? -%> + <%= link_to 'vCard', contact_path(@contact, :format => :vcf) %> + <%- end -%> +
+

<%= if !@contact.is_company then l(:label_contact) else l(:label_crm_company) end %>

+ + + <%= call_hook(:view_contacts_sidebar_attributes_top) %> + + + + <% if !@contact.job_title.blank? %> + + <% end %> + <% if !@contact.is_company %> + + <% end %> + + + <% unless @contact.address.blank? %> + + <% end %> + + + + + + + + + + + + + + <% if !@contact.skype_name.blank? %> + + + + + <% end %> + <% if !@contact.birthday.blank? %> + + + <% end %> + <% @contact.custom_field_values.compact.each do |custom_value| %> + <% if !custom_value.value.blank? %> + + <% end %> + <% end %> + + <% if @contact.assigned_to %> + + <% end %> + + <%= call_hook(:view_contacts_sidebar_attributes_bottom) %> + + + +
diff --git a/plugins/redmine_contacts/app/views/contacts/_company_contacts.html.erb b/plugins/redmine_contacts/app/views/contacts/_company_contacts.html.erb new file mode 100644 index 0000000..fad6d90 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_company_contacts.html.erb @@ -0,0 +1,13 @@ +<% @company_contacts = @contact.company_contacts.visible.uniq %> +<% if @contact.is_company %> +
+
+ <%= link_to_if_authorized l(:label_crm_add_contact), {:controller => 'contacts', :action => 'new', :project_id => @project, :contact => {:company => @contact.name}} %> +
+

<%= l(:label_contact_plural) %>

+ + <%= render :partial => 'common/contact_data', :collection => @company_contacts %> +
+
+ +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_contact_card.html.erb b/plugins/redmine_contacts/app/views/contacts/_contact_card.html.erb new file mode 100644 index 0000000..d3bb197 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_contact_card.html.erb @@ -0,0 +1,36 @@ +
+ + + + + + + + + + +
<%= contact_tag(contact_card, :type => "avatar", :size => 64) %> +

<%= contact_tag(contact_card, :type => "plain") %>

+

+ <%= h contact_card.job_title %> + <% if !contact_card.is_company %> + <%= " #{l(:label_crm_at_company)} " unless (contact_card.job_title.blank? or contact_card.company.blank?) %> + <% if contact_card.contact_company %> + <%= link_to contact_card.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => contact_card.contact_company.project(@project), :id => contact_card.contact_company.id } %> + <% else %> + <%= h contact_card.company %> + <% end %> + <% end %> +

+ +
+ <% if contact_card.phones.any? %> +

<%= contact_card.phones.first %>

+ <% end %> + + <% if contact_card.emails.any? %> + + <% end %> + <%= tag_links(contact_card.tag_list) %> +
+
diff --git a/plugins/redmine_contacts/app/views/contacts/_custom_field_form.html.erb b/plugins/redmine_contacts/app/views/contacts/_custom_field_form.html.erb new file mode 100644 index 0000000..9d74659 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_custom_field_form.html.erb @@ -0,0 +1,4 @@ +

<%= form.check_box :is_filter %>

+<% if (@custom_field.respond_to?(:format) && @custom_field.format.searchable_supported) || !@custom_field.respond_to?(:format) %> +

<%= form.check_box :searchable %>

+<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_form.html.erb b/plugins/redmine_contacts/app/views/contacts/_form.html.erb new file mode 100755 index 0000000..9d97156 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_form.html.erb @@ -0,0 +1,105 @@ +<%= back_url_hidden_field_tag %> +<%= error_messages_for 'contact' %> +
+ + + +

+ <%= avatar_to(@contact, :size => "64", :style => "vertical-align: middle;") %> + <%= link_to image_tag('delete.png'), {:controller => 'attachments', :action => 'destroy', :id => @contact.avatar}, + :data => {:confirm => l(:text_are_you_sure)}, + :method => :delete, + :class => 'delete', + :style => "vertical-align: middle;", + :title => l(:button_delete) unless @contact.avatar.blank? %> +

+

+ <%= label_tag l(:field_contact_avatar) %> + + + <%= file_field_tag 'dummy_file', + :size => 30, + :id => nil, + :class => 'file_selector', + :multiple => true, + :onchange => 'uploadAvatar(this);', + :data => { + :max_file_size => Setting.attachment_max_size.to_i.kilobytes, + :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)), + :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i, + :upload_path => uploads_path(:format => 'js'), + :description_placeholder => l(:label_optional_description) + } %> + +

+

<%= f.check_box(:is_company, :label => l(:field_contact_is_company), :onclick => "togglePerson(this)" ) %>

+

<%= f.text_field :first_name, :label => !@contact.is_company ? l(:field_contact_first_name) : l(:field_company_name), :required => true, :style => "width:90%;" %>

+
+ +

<%= f.text_field :middle_name, :label=>l(:field_contact_middle_name) %>

+

<%= f.text_field :last_name, :label=>l(:field_contact_last_name), :id => 'contact_last_name' %>

+

<%= f.text_field :company, :label=>l(:field_contact_company) -%>

+ <%= javascript_tag "observeAutocompleteField('contact_company', '#{escape_javascript auto_complete_companies_path}')" %> +

<%= f.text_field :birthday, :size => 12 %><%= calendar_for('contact_birthday') %>

+
+

<%= f.text_field :job_title, :label => !@contact.is_company ? l(:field_contact_job_title) : l(:field_company_field) %>

+ <% @contact.build_address if @contact.address.blank? %> + <%= f.fields_for(:address) do |a| %> + + <%= render :partial => 'common/address_form', :locals => {:f => a} %> + + + <% end %> + +
+

+ <%= f.text_field :phone, :label=>l(:field_contact_phone), :style => "width:90%;" -%> +
+ <%= l(:text_comma_separated) %> +

+
+ +

+ <%= f.text_field 'email', :label=>l(:field_contact_email), :style => "width:90%;" -%> +
+ <%= l(:text_comma_separated) %> +

+ +

<%= f.text_field 'website', :label=>l(:field_contact_website) -%>

+

<%= f.text_field 'skype_name', :label=>l(:field_contact_skype) -%>

+ <% @contact.custom_field_values.each do |value| %> +

"> + <%= custom_field_tag_with_label :contact, value %> +

+ <% end -%> +

<%= f.text_area :background , :cols => 80, :rows => 8, :class => 'wiki-edit', :label=>l(:field_contact_background) %>

+ <%= wikitoolbar_for 'contact_background' %> + +

+ <%= label_tag l(:label_crm_tags_plural) %> + <%= render :partial => "contacts_tags/tags_form" %> +

+ + <% if @project %> +

<%= f.select :assigned_to_id, (@project.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true, :label => l(:label_crm_assigned_to) %>

+ <% end %> + +

<%= f.select :visibility, collection_for_visibility_select, :include_blank => false, :label => l(:label_crm_contacts_visibility) %>

+ + + +
diff --git a/plugins/redmine_contacts/app/views/contacts/_form_tags.html.erb b/plugins/redmine_contacts/app/views/contacts/_form_tags.html.erb new file mode 100644 index 0000000..1068c6c --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_form_tags.html.erb @@ -0,0 +1,24 @@ +
+<%= tag_links(@contact.tag_list) %> +<% if authorize_for('contacts', 'update') %> + + <%= link_to l(:label_crm_edit_tags), {}, :onclick => "$('#edit_tags_form').show(); $('#tags_data').hide(); return false;", :id => 'edit_tags_link' %> + +<% end %> +
+ + diff --git a/plugins/redmine_contacts/app/views/contacts/_list.html.erb b/plugins/redmine_contacts/app/views/contacts/_list.html.erb new file mode 100644 index 0000000..f9d276c --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_list.html.erb @@ -0,0 +1,44 @@ +<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %> +<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> +<%= hidden_field_tag 'project_id', @project.id if @project %> +
+ + + + + <% @query.columns.each do |column| %> + <%= Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? column_header(@query, column) : column_header(column) %> + <% end %> + + + + <% previous_group = false %> + + <% @contacts.each do |contact| -%> + <% if @query.grouped? && (group = @query.group_by_column.value(contact)) != previous_group %> + <% reset_cycle %> + + + + <% previous_group = group %> + <% end %> + + + + + + <% @query.columns.each do |column| %><%= content_tag 'td', column_content(column, contact), :class => column.css_classes %><% end %> + + <% end %> + +
+ <%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleCRMIssuesSelection(this); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> +
+   + <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, contact) %> (<%= @contact_count_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> +
<%= check_box_tag("selected_contacts[]", contact.id, false, :id => nil) %>
+ +
+<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_list_cards.html.erb b/plugins/redmine_contacts/app/views/contacts/_list_cards.html.erb new file mode 100644 index 0000000..ffa5834 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_list_cards.html.erb @@ -0,0 +1,51 @@ +<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %> +<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> +<%= hidden_field_tag 'project_id', @project.id if @project %> +
+ <% i = 0 %> + <% split_on = (@contacts.size / 2.0).ceil - 1 %> + <% @contacts.each do |contact| %> + <% @contact = contact %> +
+ + + + + + + + +
<%= contact_tag(contact, :type => "avatar", :size => 64) %> +

<%= contact_tag(contact, :type => "plain") %>

+

+ <%= h contact.job_title %> + <% if !contact.is_company %> + <%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %> + <% if contact.contact_company %> + <%= link_to contact.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => contact.contact_company.project(@project), :id => contact.contact_company.id } %> + <% else %> + <%= h contact.company %> + <% end %> + <% end %> +

+ <% if contact.phones.any? %> +

<%= contact.phones.first %>

+ <% end %> + + <% if contact.emails.any? %> + + <% end %> + <%= tag_links(contact.tag_list) %> + +
+
+ + <% if i == split_on -%> +
+ <% end -%> + <% i += 1 -%> + <% end -%> +
+
+ +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_list_excerpt.html.erb b/plugins/redmine_contacts/app/views/contacts/_list_excerpt.html.erb new file mode 100644 index 0000000..70cfae0 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_list_excerpt.html.erb @@ -0,0 +1,54 @@ +<%= form_tag({}, :data => {:cm_url => context_menu_contacts_path}) do %> +<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> +<%= hidden_field_tag 'project_id', @project.id if @project %> +
+ + + <% previous_group = false %> + <% @contacts.each do |contact| %> + <% if @query.grouped? && (group = @query.group_by_column.value(contact)) != previous_group %> + <% reset_cycle %> + + + + <% previous_group = group %> + <% end %> + + + + + + + + <% end %> + +
+   + <%= group.blank? ? 'None' : column_content(@query.group_by_column, contact) %> (<%= @contact_count_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> +
+ <%= check_box_tag "selected_contacts[]", contact.id, false, :onclick => "toggleContact(event, this);" %> + + <%= link_to avatar_to(contact, :size => "32"), contact_path(contact, :project_id => @project), :id => "avatar" %> + +

<%= link_to contact.name, contact_path(contact, :project_id => @project) %>

+

+ <%= link_to contact.website, contact.website_address, :class => 'external', :only_path => true unless !contact.is_company %> + <%= mail_to contact.emails.first unless contact.is_company%> +

<%= contact.phones.first %>
+

+
+
+ <%= contact.job_title %> + <% if !contact.is_company %> + <%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %> + <%= contact.company %> + <% end %> +
+
+ <%= tag_links(contact.tag_list) %> + <%# tag_links(RedmineCrm::TagList.from(contact.cached_tag_list)) %> +
+
+
+ <% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_name_observer.html.erb b/plugins/redmine_contacts/app/views/contacts/_name_observer.html.erb new file mode 100644 index 0000000..f99322a --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_name_observer.html.erb @@ -0,0 +1,37 @@ + + + +<%# observe_field("contact_first_name", + :frequency => 1, + :update => 'duplicates', + :url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact}, + :with => "$('contact_form').serialize()") %> + +<%# observe_field("contact_middle_name", + :frequency => 1, + :update => 'duplicates', + :url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact}, + :with => "$('contact_form').serialize()") %> + +<%# observe_field("contact_last_name", + :frequency => 1, + :update => 'duplicates', + :url => {:controller => 'contacts_duplicates', :action => 'duplicates', :project_id => @project, :contact_id => @contact}, + :with => "$('contact_form').serialize()") %> diff --git a/plugins/redmine_contacts/app/views/contacts/_new_modal.html.erb b/plugins/redmine_contacts/app/views/contacts/_new_modal.html.erb new file mode 100644 index 0000000..0527fbd --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_new_modal.html.erb @@ -0,0 +1,15 @@ +

<%=l(:label_crm_contact_new)%>

+ +<%= labelled_form_for @contact, :url => project_contacts_path(@project), :remote => true do |f| %> +<%= hidden_field_tag :contact_field_name, params[:contact_field_name] %> +<%= hidden_field_tag :contacts_is_company, params[:contacts_is_company] %> +<%= render :partial => 'contacts/form', :locals => { :f => f } %> +

+ <%= submit_tag l(:button_create), :name => nil %> + <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %> +

+<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_notes.html.erb b/plugins/redmine_contacts/app/views/contacts/_notes.html.erb new file mode 100644 index 0000000..215b850 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_notes.html.erb @@ -0,0 +1,29 @@ +<% + extend Redmine::Pagination + source_id_cond = @contact.is_company ? Contact.visible.where(:company => @contact.first_name).map(&:id) << @contact.id : @contact.id + @note = Note.new(:created_on => Time.now) + + scope = Note.where({:source_id => source_id_cond, :source_type => 'Contact'}).includes(:attachments).order("#{Note.table_name}.created_on DESC") + @notes_pages = Redmine::Pagination::Paginator.new(scope.count, 20, params['page']) + @notes = scope.limit(20).offset(@notes_pages.offset) +%> +<% if authorize_for(:notes, :create) %> +
+ <%= render :partial => 'notes/add', :locals => {:note_source => @contact} %> +
+<% end %> + +<% if @contact.is_public? || authorize_for(:notes, :show) %> +
+
+ <%= render :partial => 'notes/note_item', :collection => @notes, :locals => {:show_info => @contact.is_company, :note_source => @contact} %> + <%= pagination_links_full @notes_pages %> +
+
+ +<% other_formats_links do |f| %> + <% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> + <%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %> +<% end %> + +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/_tag_list.html.erb b/plugins/redmine_contacts/app/views/contacts/_tag_list.html.erb new file mode 100644 index 0000000..f6f006b --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_tag_list.html.erb @@ -0,0 +1,10 @@ +
+ + <%= render :partial => 'contacts/tags_item', :collection => tag_list, :locals => {:is_note => false} %> + + <% if editable && authorize_for('contacts', 'update') %> + + <%= link_to l(:label_crm_edit_tags), {}, :onclick => "$('#edit_tags_form').show(); $('#tags_data').hide(); return false;", :id => 'edit_tags_link' %> + + <% end %> +
diff --git a/plugins/redmine_contacts/app/views/contacts/_tags_cloud.html.erb b/plugins/redmine_contacts/app/views/contacts/_tags_cloud.html.erb new file mode 100644 index 0000000..2388886 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_tags_cloud.html.erb @@ -0,0 +1,7 @@ +
+ +

<%= l(:label_crm_tags_plural) %>

+ <%= safe_join(tags_cloud.map{|tag| tag_link(tag.name, :count => tag.count)}, ContactsSetting.monochrome_tags? ? ', ' : ' ').html_safe %> +
+ +
diff --git a/plugins/redmine_contacts/app/views/contacts/_tags_item.html.erb b/plugins/redmine_contacts/app/views/contacts/_tags_item.html.erb new file mode 100644 index 0000000..205dcff --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/_tags_item.html.erb @@ -0,0 +1,19 @@ +<% + html_options = {:id => "tag_#{tags_item.id}", + :style => "background-color: #{tags_item.color_name}"} + taggable_type ||= 'contacts' + + tag_url = {:controller => taggable_type, + :action => 'index', + :set_filter => 1, + :fields => [:tags], + :values => {:tags => [tags_item.name]}, + :operators => {:tags => '='}} +%> + + <%- if !is_note -%> + <%= link_to tags_item.name + "#{"(" + tags_item.count.to_s + ")" if tags_item.count > 0}", {:project_id => @project}.merge!(tag_url), html_options %> + <%- else -%> + <%= link_to tags_item.name, {:controller => "contacts", :action => "contacts_notes", :project_id => @project, :tag => tags_item.name}, html_options %> + <%- end -%> + diff --git a/plugins/redmine_contacts/app/views/contacts/bulk_edit.html.erb b/plugins/redmine_contacts/app/views/contacts/bulk_edit.html.erb new file mode 100644 index 0000000..076b62e --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/bulk_edit.html.erb @@ -0,0 +1,130 @@ +

<%= l(:label_crm_bulk_edit_selected_contacts) %>

+ + +
+
    + <% @contacts.each do |contact| %> +
  • + <%= avatar_to contact, :size => "16" %> + <%= link_to_source contact %>, + <%= h contact.job_title %> + <%= " #{l(:label_crm_at_company)} " unless (contact.job_title.blank? or contact.company.blank?) %> + <% if contact.contact_company %> + <%= link_to contact.contact_company.name, {:controller => 'contacts', :action => 'show', :id => contact.contact_company.id } %> + <% else %> + <%= h contact.company %> + <% end %> + <%= "(#{l(:field_contact_tag_names)}: #{contact.tag_list})" if contact.tags.any? %> +
  • + <% end %> +
+
+ + +<%= form_tag(:action => 'bulk_update', :project_id => @project) do %> +<%= @contacts.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %> +
+
+<%= l(:label_change_properties) %> + +
+

+ + <%= text_field_tag('contact[company]', '') %> + <%= javascript_tag "observeAutocompleteField('contact_company', '#{escape_javascript auto_complete_companies_path(:project_id => @project)}')" %> +

+ + <% @custom_fields.each do |custom_field| %> +

<%= custom_field_tag_for_bulk_edit('contact', custom_field, @projects) %>

+ <% end %> + +

+ + <%= select_tag('contact[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_nobody), :value => 'none') + + options_from_collection_for_select(@assignables, :id, :name)) %> +

+ +

+ + <%= select_tag 'contact[visibility]', options_for_select(collection_for_visibility_select) %> +

+ + +
+ +
+

+ + <%= text_field_tag('contact[job_title]', '') %> +

+ +

+ + <%= text_field_tag 'contact[address_attributes][city]' -%> +

+

+ + <%= text_field_tag 'contact[address_attributes][region]' -%> +

+

+ + <%= select_tag 'contact[address_attributes][country_code]', options_for_select(l(:label_crm_countries).map{|k, v| [v, k]}), :include_blank => true -%> +

+ +
+ + + + +
+ +
+<%= l(:label_crm_tags_plural) %> + +
+

+ + <%= text_field_tag 'add_tag_list', '', :size => 10, :class => "hol" %><%= tagsedit_with_source_for("#add_tag_list", auto_complete_contact_tags_path(:project_id => @project)) %> +

+ +
+ +
+

+ + <%= text_field_tag 'delete_tag_list', '', :label => :field_contact_tag_names, :size => 10, :class => "hol" %><%= tagsedit_with_source_for("#delete_tag_list", auto_complete_contact_tags_path(:project_id => @project)) %> +

+
+
+ +<% if @add_projects.any? %> +
+<%= l(:label_project_plural) %> +
+

+ + <%= select_tag 'add_projects_list[]', content_tag('option', l(:label_no_change_option), :value => '', :selected => 'selected') + project_tree_options_for_select(@add_projects), :multiple => false %> +

+
+ +
+

+ + <%= select_tag 'delete_projects_list[]', content_tag('option', l(:label_no_change_option), :value => '', :selected => 'selected') + project_tree_options_for_select(@add_projects), :multiple => false %> +

+
+ +
+<% end %> + + + +
<%= l(:field_notes) %> +<%= text_area_tag 'note[content]', '', :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'note_content' %> +
+
+ +

<%= submit_tag l(:button_submit) %>

+<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/contacts_notes.html.erb b/plugins/redmine_contacts/app/views/contacts/contacts_notes.html.erb new file mode 100644 index 0000000..95b51bc --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/contacts_notes.html.erb @@ -0,0 +1,64 @@ +<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> +
+<% if !@tag %> + <%= form_tag(filtered_params, :id => "query_form", :method => :get) do %> + <%= hidden_field_tag('project_id', @project.to_param) if @project %> +

+ + <%= l(:label_crm_contact_all_note_plural) %> + + + <%= text_field_tag(:search_note, params[:search_note], :autocomplete => "off", :class => "live_search_field", :placeholder => l(:label_crm_contact_search) ) %> + + + + +

+ <% end %> +<% else %> +

<%= "#{l(:label_crm_contact_tag)}(#{@notes_pages.item_count}): #{tag_links(@tag)}".html_safe %>

+<% end %> +
+ +
+ <%= render :partial => 'notes/notes_list' %> +
+ +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %> + <%= f.link_to 'CSV', :url => filtered_params %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> +

<%= l(:label_crm_note_plural) %>

+
+ <% collection_for_note_types_select.each do |note_type| %> + <%= radio_button_tag "note_type", note_type[1], filtered_params[:type_id].to_s == note_type[1].to_s, {:onchange => "document.location='#{url_for(filtered_params.merge(:type_id => note_type[1]))}';", :id => "note_type_#{note_type[1]}" }%> + <%= label_tag "note_type_#{note_type[1]}", note_type[0] %> +
+ <% end %> +
+

<%= l(:label_crm_tags_plural) %>

+
+ <%= @tags.map{|tag| content_tag(:span, link_to(tag.name, {:controller => "contacts", :action => "contacts_notes", :project_id => @project, :tag => tag.name}), {}.merge(ContactsSetting.monochrome_tags? ? {:class => "tag-label"} : {:class => "tag-label-color", :style => "background-color: #{tag_color(tag.name)}"}))}.join(' ').html_safe %> +
+ <%= render :partial => 'common/recently_viewed' %> +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/context_menu.html.erb b/plugins/redmine_contacts/app/views/contacts/context_menu.html.erb new file mode 100644 index 0000000..5c4f43d --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/context_menu.html.erb @@ -0,0 +1,35 @@ +
    + <%= call_hook(:view_contacts_context_menu_start, {:contacts => @contacts, :can => @can, :back => @back }) %> + + <% unless @contact.nil? %> +
  • <%= context_menu_link l(:button_edit), {:controller => 'contacts', :action => 'edit', :id => @contact, :project_id => @project}, :class => 'icon-edit', :disabled => !@can[:edit] %>
  • + <% if User.current.logged? %> +
  • <%= watcher_link(@contact, User.current) %>
  • + <% end %> + + <% if !@project.nil? %> +
  • <%= context_menu_link l(:label_crm_deal_new), {:controller => 'deals', :action => 'new', :project_id => @project, :contact_id => @contact}, + :class => 'icon-add-deal', :disabled => !@can[:create_deal] %>
  • + <% if @contact.is_company? %> +
  • <%= context_menu_link l(:label_crm_add_contact), {:controller => 'contacts', :action => 'new', :project_id => @project, :contact => {:company => @contact.name}}, + :class => 'icon-company-contact', :disabled => !@can[:create] %>
  • + <% end %> + <% end %> + <% else %> +
  • <%= context_menu_link l(:button_edit), {:controller => 'contacts', :action => 'bulk_edit', :ids => @contacts.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
  • + <% end %> +
  • <%= context_menu_link l(:label_crm_send_mail), {:controller => 'contacts', :action => 'edit_mails', :ids => @contacts.collect(&:id), :project_id => @project}, :class => 'icon-email', :disabled => !@can[:send_mails] %>
  • + + <%= call_hook(:view_contacts_context_menu_before_delete, {:contacts => @contacts, :can => @can, :back => @back }) %> + +
  • <%= context_menu_link l(:button_delete), {:controller => 'contacts', :action => 'bulk_destroy', :ids => @contacts.collect(&:id), :project_id => @project}, + :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon-del', :disabled => !@can[:delete] %>
  • + <% if !@contact && Redmine::VERSION.to_s >= '3.3' %> +
  • + <%= context_menu_link l(:button_filter), _project_contacts_path(@project, :set_filter => 1, :ids => @contacts.map(&:id).join(',')), + :class => 'icon-list' %> +
  • + <% end %> + <%= call_hook(:view_contacts_context_menu_end, {:contacts => @contacts, :can => @can, :back => @back }) %> +
diff --git a/plugins/redmine_contacts/app/views/contacts/create.js.erb b/plugins/redmine_contacts/app/views/contacts/create.js.erb new file mode 100644 index 0000000..0361793 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/create.js.erb @@ -0,0 +1,13 @@ +hideModal(); +<% field_id = params[:contact_field_name].to_s.gsub("[", "_").gsub("]", "") -%> +$('select#<%= field_id %>') + .append($("") + .attr("value",'<%= @contact.id %>') + .attr("selected",'selected') + .text('<%= @contact.name %>')); +$('input#<%= field_id %>').val('<%= @contact.id %>'); +$('#<%= field_id %>_selected_contact').text('<%= @contact.name %>'); +$('#<%= field_id %>_selected_contact').show(); +$('#<%= field_id %>_selected_contact').scrollTop( 0 ); +$('input#<%= field_id %>').hide(); +$('#<%= field_id %>_edit_link').show(); diff --git a/plugins/redmine_contacts/app/views/contacts/edit.html.erb b/plugins/redmine_contacts/app/views/contacts/edit.html.erb new file mode 100755 index 0000000..56e7dae --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/edit.html.erb @@ -0,0 +1,28 @@ +
+ <%= link_to_if_authorized l(:label_crm_merge_duplicate_plural), {:controller => 'contacts_duplicates', :action => 'index', :project_id => @project, :contact_id => @contact}, :class => 'icon icon-merge' unless @contact.new_record? %> +
+ +

<%= l(:label_crm_contact_edit_information) %>

+ +<%= labelled_form_for :contact, @contact, + :url => {:action => 'update', :project_id => @project, :id => @contact}, + :html => { :multipart => true, :method => :put, :id => "contact_form" } do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= render :partial => 'name_observer' %> + <%= submit_tag l(:button_save) -%> +<% end -%> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + <%= render :partial => 'contacts_duplicates/duplicates' %> +
+ <%= render :partial => 'contacts_projects/related' %> +
+<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag 'attachments' %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + <%= robot_exclusion_tag %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/edit_mails.html.erb b/plugins/redmine_contacts/app/views/contacts/edit_mails.html.erb new file mode 100644 index 0000000..bc57f5a --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/edit_mails.html.erb @@ -0,0 +1,64 @@ +

<%= l(:label_crm_bulk_send_mail_selected_contacts) %>

+ + +
+
    + <% @contacts.each do |contact| %> +
  • + <%= avatar_to contact, :size => "16" %> + <%= link_to_source contact %> + <%= "(#{contact.job_title}) " unless contact.job_title.blank? %> + - <%= contact.emails.first %> +
  • + <% end %> +
+
+ + +<%= form_for(:email_message, :url => {:action => 'send_mails', :project_id => @project}, :html => {:multipart => true, :id => 'message-form'}) do %> +<%= @contacts.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %> + +
+

+ + <%= text_field_tag('from', "#{User.current.name} <#{User.current.mail}>", :style => "width: 98%;") %> + <%= link_to "#{l(:label_crm_contacts_cc)}/#{l(:label_crm_contacts_bcc)}", '#' , :onclick => "$('#mail_cc').show();$(this).hide();" %> +

+ + +

+ + <%= text_field_tag('subject', '', :id => "subject", :style => "width: 98%;") %> +

+

+ + <%= text_area_tag 'message-content', '', :cols => 60, :rows => 10, :class => 'wiki-edit' %> + <%= l(:text_email_macros, :macro => "%%NAME%%, %%LAST_NAME%%, %%MIDDLE_NAME%%, %%FULL_NAME%%, %%COMPANY%%, %%DATE%%, %%[Custom field]%%") %> +

+ <%= wikitoolbar_for 'message-content' %> + +

<%= label_tag('attachments[1][file]', l(:label_attachment_plural))%><%= render :partial => 'attachments/form' %>

+
+ +

+ <%= submit_tag l(:button_submit) %> + <%= preview_link({ :controller => 'contacts', :action => 'preview_email' }, 'message-form') %> +

+ +<% end %> + +
+ +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/index.api.rsb b/plugins/redmine_contacts/app/views/contacts/index.api.rsb new file mode 100644 index 0000000..c7fd0dc --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/index.api.rsb @@ -0,0 +1,53 @@ +api.array :contacts, api_meta(:total_count => @contacts_count, :offset => @offset, :limit => @limit) do + @contacts.each do |contact| + api.contact do + api.id contact.id + api.avatar(:attachment_id => contact.avatar.id) if contact.avatar + api.is_company contact.is_company + api.first_name contact.first_name + api.last_name contact.last_name + api.middle_name contact.middle_name + api.company contact.company + api.website contact.website + api.skype_name contact.skype_name + api.birthday contact.birthday + api.job_title contact.job_title + api.background contact.background + api.author(:id => contact.author_id, :name => contact.author.name) unless contact.author.nil? + api.assigned_to(:id => contact.assigned_to_id, :name => contact.assigned_to.name) unless contact.assigned_to.nil? + + api.address do + api.full_address contact.address + api.street contact.street1 + api.city contact.city + api.region contact.region + api.country contact.country + api.country_code contact.address.country_code unless contact.address.blank? + api.postcode contact.postcode + end + + api.array :phones do + contact.phones.each do |phone| + api.phone do + api.number phone + end + end + end if contact.phones.any? + + api.array :emails do + contact.emails.each do |email| + api.email do + api.address email + end + end + end if contact.emails.any? + + + api.tag_list contact.tag_list + render_api_custom_values contact.custom_field_values, api + + api.created_on contact.created_on + api.updated_on contact.updated_on + end + end +end diff --git a/plugins/redmine_contacts/app/views/contacts/index.html.erb b/plugins/redmine_contacts/app/views/contacts/index.html.erb new file mode 100755 index 0000000..70f36d1 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/index.html.erb @@ -0,0 +1,203 @@ +<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> +
+ <% if !@query.new_record? && @query.editable_by?(User.current) %> + <%= link_to l(:button_contacts_edit_query), edit_crm_query_path(@query, :object_type => "contact"), :class => 'icon icon-edit' %> + <%= link_to l(:button_contacts_delete_query), crm_query_path(@query, :object_type => "contact"), :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' %> + <% end %> + <%= link_to_if_authorized l(:label_crm_contact_new), {:controller => 'contacts', :action => 'new', :project_id => @project}, :class => 'icon icon-add' %> + <%= link_to_if_authorized l(:label_crm_import), {:controller => 'contact_imports', :action => 'new', :project_id => @project}, :class => 'icon icon-import', :id => 'import_from_csv' %> + <%= call_hook(:view_contacts_action_menu) %> +
+ + +<% html_title(@query.new_record? ? l(:label_contact_plural) : @query.name) %> + + +<%= form_tag({ :controller => 'contacts', :action => 'index', :project_id => @project }, :method => :get, :id => 'query_form') do %> + + +

+ + <%= @query.new_record? ? l(:label_contact_plural) : h(@query.name) %> + + + + <%= text_field_tag(:search, params[:search], :autocomplete => "off", :class => "live_search_field", :placeholder => l(:label_crm_contact_search) ) %> + + + + + <%= tag_links(@filter_tags) %> + + +

+ + <%= hidden_field_tag 'set_filter', '1' %> + <%= hidden_field_tag 'object_type', 'contact' %> +
+
"> + <%= l(:label_filter_plural) %> +
"> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
+
+ + +
+ +

+ <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> + <% if @query.new_record? && User.current.allowed_to?(:save_contacts_queries, @project, :global => true) %> + <%= link_to_function l(:button_save), + "$('#query_form').attr('action', '#{ @project ? new_project_crm_query_path(@project) : new_crm_query_path }'); submit_query_form('query_form')", + :class => 'icon icon-save' %> + + <% end %> +

+ +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +
+ <% if @contacts.empty? %> +

<%= l(:label_no_data) %>

+ <% else %> + <%= render :partial => contacts_list_style %> + <%= pagination_links_full @contacts_pages, @contacts_count %> + <% end %> +
+ <% if User.current.allowed_to?(:export_contacts, @project, :global => true) %> + <% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => filtered_params.merge(:key => User.current.rss_key) %> + <% if contacts_list_style == 'list' %> + <%= f.link_to 'CSV', :url => filtered_params, :onclick => "showModal('csv-export-options', '350px'); return false;" %> + <% else %> + <%= f.link_to 'CSV', :url => filtered_params %> + <% end %> + <%- if ContactsSetting.vcard? -%> + <%= f.link_to 'VCF', :url => filtered_params %> + <%- end -%> + <%- if ContactsSetting.spreadsheet? -%> + <%= f.link_to 'XLS', :url => filtered_params %> + <%- end -%> + <% end %> + + <% end %> +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + <%= render :partial => 'tags_cloud', :object => @tags %> + <%= render_sidebar_crm_queries('contact') %> + <%= render :partial => 'notes/last_notes', :object => @last_notes %> + <%= render :partial => 'common/recently_viewed' %> + + <%= call_hook(:view_contacts_sidebar_contacts_list_bottom) %> + +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => l(:label_contact_plural)) %> + +<% end %> + +<% if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? %> + <%= context_menu %> +<% else %> + <%= context_menu url_for( {:controller => "contacts", :action => "context_menu"} ) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/load_tab.js.erb b/plugins/redmine_contacts/app/views/contacts/load_tab.js.erb new file mode 100644 index 0000000..efedc6e --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/load_tab.js.erb @@ -0,0 +1,5 @@ +<% + @tab = params[:tab_name] + @partial = params[:partial] +%> +$('#tab-placeholder-<%= @tab %>').html("<%= j(render :partial => @partial) %>") diff --git a/plugins/redmine_contacts/app/views/contacts/new.html.erb b/plugins/redmine_contacts/app/views/contacts/new.html.erb new file mode 100755 index 0000000..51fc0be --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/new.html.erb @@ -0,0 +1,23 @@ +

<%= l(:label_crm_contact_new) %>

+ +<%= labelled_form_for :contact, @contact, :url => {:action => 'create', :project_id => @project}, :html => { :multipart => true, :id => 'contact_form'} do |f| %> + <%= render :partial => 'form', :locals => {:f => f} %> + <%= render :partial => 'name_observer' %> + <%= submit_tag l(:button_save) -%> + <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> +<% end -%> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + <%= render :partial => 'contacts_duplicates/duplicates' %> + <%= render :partial => 'contacts_vcf/load' %> +<% end %> + + + +<% content_for :header_tags do %> + <%= javascript_include_tag 'attachments' %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + <%= robot_exclusion_tag %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts/new.js.erb b/plugins/redmine_contacts/app/views/contacts/new.js.erb new file mode 100644 index 0000000..934d679 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/new.js.erb @@ -0,0 +1,9 @@ +$('#ajax-modal').html('<%= escape_javascript(render :partial => 'contacts/new_modal') %>'); +$('#ajax-modal #contact_data .extended').hide(); +$('#ajax-modal #contact_data #show_details_link').show(); +showModal('ajax-modal', '800px'); + +$('#new_contact').submit( function(event) { + $('.file_selector').val(''); + event.preventDefault(); +}); diff --git a/plugins/redmine_contacts/app/views/contacts/show.api.rsb b/plugins/redmine_contacts/app/views/contacts/show.api.rsb new file mode 100644 index 0000000..5338932 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/show.api.rsb @@ -0,0 +1,110 @@ +api.contact do + api.id @contact.id + api.avatar(:attachment_id => @contact.avatar.id) if @contact.avatar + api.is_company @contact.is_company + + api.first_name @contact.first_name + api.last_name @contact.last_name + api.middle_name @contact.middle_name + api.company @contact.company + api.website @contact.website + api.skype_name @contact.skype_name + api.birthday @contact.birthday + api.job_title @contact.job_title + api.background @contact.background + api.author(:id => @contact.author_id, :name => @contact.author.name) unless @contact.author.nil? + api.assigned_to(:id => @contact.assigned_to_id, :name => @contact.assigned_to.name) unless @contact.assigned_to.nil? + + api.address do + api.full_address @contact.address + api.street @contact.street1 + api.city @contact.city + api.region @contact.region + api.country @contact.country + api.country_code @contact.address.country_code unless @contact.address.blank? + api.postcode @contact.postcode + end + + api.array :phones do + @contact.phones.each do |phone| + api.phone do + api.number phone + end + end + end if @contact.phones.any? + + api.array :emails do + @contact.emails.each do |email| + api.email do + api.address email + end + end + end if @contact.emails.any? + + api.tag_list @contact.tag_list + render_api_custom_values @contact.custom_field_values, api + api.created_on @contact.created_on + api.updated_on @contact.updated_on + + + api.array :projects do + @contact.projects.each do |project| + api.project(:id => project.id, :name => project.name) + end + end if @contact.projects.present? + + if authorize_for(:notes, :show) + api.array :notes do + @contact.notes.each do |note| + api.note do + api.id note.id + api.content note.content + api.type_id note.type_id + api.author(:id => note.author_id, :name => note.author.name) unless note.author.nil? + api.created_on note.created_on + api.updated_on note.updated_on + end + end + end if include_in_api_response?('notes') && @contact.notes.present? && User.current.allowed_to?(:view_contacts, @project) + end + + api.array :contacts do + @contact.company_contacts.each do |contact| + api.contact(:id => contact.id, :name => contact.name ) + end + end if include_in_api_response?('contacts') && @contact.company_contacts.present? + api.array :deals do + (@contact.related_deals + @contact.deals).each do |deal| + api.deal do + api.id deal.id + api.price deal.price + api.currency deal.currency + api.price_type deal.price_type + api.name deal.name + api.project(:id => deal.project.id, :name => deal.project.name) + api.status(:id => deal.status.id, :name => deal.status.name) + api.background deal.background + api.created_on deal.created_on + api.updated_on deal.updated_on + end + end + end if include_in_api_response?('deals') && (@contact.related_deals + @contact.deals).present? && User.current.allowed_to?(:view_deals, @project) + + if authorize_for(:issues, :show) + api.array :issues do + @contact.issues.each do |issue| + api.issue do + api.id issue.id + api.subject issue.subject + api.status(:id => issue.status_id, :name => issue.status.name) unless issue.status.nil? + api.due_date issue.due_date + api.created_on issue.created_on + api.updated_on issue.updated_on + end + end + end if include_in_api_response?('issues') && @contact.issues.present? && User.current.allowed_to?(:view_issues, @project) + end + + call_hook(:api_contacts_show) + +end diff --git a/plugins/redmine_contacts/app/views/contacts/show.html.erb b/plugins/redmine_contacts/app/views/contacts/show.html.erb new file mode 100755 index 0000000..245bef2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts/show.html.erb @@ -0,0 +1,86 @@ +<%= error_messages_for 'contact', 'note' %> + +<% html_title "#{l(:label_contact)} ##{@contact.id}: #{@contact.name}" %> + +
+ <%= call_hook(:view_contacts_before_actions, :contact => @contact, :project => @project) %> + <%= link_to l(:label_profile), user_path(@contact.redmine_user), :class => 'icon icon-user' unless @contact.redmine_user.blank? %> + <%= link_to(l(:button_create), {:controller => 'users', :action => 'new_from_contact', :contact_id => @contact.id, :id => 'current'}, :class => 'icon icon-user') if (User.current.admin? && @contact.redmine_user.blank? && !@contact.email.blank?) %> + <%= link_to_if_authorized l(:label_crm_send_mail), {:controller => 'contacts', :action => 'edit_mails', :ids => [@contact.id], :project_id => @project}, :class => 'icon icon-email' unless @contact.primary_email.blank? %> + <%= watcher_link(@contact, User.current) %> + <%= link_to l(:button_edit), {:controller => 'contacts', :action => 'edit', :project_id => @project, :id => @contact}, :class => 'icon icon-edit' if @contact.editable? %> + <%= link_to l(:button_delete), {:controller => 'contacts', :action => 'destroy', :project_id => @project, :id => @contact}, :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' if @contact.deletable? %> + <%= call_hook(:view_contacts_after_actions, :contact => @contact, :project => @project) %> +
+ +

<%= !@contact.is_company ? l(:label_contact) : l(:label_crm_company) %> #<%= @contact.id %>

+ +
+ + + + + <% if @contact.phones.any? || @contact.emails.any? %> + + <% end %> + +
<%= avatar_to(@contact, :size => "64", :full_size => true) %> +

<%= h @contact.name %>

+ <% if !@contact.is_company %> +

+ <%= h @contact.job_title %> + <%= " #{l(:label_crm_at_company)} " unless (@contact.job_title.blank? or @contact.company.blank?) %> + <% if @contact.contact_company %> + <%= link_to @contact.contact_company.name, {:controller => 'contacts', :action => 'show', :project_id => @contact.contact_company.project(@project), :id => @contact.contact_company.id } %> + <% else %> + <%= h @contact.company %> + <% end %> +

+ <% end %> + + <%= render :partial => 'form_tags', :object => @contact.tags, :locals => {:editable => true} %> + +
+
    + <% if @contact.phones.any? %> +
  • <%= @contact.phones.first %>
  • + <% end %> + + <% if @contact.emails.any? %> + + <% end %> +
+
+ <%= call_hook(:view_contacts_show_details_bottom, :contact => @contact) %> +
+ +<%= render_contact_tabs contact_tabs(@contact) %> + +<% content_for :sidebar do %> + + <%= render :partial => 'common/sidebar' %> + <%= render :partial => 'attributes' %> + <%= call_hook(:view_contacts_sidebar_after_attributes, :contact => @contact) %> + <%= render :partial => 'contacts_issues/issues', :locals => {:contact => @contact, :issues => @contact_issues} %> + <%= call_hook(:view_contacts_sidebar_after_tasks, :contact => @contact) %> + <%= render :partial => 'common/notes_attachments', :object => @contact.notes_attachments %> + <%= call_hook(:view_contacts_sidebar_after_notes_attachments, :contact => @contact) %> + <% if !@contact.background.blank? %> +

<%= l(:label_crm_background_info) %>

+
<%= textilizable(@contact, :background) %>
+ <% end %> + +
+ <%= render :partial => 'contacts_projects/related' %> +
+ + <%= render :partial => 'common/recently_viewed' %> + +<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@contact.name} - ##{@contact.id}") %> + +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_duplicates/_duplicates.html.erb b/plugins/redmine_contacts/app/views/contacts_duplicates/_duplicates.html.erb new file mode 100644 index 0000000..87e9d04 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_duplicates/_duplicates.html.erb @@ -0,0 +1,23 @@ +
+<% if @contact.duplicates.any? %> + <% if !@contact.new_record? %> +
+ <%= link_to_if_authorized l(:label_crm_merge_duplicate_plural), {:controller => 'contacts_duplicates', :action => 'index', :project_id => @project, :contact_id => @contact} %> +
+ <% end %> + +

<%= l(:label_crm_duplicate_plural) %>

+
    + <% @contact.duplicates.each do |contact| %> +
  • + + <%= avatar_to contact, :size => "16" %> + <%= link_to_source contact %> + <%= "(#{contact.job_title}) " unless contact.job_title.blank? %> +
  • + <% end %> +
+ + +<% end %> +
diff --git a/plugins/redmine_contacts/app/views/contacts_duplicates/_list.html.erb b/plugins/redmine_contacts/app/views/contacts_duplicates/_list.html.erb new file mode 100644 index 0000000..ce5b04d --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_duplicates/_list.html.erb @@ -0,0 +1,8 @@ +<% @contacts.each do |contact| %> +
  • + <%= radio_button_tag "duplicate_id", contact.id %> + <%= avatar_to contact, :size => "16" %> + <%= link_to_source contact %> + <%= "(#{contact.job_title}) " unless contact.job_title.blank? %> +
  • +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_duplicates/index.html.erb b/plugins/redmine_contacts/app/views/contacts_duplicates/index.html.erb new file mode 100644 index 0000000..4470ccd --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_duplicates/index.html.erb @@ -0,0 +1,54 @@ +<%= breadcrumb link_to(@contact.name, note_source_url(@contact)) %> + +
    + + + + + + +
    <%= link_to avatar_to(@contact, :size => "32"), note_source_url(@contact), :id => "avatar" %> +

    + <%= l(:label_crm_duplicate_for_plural) %>: <%= @contact.name %> +

    +

    + <%= h @contact.job_title %> + <%= " #{l(:label_crm_at_company)} " unless (@contact.job_title.blank? or @contact.company.blank?) %> + <% if @contact.is_company && @contact.contact_company %> + <%= link_to @contact.contact_company.name, note_source_url(@contact.contact_company) %> + <% else %> + <%= h @contact.company %> + <% end %> +

    + +
    +
    + +<%= form_tag({:controller => 'contacts_duplicates', :action => 'merge', :project_id => @project, :contact_id => @contact}) do %> +
    + <%= text_field_tag(:principal_search, params[:topic_search] , :autocomplete => "off", :placeholder => l(:label_crm_contact_search) ) %> + <%= javascript_tag "observeSearchfield('principal_search', 'contact_duplicates', '#{escape_javascript contacts_duplicates_search_path(:contact_id => @contact, :by_email => true)}')" %> + + <%= content_tag('div', l(:notice_merged_warning), :class => "flash warning") %> + +
      + <%= render :partial => 'list' %> +
    +
    + <%= submit_tag l(:label_crm_merge_duplicate_plural) %> +<% end %> + +<% html_title "#{l(:label_crm_duplicate_plural)} #{@contact.name}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + + <%= render :partial => 'common/recently_viewed' %> +<% end %> + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_additional_assets.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_additional_assets.html.erb new file mode 100644 index 0000000..12e9e48 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_additional_assets.html.erb @@ -0,0 +1,6 @@ +<% content_for :header_tags do %> + <%= select2_assets %> + <%= stylesheet_link_tag(:contacts, :plugin => 'redmine_contacts') %> + <%= javascript_include_tag(:contacts_select2, :plugin => 'redmine_contacts') %> + <%= javascript_include_tag(:contacts, :plugin => 'redmine_contacts') %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_attributes.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_attributes.html.erb new file mode 100644 index 0000000..6175388 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_attributes.html.erb @@ -0,0 +1,35 @@ +<%= fields_for "issue" do |ff| %> + + <%= label_tag 'task_subject', l(:field_subject)%>
    + <%= ff.text_field :subject %> + +

    + <%= label_tag :assigned_to_id, l(:field_assigned_to)%>
    + <%= ff.select :assigned_to_id, @project.assignable_users.collect {|m| [m.name, m.id]}, :selected => User.current.id, :include_blank => true %> +

    + <%= label_tag 'due_date', l(:field_due_date)%>
    + <%= ff.text_field :due_date, :value => Date.today, :size => 12 %><%= calendar_for('issue_due_date') %>
    + +

    + <%= label_tag :description, l(:field_description)%>
    + <%= ff.text_area :description, :value => "", :rows => 6, :class => 'wiki-edit' , :style => "width: 98%;" %>
    +

    + + <% if @project.issue_categories.any? %> +

    + <%= label_tag :category_id, l(:field_category)%>
    + <%= ff.select :category_id, (@project.issue_categories.collect {|c| [c.name, c.id]}), :include_blank => true %> +

    + <% end %> + +

    + <%= label_tag :tracker_id, l(:field_tracker)%>
    + <%= ff.select :tracker_id, @project.trackers.collect {|t| [t.name, t.id]} %> +

    + +<% end %> + +
    +
    +
    +<%= submit_tag l(:button_add), :class => "button-small" %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_contacts.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_contacts.html.erb new file mode 100644 index 0000000..8a9512c --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_contacts.html.erb @@ -0,0 +1,45 @@ +<% if !@issue.blank? && User.current.allowed_to?(:view_contacts, @project) %> + +
    + + + + + <% if User.current.allowed_to?(:manage_contact_issue_relations, @project) %> +
    + <%= link_to l(:button_add), + {:controller => 'contacts_issues', :action => 'new', :issue_id => @issue}, + :remote => true, + :method => 'get' %> +
    + <% end %> + + +

    <%= l(:label_contact_plural) %>

    + +
      + <% @issue.contacts.order_by_name.visible.each do |contact| %> +
    • + <%= contact_tag(contact) %> + <%= "(#{contact.job_title}) " unless contact.job_title.blank? %> + <% if User.current.allowed_to?(:delete_contacts, @project) %> + <%= link_to(image_tag('delete.png'), + { :controller => 'contacts_issues', + :action => 'delete', + :issue_id => @issue, + :project_id => @project, + :id => contact.id}, + :remote => true, + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => "delete", + :title => l(:button_delete) ) %> + + <% end %> +
    • + <% end %> +
    + +
    + +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_issue_item.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_issue_item.html.erb new file mode 100644 index 0000000..1ce3868 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_issue_item.html.erb @@ -0,0 +1,11 @@ +> + + <%= check_box_tag :close, '', issue_item.closed?, :disabled => (issue_item.assigned_to != User.current) || issue_item.closed?, :onclick => "$(this).attr('disabled', 'disabled'); $('#contact_issue_#{issue_item.id}').css('text-decoration', 'line-through');$.post('#{url_for({:controller => "contacts_issues", :action => "close", :issue_id => issue_item.id})}');".html_safe unless Setting.plugin_redmine_contacts[:one_click_close] %> + + + + <%= link_to(issue_item.subject, {:controller => :issues, :action => :show, :id => issue_item}, :class => "issue status-#{issue_item.status_id} priority-#{issue_item.priority_id} #{'closed' if issue_item.closed?}") %> + <%= avatar(issue_item.assigned_to, :size => "14", :title => issue_item.assigned_to.name).to_s.html_safe if issue_item.assigned_to %> + + <%= format_date(issue_item.due_date) %> + diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_issues.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_issues.html.erb new file mode 100644 index 0000000..35832e1 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_issues.html.erb @@ -0,0 +1,35 @@ +
    + +
    + <%= link_to l(:label_issue_new), {}, :onclick => "$('#add_issue').show(); $('#add_issue_link').hide(); return false;", :id => 'add_issue_link' if User.current.allowed_to?(:add_issues, @project) && User.current.allowed_to?(:manage_contact_issue_relations, @project) %> +
    + +

    <%= @contact_issues_count > 0 ? link_to("#{l(:label_issue_plural)} (#{@contact_issues_count})", {:controller => 'issues', + :action => 'index', + :set_filter => 1, + :f => [:contacts, :status_id], + :v => {:contacts => [@contact.id]}, + :op => {:contacts => '=', :status_id => '*'}}) : "#{l(:label_issue_plural)} (#{@contact_issues_count})" %>

    + + +<%= error_messages_for 'issue' %> + +<% if User.current.allowed_to?(:add_issues, @project) %> + +<% end %> + +<% if issues.any? %> + + <%= render :partial => 'contacts_issues/issue_item', :collection => issues %> +
    +<% end %> + +
    diff --git a/plugins/redmine_contacts/app/views/contacts_issues/_new_modal.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/_new_modal.html.erb new file mode 100644 index 0000000..8e092d8 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/_new_modal.html.erb @@ -0,0 +1,27 @@ +

    <%= l(:label_crm_add_contact_plural) %>

    + +<%= form_tag({:controller => 'contacts_issues', + :action => 'create', + :issue_id => @issue, + :project_id => @project}, + :remote => true, + :method => :post, + :id => 'add-contact-form') do %> + +

    <%= label_tag 'contact_search', l(:label_crm_search_for_contact) %>:<%= text_field_tag 'contact_search', nil, :placeholder => l(:label_crm_contact_search) %>

    + <%= javascript_tag "observeSearchfield('contact_search', 'contacts_for_issue', '#{escape_javascript url_for(:controller => 'contacts_issues', + :action => 'autocomplete_for_contact', :project_id => @project, :issue_id => @issue, :cross_project_contacts => ContactsSetting.cross_project_contacts? ? '1' : '0')}')" %> + +
    + <%= contacts_check_box_tags 'contacts_issue[contact_ids][]', Contact.includes(:avatar).by_project(ContactsSetting.cross_project_contacts? ? nil : @project).visible.first(100) - @issue.contacts %> +
    + +

    + <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %> + <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %> +

    +<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/autocomplete_for_contact.html.erb b/plugins/redmine_contacts/app/views/contacts_issues/autocomplete_for_contact.html.erb new file mode 100644 index 0000000..1562384 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/autocomplete_for_contact.html.erb @@ -0,0 +1 @@ +<%= contacts_check_box_tags 'contacts_issue[contact_ids][]', @contacts %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/close.js.erb b/plugins/redmine_contacts/app/views/contacts_issues/close.js.erb new file mode 100644 index 0000000..9cceeae --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/close.js.erb @@ -0,0 +1,5 @@ +<% if RedmineContacts.settings[:show_closed_issues] %> + $('#contact_issue_<%= params[:issue_id] %> td.task_item_subject a').toggleClass('closed'); +<% else %> + $('#contact_issue_<%= params[:issue_id] %>').effect('fade', {}, 1000); +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_issues/create.js.erb b/plugins/redmine_contacts/app/views/contacts_issues/create.js.erb new file mode 100644 index 0000000..95d0e1c --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/create.js.erb @@ -0,0 +1,2 @@ +$('#ajax-modal').html('<%= escape_javascript(render :partial => 'contacts_issues/new_modal') %>'); +$('#issue_contacts').html('<%= escape_javascript(render :partial => 'contacts_issues/contacts') %>'); diff --git a/plugins/redmine_contacts/app/views/contacts_issues/delete.js.erb b/plugins/redmine_contacts/app/views/contacts_issues/delete.js.erb new file mode 100644 index 0000000..ce012bc --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/delete.js.erb @@ -0,0 +1 @@ +$('#issue_contacts #contact_<%= @contact.id %>').effect('fade'); diff --git a/plugins/redmine_contacts/app/views/contacts_issues/new.js.erb b/plugins/redmine_contacts/app/views/contacts_issues/new.js.erb new file mode 100644 index 0000000..0d04c7d --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_issues/new.js.erb @@ -0,0 +1,3 @@ +$('#ajax-modal').html('<%= escape_javascript(render :partial => 'contacts_issues/new_modal') %>'); +showModal('ajax-modal', '400px'); +$('#ajax-modal').addClass('new-contacts-issue'); diff --git a/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.html.erb b/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.html.erb new file mode 100755 index 0000000..c769afe --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.html.erb @@ -0,0 +1 @@ +<%= textilizable(@params[:message], :only_path => false).gsub(/¶/, '').html_safe %> diff --git a/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.text.erb b/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.text.erb new file mode 100755 index 0000000..fafb30e --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_mailer/bulk_mail.text.erb @@ -0,0 +1 @@ +<%= @params[:message] %> diff --git a/plugins/redmine_contacts/app/views/contacts_projects/_related.html.erb b/plugins/redmine_contacts/app/views/contacts_projects/_related.html.erb new file mode 100644 index 0000000..cd570e9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_projects/_related.html.erb @@ -0,0 +1,22 @@ +
    + <%= link_to l(:button_add), new_contact_contacts_project_path(@contact, :project_id => @project), + :remote => true if User.current.allowed_to?(:edit_contacts, @project)%> +
    + + +

    <%= l(:label_project_plural) %>

    + +<% unless !(@show_form == "true") %> + <%= form_tag(contact_contacts_projects_path(@contact, :project_id => @project), + :remote => true, + :method => :post, + :id => 'add-project-form') do %> +

    <%= select_tag :id, project_tree_options_for_select(Project.allowed_to(:edit_contacts).order(:lft).all), :prompt => "--- #{l(:actionview_instancetag_blank_option)} ---" %> + + <%= submit_tag l(:button_add), :class => "button-small" %> + <%= link_to l(:button_cancel), {}, :onclick => "$('#add-project-form').hide(); return false;" %> +

    + <% end %> +<% end %> + +<%= render_contact_projects_hierarchy @contact.projects %> diff --git a/plugins/redmine_contacts/app/views/contacts_projects/new.js.erb b/plugins/redmine_contacts/app/views/contacts_projects/new.js.erb new file mode 100644 index 0000000..09a2802 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_projects/new.js.erb @@ -0,0 +1 @@ +$('#contact_projects').html('<%= escape_javascript(render :partial => "contacts_projects/related") %>') diff --git a/plugins/redmine_contacts/app/views/contacts_tags/_tags_form.html.erb b/plugins/redmine_contacts/app/views/contacts_tags/_tags_form.html.erb new file mode 100644 index 0000000..32d8aee --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_tags/_tags_form.html.erb @@ -0,0 +1,3 @@ + + <%= text_field_tag 'contact[tag_list]', "#{@contact.tags.map(&:name).join(',').html_safe}", :size => 10, :class => 'hidden', :id => "allowSpacesTags" %><%= tagsedit_with_source_for('#allowSpacesTags', auto_complete_contact_tags_path(:project_id => @project)) %> + diff --git a/plugins/redmine_contacts/app/views/contacts_tags/context_menu.html.erb b/plugins/redmine_contacts/app/views/contacts_tags/context_menu.html.erb new file mode 100644 index 0000000..3e6a326 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_tags/context_menu.html.erb @@ -0,0 +1,12 @@ +
      + <% if @tag -%> +
    • <%= link_to l(:button_edit), edit_contacts_tag_path(@tag), + :class => 'icon-edit' %>
    • + <% else %> +
    • <%= link_to l(:label_crm_megre_tags), merge_contacts_tags_path(:ids => @tags.collect(&:id)), + :class => 'icon-merge' %>
    • + <% end %> + +
    • <%= link_to l(:button_delete), contacts_tags_path(:ids => @tags.collect(&:id), :back_url => @back), + :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon-del' %>
    • +
    diff --git a/plugins/redmine_contacts/app/views/contacts_tags/edit.html.erb b/plugins/redmine_contacts/app/views/contacts_tags/edit.html.erb new file mode 100644 index 0000000..4f25081 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_tags/edit.html.erb @@ -0,0 +1,12 @@ +

    <%= link_to l(:contacts_title), {:controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags"} %> » <%=h (l(:label_tag) + ": " + @tag.name) %>

    + +<%= form_tag(contacts_tag_path(@tag), :class => "tabular", :method => :put) do %> + <%= error_messages_for 'tag' %> +
    +

    + <%= label_tag("tag_name", l(:field_name)) %> + <%= text_field 'tag', 'name' %> +

    +
    + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_tags/index.api.rsb b/plugins/redmine_contacts/app/views/contacts_tags/index.api.rsb new file mode 100644 index 0000000..3d8bff4 --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_tags/index.api.rsb @@ -0,0 +1,9 @@ +api.array :tags, api_meta(:total_count => @tags.count) do + @tags.each do |tag| + api.tag do + api.id tag.id + api.name tag.name + api.color tag_color(tag.name) + end + end +end diff --git a/plugins/redmine_contacts/app/views/contacts_tags/merge.html.erb b/plugins/redmine_contacts/app/views/contacts_tags/merge.html.erb new file mode 100644 index 0000000..6de5a6f --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_tags/merge.html.erb @@ -0,0 +1,14 @@ +

    <%= link_to l(:label_crm_tags_plural), {:controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => "tags"} %> » <%= l(:label_crm_megre_tags) %>

    +<%= form_tag(merge_contacts_tags_path(:ids => @tags.map(&:id)), :class => "tabular") do %> + <%= error_messages_for 'tag' %> +
    +

    + + <%= tag_links(@tags.map(&:name)) %> +

    +

    + <%= text_field 'tag', 'name', :value => @tags.first.name %>

    +
    + + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/contacts_vcf/_load.html.erb b/plugins/redmine_contacts/app/views/contacts_vcf/_load.html.erb new file mode 100644 index 0000000..b244c6a --- /dev/null +++ b/plugins/redmine_contacts/app/views/contacts_vcf/_load.html.erb @@ -0,0 +1,16 @@ +<%- if ContactsSetting.vcard? -%> +

    <%= l(:label_crm_import) %>

    + + + <%= link_to l(:label_crm_vcf_import), {}, :onclick => "$('#import_link').toggle(); $('#import_file').toggle(); return false;" %> + + <%= form_tag({:controller => :contacts_vcf, :action => :load, :project_id => @project}, :multipart => true ) do %> + + <% end %> + +<%- end -%> diff --git a/plugins/redmine_contacts/app/views/crm_calendars/_buttons.html.erb b/plugins/redmine_contacts/app/views/crm_calendars/_buttons.html.erb new file mode 100644 index 0000000..fb73828 --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_calendars/_buttons.html.erb @@ -0,0 +1,10 @@ +

    + <%= link_to_previous_month(@year, @month) %> | <%= link_to_next_month(@year, @month) %> +

    + +

    +<%= label_tag('month', l(:label_month)) %> +<%= select_month(@month, :prefix => "month", :discard_type => true) %> +<%= label_tag('year', l(:label_year)) %> +<%= select_year(@year, :prefix => "year", :discard_type => true) %> +

    diff --git a/plugins/redmine_contacts/app/views/crm_calendars/_crm_calendar.html.erb b/plugins/redmine_contacts/app/views/crm_calendars/_crm_calendar.html.erb new file mode 100644 index 0000000..f7822da --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_calendars/_crm_calendar.html.erb @@ -0,0 +1,21 @@ + + +<% 7.times do |i| %><% end %> + + + +<% day = @calendar.startdt +while day <= @calendar.enddt %> +<%= ("".html_safe) if day.cwday == @calendar.first_wday %> + +<%= ''.html_safe if day.cwday==@calendar.last_wday and day!=@calendar.enddt %> +<% day = day + 1 +end %> + + +
    <%= day_name( (@calendar.first_wday+i)%7 ) %>
    #{(day+(11-day.cwday)%7).cweek} +

    <%= day.day %>

    +<% @calendar.events_on(day).each do |i| %> + <%= render :partial => "crm_calendars/#{i.class.name.downcase}_calendar_event", :locals => {:event => i, :day => day} %> +<% end %> +
    diff --git a/plugins/redmine_contacts/app/views/crm_calendars/_deal_calendar_event.html.erb b/plugins/redmine_contacts/app/views/crm_calendars/_deal_calendar_event.html.erb new file mode 100644 index 0000000..2bc5f42 --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_calendars/_deal_calendar_event.html.erb @@ -0,0 +1,5 @@ +
    + <%= h("#{event.project} -") unless @project && @project == event.project %> + <%= link_to "#{event.price_to_s} #{event.name}", deal_path(event), :class => "icon icon-add-deal" %> + <%= event.contact.name if event.contact %> +
    diff --git a/plugins/redmine_contacts/app/views/crm_queries/edit.html.erb b/plugins/redmine_contacts/app/views/crm_queries/edit.html.erb new file mode 100644 index 0000000..a68795e --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_queries/edit.html.erb @@ -0,0 +1,6 @@ +

    <%= l(:label_query) %>

    + +<%= form_tag(crm_query_path(@query, :object_type => @object_type), :onsubmit => 'selectAllOptions("selected_columns");', :method => :put) do %> + <%= render :partial => 'queries/form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/crm_queries/index.api.rsb b/plugins/redmine_contacts/app/views/crm_queries/index.api.rsb new file mode 100644 index 0000000..cdf8e40 --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_queries/index.api.rsb @@ -0,0 +1,10 @@ +api.array :queries, api_meta(:total_count => @query_count, :offset => @offset, :limit => @limit) do + @queries.each do |query| + api.query do + api.id query.id + api.name query.name + api.is_public query.is_public? + api.project_id query.project_id + end + end +end diff --git a/plugins/redmine_contacts/app/views/crm_queries/index.html.erb b/plugins/redmine_contacts/app/views/crm_queries/index.html.erb new file mode 100644 index 0000000..ade4c30 --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_queries/index.html.erb @@ -0,0 +1,25 @@ +
    +<%= link_to_if_authorized l(:label_query_new), new_project_crm_query_path(:project_id => @project, :object_type => @object_type), :class => 'icon icon-add' %> +
    + +

    <%= l(:label_query_plural) %>

    + +<% if @queries.empty? %> +

    <%=l(:label_no_data)%>

    +<% else %> + + <% @queries.each do |query| %> + + + + + <% end %> +
    + <%= link_to h(query.name), :controller => "#{@object_type}s", :action => 'index', :project_id => @project, :query_id => query %> + + <% if query.editable_by?(User.current) %> + <%= link_to l(:button_edit), edit_crm_query_path(query), :class => 'icon icon-edit' %> + <%= delete_link crm_query_path(query) %> + <% end %> +
    +<% end %> diff --git a/plugins/redmine_contacts/app/views/crm_queries/new.html.erb b/plugins/redmine_contacts/app/views/crm_queries/new.html.erb new file mode 100644 index 0000000..832a18e --- /dev/null +++ b/plugins/redmine_contacts/app/views/crm_queries/new.html.erb @@ -0,0 +1,6 @@ +

    <%= l(:label_query_new) %>

    + +<%= form_tag(@project ? project_crm_queries_path(@project, :object_type => @object_type) : crm_queries_path(:object_type => @object_type), :onsubmit => 'selectAllOptions("selected_columns");') do %> + <%= render :partial => 'queries/form', :locals => {:query => @query} %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_categories/_form.html.erb b/plugins/redmine_contacts/app/views/deal_categories/_form.html.erb new file mode 100644 index 0000000..82702e2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_categories/_form.html.erb @@ -0,0 +1,5 @@ +<%= error_messages_for 'category' %> + +
    +

    <%= f.text_field :name, :size => 30, :required => true %>

    +
    diff --git a/plugins/redmine_contacts/app/views/deal_categories/destroy.html.erb b/plugins/redmine_contacts/app/views/deal_categories/destroy.html.erb new file mode 100644 index 0000000..bfcb443 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_categories/destroy.html.erb @@ -0,0 +1,15 @@ +

    <%=l(:label_crm_deal_category)%>: <%=h @category.name %>

    + +<%= form_tag(deal_category_path(@category), :method => :delete) do %> +
    +

    <%= l(:text_deal_category_destroy_question, @deal_count) %>

    +


    +<% if @categories.size > 0 %> +: +<%= select_tag 'reassign_to_id', options_from_collection_for_select(@categories, 'id', 'name') %>

    +<% end %> +
    + +<%= submit_tag l(:button_apply) %> +<%= link_to l(:button_cancel), :controller => 'projects', :action => 'settings', :id => @project, :tab => 'contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_categories/edit.html.erb b/plugins/redmine_contacts/app/views/deal_categories/edit.html.erb new file mode 100644 index 0000000..37586e6 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_categories/edit.html.erb @@ -0,0 +1,6 @@ +

    <%=l(:label_crm_deal_category)%>

    + +<%= labelled_form_for @category, :as => :category, :url => deal_category_url(@category), :html => {:method => :put} do |f| %> +<%= render :partial => 'deal_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_categories/index.api.rsb b/plugins/redmine_contacts/app/views/deal_categories/index.api.rsb new file mode 100644 index 0000000..bec4561 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_categories/index.api.rsb @@ -0,0 +1,8 @@ +api.array :deal_categories, api_meta(:total_count => @categories.count) do + @categories.each do |category| + api.category do + api.id category.id + api.name category.name + end + end +end diff --git a/plugins/redmine_contacts/app/views/deal_categories/new.html.erb b/plugins/redmine_contacts/app/views/deal_categories/new.html.erb new file mode 100644 index 0000000..5a64671 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_categories/new.html.erb @@ -0,0 +1,6 @@ +

    <%=l(:label_issue_category_new)%>

    + +<%= labelled_form_for @category, :as => :category, :url => project_deal_categories_path(@project) do |f| %> +<%= render :partial => 'deal_categories/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_contacts/_contacts.html.erb b/plugins/redmine_contacts/app/views/deal_contacts/_contacts.html.erb new file mode 100644 index 0000000..1199520 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/_contacts.html.erb @@ -0,0 +1,19 @@ +<% if @deal.all_contacts.any? %> +
    +
    + <%= link_to l(:button_add), + {:controller => 'deal_contacts', + :action => 'search', + :project_id => @project, + :deal_id => @deal}, + :remote => true if User.current.allowed_to?({:controller => 'deal_contacts', :action => 'add'}, @project) %> +
    + +

    <%= l(:label_crm_contractor_plural) %>

    + + <%= render :partial => 'common/contact_data', :object => @deal.contact if @deal.contact %> + <% @deal.related_contacts.each do |contact| %> + <%= render :partial => 'common/contact_data', :object => contact, :locals => {:actions => remove_contractor_link(contact)} %> + <% end %> +
    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_contacts/_new_modal.erb b/plugins/redmine_contacts/app/views/deal_contacts/_new_modal.erb new file mode 100644 index 0000000..231243b --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/_new_modal.erb @@ -0,0 +1,23 @@ +

    <%= l(:label_crm_add_contact_plural) %>

    + +<%= form_tag({:controller => 'deal_contacts', + :action => 'add', + :deal_id => @deal, + :project_id => @project}, + :remote => true, + :method => :post, + :id => 'add-contact-form', + :class => 'select-users') do |f| %> + +

    <%= label_tag 'contact_search', l(:label_crm_search_for_contact) %>:<%= text_field_tag 'contact_search', nil, :placeholder => l(:label_crm_contact_search) %>

    + <%= javascript_tag "observeSearchfield('contact_search', 'contacts_for_issue', '#{ escape_javascript url_for(:controller => :deal_contacts, :action => :autocomplete, :deal_id => @deal, :project_id => @project) }')" %> + +
    + <%= contacts_check_box_tags('contact_id[]', @contacts) %> +
    + +

    + <%= submit_tag l(:button_add), :name => nil, :onclick => "hideModal(this);" %> + <%= submit_tag l(:button_cancel), :name => nil, :onclick => "hideModal(this);", :type => 'button' %> +

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_contacts/add.js.erb b/plugins/redmine_contacts/app/views/deal_contacts/add.js.erb new file mode 100644 index 0000000..ed7c0a6 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/add.js.erb @@ -0,0 +1 @@ +$('#deal_contacts').html('<%= escape_javascript(render :partial => "deal_contacts/contacts") %>'); diff --git a/plugins/redmine_contacts/app/views/deal_contacts/autocomplete.html.erb b/plugins/redmine_contacts/app/views/deal_contacts/autocomplete.html.erb new file mode 100644 index 0000000..75ea3fa --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/autocomplete.html.erb @@ -0,0 +1 @@ +<%= contacts_check_box_tags 'contact_id[]', @contacts %> diff --git a/plugins/redmine_contacts/app/views/deal_contacts/delete.js.erb b/plugins/redmine_contacts/app/views/deal_contacts/delete.js.erb new file mode 100644 index 0000000..b83442c --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/delete.js.erb @@ -0,0 +1 @@ +$('#contact_data_<%= params[:contact_id] %>').effect('fade', {}, 1000); diff --git a/plugins/redmine_contacts/app/views/deal_contacts/search.js.erb b/plugins/redmine_contacts/app/views/deal_contacts/search.js.erb new file mode 100644 index 0000000..baeb278 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_contacts/search.js.erb @@ -0,0 +1,4 @@ +var modal = $('#ajax-modal'); +modal.html('<%= escape_javascript(render :partial => 'deal_contacts/new_modal') %>'); +showModal('ajax-modal', '400px'); +modal.addClass('new-contact'); diff --git a/plugins/redmine_contacts/app/views/deal_statuses/_form.html.erb b/plugins/redmine_contacts/app/views/deal_statuses/_form.html.erb new file mode 100644 index 0000000..c340cfd --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_statuses/_form.html.erb @@ -0,0 +1,40 @@ +<%= error_messages_for 'deal_status' %> + +
    + +

    +<%= text_field 'deal_status', 'name' %>

    + + +

    +<%= text_field 'deal_status', 'color_name', :class => "colorpicker" %> + +

    + + + +

    +<%= select_tag 'deal_status[status_type]', options_for_select([[l(:label_open_issues), DealStatus::OPEN_STATUS], [l(:label_crm_deal_status_won), DealStatus::WON_STATUS], [l(:label_crm_deal_status_lost), DealStatus::LOST_STATUS]], @deal_status.status_type) %> + +

    +<%= check_box 'deal_status', 'is_default' %>

    + +<%= call_hook(:view_deal_statuses_form, :deal_status => @deal_status) %> + + +
    + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= javascript_include_tag :"jquery.colorPicker.min.js", :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :"colorPicker.css", :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_statuses/edit.html.erb b/plugins/redmine_contacts/app/views/deal_statuses/edit.html.erb new file mode 100644 index 0000000..e0c1263 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_statuses/edit.html.erb @@ -0,0 +1,6 @@ +

    <%= link_to l(:label_crm_deal_status_plural), :controller => 'deal_statuses', :action => 'index' %> » <%=h @deal_status %>

    + +<%= form_tag({:action => 'update', :id => @deal_status}, :class => "tabular", :method => :put) do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deal_statuses/index.api.rsb b/plugins/redmine_contacts/app/views/deal_statuses/index.api.rsb new file mode 100644 index 0000000..2f7448c --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_statuses/index.api.rsb @@ -0,0 +1,12 @@ +api.array :deal_statuses, api_meta(:total_count => @deal_statuses.count) do + @deal_statuses.each do |deal_status| + api.deal_status do + api.id deal_status.id + api.name deal_status.name + api.position deal_status.position + api.is_default deal_status.is_default + api.status_type deal_status.status_type + api.color deal_status.color + end + end +end diff --git a/plugins/redmine_contacts/app/views/deal_statuses/index.html.erb b/plugins/redmine_contacts/app/views/deal_statuses/index.html.erb new file mode 100644 index 0000000..bf891b9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_statuses/index.html.erb @@ -0,0 +1,30 @@ +
    +<%= link_to l(:label_crm_deal_status_new), {:action => 'new'}, :class => 'icon icon-add' %> +
    + +

    <%=l(:label_crm_deal_status_plural)%>

    + + + + + + + + + + +<% for status in @deal_statuses %> + "> + + + + + + +<% end unless @deal_statuses.blank? %> + +
    <%=l(:field_status)%><%=l(:field_is_default)%><%=l(:label_crm_deal_status_type)%><%=l(:button_sort)%>
    <%= link_to status.name, :action => 'edit', :id => status %><%= checked_image status.is_default? %><%= status.status_type_name %><%= reorder_links('deal_status', {:action => 'update', :id => status}, :put) %> + <%= delete_link deal_status_path(status) %> +
    + +<% html_title(l(:label_crm_deal_status_plural)) -%> diff --git a/plugins/redmine_contacts/app/views/deal_statuses/new.html.erb b/plugins/redmine_contacts/app/views/deal_statuses/new.html.erb new file mode 100644 index 0000000..781f99e --- /dev/null +++ b/plugins/redmine_contacts/app/views/deal_statuses/new.html.erb @@ -0,0 +1,6 @@ +

    <%= link_to l(:label_crm_deal_status_plural), :controller => 'deal_statuses', :action => 'index' %> » <%=l(:label_crm_deal_status_new)%>

    + +<%= form_tag({:action => 'create'}, :class => "tabular") do %> + <%= render :partial => 'form' %> + <%= submit_tag l(:button_create) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_attributes.html.erb b/plugins/redmine_contacts/app/views/deals/_attributes.html.erb new file mode 100644 index 0000000..bac3561 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_attributes.html.erb @@ -0,0 +1,14 @@ +<% if @deal.custom_field_values.any? %> +
    + +

    <%= l(:label_deal) %>

    + + <% @deal.custom_field_values.each do |custom_value| %> + <% if !custom_value.value.blank? %> + + <% end %> + <% end %> +
    <%= custom_value.custom_field.name %>: <%=h show_value(custom_value) %>
    + +
    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_board_deals_counts.html.erb b/plugins/redmine_contacts/app/views/deals/_board_deals_counts.html.erb new file mode 100644 index 0000000..fcd3ab2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_board_deals_counts.html.erb @@ -0,0 +1,7 @@ + + <% deal_statuses.each do |deal_status| %> + + <%= "#{deal_status.name} (#{@deals_scope.where(:status_id => deal_status.id).count})"%> + + <% end %> + diff --git a/plugins/redmine_contacts/app/views/deals/_board_total.html.erb b/plugins/redmine_contacts/app/views/deals/_board_total.html.erb new file mode 100644 index 0000000..2deabe1 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_board_total.html.erb @@ -0,0 +1,7 @@ + + <% deal_statuses.each do |deal_status| %> + + <%= prices_collection_by_currency(@deals_scope.group(:currency).where(:status_id => deal_status.id).sum(:price), :hide_zeros => true).join('
    ').html_safe %> + + <% end %> + diff --git a/plugins/redmine_contacts/app/views/deals/_custom_field_form.html.erb b/plugins/redmine_contacts/app/views/deals/_custom_field_form.html.erb new file mode 100644 index 0000000..9208ff5 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_custom_field_form.html.erb @@ -0,0 +1,5 @@ +

    <%= form.check_box :is_filter %>

    +

    <%= form.check_box :visible, :label => l(:label_crm_contacts_show_in_list) %>

    +<% if (@custom_field.respond_to?(:format) && @custom_field.format.searchable_supported) || !@custom_field.respond_to?(:format) %> +

    <%= form.check_box :searchable %>

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_deals_statistics.html.erb b/plugins/redmine_contacts/app/views/deals/_deals_statistics.html.erb new file mode 100644 index 0000000..d9b9197 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_deals_statistics.html.erb @@ -0,0 +1,24 @@ +<% if deal_statuses.any? %> +
    +
    + <%= link_to l(:label_crm_sales_funnel), {:controller => 'deals', :action => 'index', :project_id => @project, :deals_list_style => 'list_pipeline'} %> +
    +

    <%= l(:label_crm_statistics) %>

    + + <% deal_statuses.each do |deal_status| %> + + + + + <% end %> +
    + > + <%= link_to "#{deal_status.name}(#{@project ? @project.deals.visible.with_status(deal_status.id).count : Deal.visible.with_status(deal_status.id).count})".html_safe, deal_status_url(deal_status.id, :project_id => @project) %> + + + + <%= prices_collection_by_currency(Deal.by_project(@project).visible.where(:status_id => deal_status.id).group(:currency).sum(:price), :hide_zeros => true).join(' / ').html_safe %> + +
    +
    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_form.html.erb b/plugins/redmine_contacts/app/views/deals/_form.html.erb new file mode 100644 index 0000000..69b1883 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_form.html.erb @@ -0,0 +1,68 @@ +<%= labelled_fields_for :deal, @deal do |f| %> +<%= call_hook(:view_deals_form_details_top, {:deal => @deal, :form => f }) %> + +

    <%= f.text_field :name, :label=>l(:field_deal_name), :size => 80, :required => true %>

    +

    <%= f.select :project_id, project_tree_options_for_select(Deal.allowed_target_projects, :selected => @deal.project), {:required => true}, :onchange => "updateCustomForm('#{escape_javascript update_form_deals_path(:id => @deal, :format => 'js')}', '#deal_form')" %>

    +

    <%= f.text_area :background , :cols => 80, :rows => 8, :class => 'wiki-edit', :label=>l(:field_deal_background) %>

    <%= wikitoolbar_for 'deal_background' %> + +
    +
    + <% if @project.deal_statuses.any? %> +

    <%= f.select :status_id, collection_for_status_select, :include_blank => false, :label=>l(:field_contact_status) %>

    + <% end %> + +

    + <%= label_tag :deal_contact_id, RedmineContacts.companies_select ? l(:label_crm_company) : l(:label_contact)%> + <%= select_contact_tag('deal[contact_id]', + @deal.contact, + :include_blank => true, + :add_contact => true, + :is_company => RedmineContacts.companies_select) %> +

    +

    + <% if RedmineContacts.products_plugin_installed? %> + <%= f.text_field :price, :label => l(:field_deal_price), :size => 10, :disabled => @deal.lines.present? %> + <% else %> + <%= f.text_field :price, :label => l(:field_deal_price), :size => 10 %> + <% end %> + <%= select_tag "deal[currency]", options_for_select(collection_for_currencies_select(ContactsSetting.default_currency, ContactsSetting.major_currencies), @deal.currency), :include_blank => true, :style => "width: initial;" + %> +

    +

    <%= f.select :assigned_to_id, (@project.assignable_users.collect {|m| [m.name, m.id]}), :include_blank => true, :label => l(:label_crm_assigned_to) %>

    +
    + +
    + <% unless @project.deal_categories.empty? %> +

    <%= f.select :category_id, (@project.deal_categories.collect {|c| [c.name, c.id]}), :include_blank => true %>

    + <% end %> +

    <%= f.select :probability, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }), :label => l(:label_crm_probability), :include_blank => true %>

    +

    <%= f.text_field :due_date, :label => l(:field_due_date), :size => 12 %><%= calendar_for('deal_due_date') %>

    +
    +
    +
    + +<% custom_field_values = @deal.custom_field_values %> +
    +
    + <% i = 0 %> + <% split_on = (custom_field_values.size / 2.0).ceil - 1 %> + <% custom_field_values.each do |value| %> +

    <%= custom_field_tag_with_label :deal, value, :required => value.custom_field.is_required? %>

    + <% if i == split_on -%> +
    + <% end -%> + <% i += 1 -%> + <% end -%> +
    +
    +<% if RedmineContacts.products_plugin_installed? %> +

    + <%= label_tag l(:label_deal_items) %> + <%= link_to('Add new', 'javascript:void(0)', :onclick => 'toogleDealItems(this); return false;', :class => 'icon icon-add') %> +

    + <%= field_set_tag(l(:label_deal_items), :class => "deal_items #{'hol' unless @deal.lines.present?}") do %> + <%= render :partial =>'shared/new_product_line', :locals => { :form => f, :parent_object => @deal} %> + <% end %> +<% end %> +<%= call_hook(:view_deals_form_details_bottom, {:deal => @deal, :form => f }) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_line_fields.html.erb b/plugins/redmine_contacts/app/views/deals/_line_fields.html.erb new file mode 100644 index 0000000..acc8160 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_line_fields.html.erb @@ -0,0 +1,45 @@ +<% product = f.object.product %> + + <% unless product.blank? %> + <%= product_tag(product, :size => 32, :type => 'image') %> + <% end %> + + <%= product_tag(product, :type => 'plain') unless product.blank? %> + <%= f.hidden_field :product_id %> + <% if !product.blank? && f.object.description.blank? %> +
    + <%= link_to_function "(#{l(:label_products_add_description)})", "$(this).hide(); $(this).parent().next().show(); return false;" %> + <% end %> + <%= f.text_area :description, :no_label => true, :rows => f.object.description.blank? ? 2 : [f.object.description.lines.count, 2].max, :onkeyup => "activateTextAreaResize(this);", :style => "width:99%; #{(product.blank? || !f.object.description.blank?) ? "" : "display:none;"}" -%> + + <% f.object.custom_field_values.each do |cf| %> + <%= custom_field_tag("order[lines_attributes][#{f.index}]", cf) %> + <% end %> + <%= f.text_field :quantity, :no_label => true, :size => 6, :onkeyup => 'updateTotal(this)' %> + <%= f.text_field :price, :no_label => true, :size => 8, :onkeyup => 'updateTotal(this)' %> + + <% if !ContactsSetting.disable_taxes? || (f.object.container.respond_to?(:has_taxes?) && f.object.container.has_taxes?) %> + + <% line_tax = (f.object.new_record? && f.object.tax.blank?) ? ContactsSetting.default_tax : f.object.tax %> + <%= check_box_tag :show_tax, "1", false, :onclick=>"$(this).hide(); $(this).parent().find('.tax-fields').show(); $(this).next().find('input').focus(); return false;" if line_tax.blank? || line_tax == 0 %> + "><%= f.text_field :tax, :no_label => true, :size => 5, :value => line_tax %> % + + + <% end %> + + <%= check_box_tag :show_discount, "1", false, :onclick=>"$(this).hide(); $(this).parent().find('.discount-fields').show(); $(this).next().find('input').focus(); return false;" if f.object.discount.to_i == 0 %> + "> + <%= f.text_field :discount, :no_label => true, :size => 5, :style => f.object.discount.to_i == 0 ? "display:none;" : "", :onkeyup => 'updateTotal(this)', :class => "discount-fields" %> % + + + + <%= format("%.2f\n", f.object.total) if f.object.price && f.object.quantity %> + + <%= deals_link_to_remove_fields "", f, :class => "icon icon-del" %> + + <%= f.hidden_field :position, :class => 'position' %> + + + diff --git a/plugins/redmine_contacts/app/views/deals/_list.html.erb b/plugins/redmine_contacts/app/views/deals/_list.html.erb new file mode 100644 index 0000000..9500e97 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_list.html.erb @@ -0,0 +1,71 @@ +<%= form_tag({}, :data => {:cm_url => context_menu_deals_path}) do -%> +<%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params), :id => nil %> +

    + + + + + <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %> + <% @query.inline_columns.each do |column| %> + <%= Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? column_header(@query, column) : column_header(column) %> + <% end %> + + + <% previous_group = false %> + + <% @deals.each do |deal| -%> + <% if @query.grouped? && (group = @query.group_by_column.value(deal)) != previous_group %> + <% reset_cycle %> + + + + <% previous_group = group %> + <% end %> + + + + <%= raw @query.inline_columns.map {|column| ""}.join %> + + <% @query.block_columns.each do |column| + if (text = column_content(column, deal)) && text.present? -%> + + + + <% end -%> + <% end -%> + <% end -%> + + + + +
    + <%= link_to image_tag('toggle_check.png'), {}, + :onclick => 'toggleCRMIssuesSelection(this); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> +
    +   + <%= group.blank? ? l(:label_none) : column_content(@query.group_by_column, deal) %> <%= @deal_count_by_group[group] %> + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", + "toggleAllRowGroups(this)", :class => 'toggle-all') %> +
    <%= check_box_tag("ids[]", deal.id, false, :id => nil) %><%= link_to deal.id, deal_path(deal) %>#{column_content(column, deal)}
    <%= text %>
    + + + + <% if @deal_weighted_amount.map{|k, v| v.to_i > 0}.any? %> + + + <% end %> + + + + + +
    <%= l(:label_crm_expected_revenue) %>: + <%= prices_collection_by_currency(@deal_weighted_amount, :hide_zeros => true).join('
    ').html_safe %> +
    <%= "#{l(:label_total)} (#{@deals_count}):" %> + <%= prices_collection_by_currency(@deal_amount, :hide_zeros => true).join('
    ').html_safe %> +
    + +
    +
    +<% end -%> diff --git a/plugins/redmine_contacts/app/views/deals/_list_board.html.erb b/plugins/redmine_contacts/app/views/deals/_list_board.html.erb new file mode 100644 index 0000000..d7653ce --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_list_board.html.erb @@ -0,0 +1,104 @@ +<% if deal_statuses.any? %> + <% if User.current.allowed_to?(:edit_deals, @project) %> + + <% end %> + + <%= form_tag({}, :data => {:cm_url => context_menu_deals_path}) do -%> + <%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> + <%= hidden_field_tag 'project_id', @project.id if @project %> + <% board_statuses = params[:status_id] == 'o' ? deal_statuses.open : deal_statuses %> +
    + + + <%= render :partial => 'board_deals_counts' %> + + + + <% board_statuses.each do |deal_status| %> + + + <% end %> + + + + <%= render :partial => 'board_total' %> + + + +
    + <% @deals.where(:status_id => deal_status.id).order("#{Deal.table_name}.updated_on DESC").each do |deal| %> +
    +

    + <%= deal.price_to_s %> + <%= content_tag(:span, " (#{deal.probability}%)" ) if deal.probability %> +

    +

    <%= link_to deal.name, deal_path(deal) %>

    + <% if deal.contact %> +

    + <%= contact_tag(deal.contact) %> +

    + <% end %> +
    + <% end %> +
    +
    + + <% end %> +<% else %> +

    <%= l(:text_crm_no_deal_statuses_in_project) %>

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_list_excerpt.html.erb b/plugins/redmine_contacts/app/views/deals/_list_excerpt.html.erb new file mode 100644 index 0000000..1f8f666 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_list_excerpt.html.erb @@ -0,0 +1,71 @@ +<%= form_tag({}, :data => {:cm_url => context_menu_deals_path}) do -%> + <%= hidden_field_tag 'back_url', url_for(params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params) %> + <%= hidden_field_tag 'project_id', @project.id if @project %> +
    + + + <% previous_group = false %> + <% @deals.each do |deal| %> + <% if @query.grouped? && (group = @query.group_by_column.value(deal)) != previous_group %> + <% reset_cycle %> + + + + <% previous_group = group %> + <% end %> + + + + + + + + + <% end %> + +
    +   + <%= group.blank? ? 'None' : column_content(@query.group_by_column, deal) %> (<%= @deal_count_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> +
    + <%= check_box_tag "ids[]", deal.id, false, :onclick => "toggleContact(event, this);" %> + + <%= link_to avatar_to(deal, :size => "32"), {:controller => 'deals', :action => 'show', :id => deal.id}, :id => "avatar" %> + +

    <%= link_to deal.name, :controller => 'deals', :action => 'show', :id => deal.id %>

    +

    + <%= link_to_source(deal.contact) if deal.contact %> +

    +
    +
    <%= deal.price_to_s %> + <%= content_tag(:span, " (#{deal.probability}%)" ) if deal.probability %> + <% if deal.status && deal.project.deal_statuses.any? %> + <%= deal_status_tag(deal.status) %> + <% end %> +
    +
    + <%= h deal.category %><%= " (#{format_date(deal.due_date)})" if deal.due_date %> +
    +
    + + + + + <% if @deal_weighted_amount.map{|k, v| v.to_i > 0}.any? %> + + + <% end %> + + + + + +
    <%= l(:label_crm_expected_revenue) %>: + <%= prices_collection_by_currency(@deal_weighted_amount, :hide_zeros => true).join('
    ').html_safe %> +
    <%= "#{l(:label_total)} (#{@deals_count}):" %> + <%= prices_collection_by_currency(@deal_amount, :hide_zeros => true).join('
    ').html_safe %> +
    + +
    + +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_list_pipeline.html.erb b/plugins/redmine_contacts/app/views/deals/_list_pipeline.html.erb new file mode 100644 index 0000000..46dadac --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_list_pipeline.html.erb @@ -0,0 +1,45 @@ +<% if deal_statuses.any? %> +<% @deals = @query.results_scope( + :include => [{:contact => [:avatar, :projects, :address]}, :author] + ) + @processor = DealsPipelineProcessor.new(@deals) %> +
    + + + + + + + + + + <% deal_statuses.each_with_index do |deal_status, index| %> + <% status_scope = @processor.deals_for_status(deal_status) %> + + + + + + <% end %> + + + + + +
    <%= h l(:label_crm_deal_status) %><%= h l(:label_crm_count) %><%= h l(:label_total) %>
    + <%= pipeline_status_tag(deal_status, status_scope.count, index) %> + + + <%= h status_scope.size %> + + + + <%= pipeline_prices(status_scope) %> + +
    <%= "#{l(:label_total)} (#{@processor.count}):" %> + <%= pipeline_prices(@processor.scope) %> +
    +
    +<% else %> +

    <%= l(:text_crm_no_deal_statuses_in_project) %>

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/_process_item.html.erb b/plugins/redmine_contacts/app/views/deals/_process_item.html.erb new file mode 100644 index 0000000..fc2dea2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_process_item.html.erb @@ -0,0 +1,29 @@ +<% show_info = true if show_info.nil? %> +<% show_author = true if !show_author.nil? %> + +
    > + + + + <% if show_info %> + <% if show_author %> + + <% else %> + + <% end %> + <% end %> + + + +
    <%= link_to avatar(process_item.author, :size => "32"), note_source_url(process_item.deal), :id => "avatar" %><%= link_to avatar_to(process_item.deal, :size => "32"), note_source_url(process_item.deal), :id => "avatar" %> +

    + <%# note_type_icon(process_item) %> + <%= link_to_source(process_item.deal) + "," if show_info %> + <%= authoring_note process_item.created_at, process_item.author %> +

    +
    + <%= deal_status_tag(process_item.from) + " → ".html_safe if process_item.from %><%= deal_status_tag(process_item.to) if process_item.to %> +
    +
    + +
    diff --git a/plugins/redmine_contacts/app/views/deals/_related_deals.html.erb b/plugins/redmine_contacts/app/views/deals/_related_deals.html.erb new file mode 100644 index 0000000..8ce0006 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/_related_deals.html.erb @@ -0,0 +1,28 @@ +<% @deals = @contact.all_visible_deals %> + +
    + +
    + <%= link_to_if_authorized l(:label_crm_deal_new), {:controller => 'deals', :action => 'new', :project_id => @project, :contact_id => @contact} %> +
    + +

    <%= "#{l(:label_deal_plural)}" %> <%= " - #{prices_collection_by_currency(@deals.select{|d| d.open? && !d.price.blank?}.group_by(&:currency).map{|k, v| [k, v.sum(&:price)]}).join(' / ')}".html_safe if @deals.detect{|d| d.open? && !d.price.blank?} %>

    + + +<% if @deals.any? %> + + <% @deals.each do |deal| %> + + + + <% end %> + +<% end %> + +
    diff --git a/plugins/redmine_contacts/app/views/deals/bulk_edit.html.erb b/plugins/redmine_contacts/app/views/deals/bulk_edit.html.erb new file mode 100644 index 0000000..b59f77f --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/bulk_edit.html.erb @@ -0,0 +1,99 @@ +

    <%= l(:label_crm_bulk_edit_selected_deals) %>

    + + +
    +
      + <% @deals.each do |deal| %> +
    • + <%= avatar_to deal, :size => "16" %> + <%= link_to deal.full_name, polymorphic_url(deal) %> + <%= "(#{deal.price_to_s}) " unless deal.price.blank? %> + <%= deal_status_tag(deal.status) if deal.status %> +
    • + <% end %> +
    +
    + + +<%= form_tag(:action => 'bulk_update') do %> +<%= @deals.collect {|i| hidden_field_tag('ids[]', i.id)}.join.html_safe %> +
    +
    +<%= l(:label_change_properties) %> + +
    +

    + + <%= select_tag 'deal[project_id]', content_tag('option', l(:label_no_change_option), :value => '') + project_tree_options_for_select(Deal.allowed_target_projects) %> +

    + <% if @available_statuses.any? %> +

    + + <%= select_tag('deal[status_id]', content_tag('option', l(:label_no_change_option), :value => '') + + options_from_collection_for_select(@available_statuses, :id, :name)) %> +

    + <% end %> + +

    + + <%= select_tag('deal[assigned_to_id]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_nobody), :value => 'none') + + options_from_collection_for_select(@assignables, :id, :name)) %> +

    + + + <% @custom_fields.each do |custom_field| %> +

    <%= custom_field_tag_for_bulk_edit('deal', custom_field, @projects) %>

    + <% end %> + + <% @deals.first.custom_field_values.each do |value| %> +

    + <% value.value = '' %> + <%= custom_field_tag_with_label :contact, value %> +

    + <% end -%> + +
    + +
    + <% if @available_categories.any? %> +

    + + <%= select_tag('deal[category_id]', content_tag('option', l(:label_no_change_option), :value => '') + + content_tag('option', l(:label_none), :value => 'none') + + options_from_collection_for_select(@available_categories, :id, :name)) %> +

    + <% end %> + +

    + + <%= select_tag "deal[currency]", content_tag('option', l(:label_no_change_option), :value => '') + options_for_select(collection_for_currencies_select(ContactsSetting.default_currency, ContactsSetting.major_currencies)) %> + +

    +

    + + <%= text_field_tag "deal[due_date]", "", :size => 12 %><%= calendar_for('deal_due_date') %> +

    +

    + + <%= select_tag "deal[probability]", content_tag('option', l(:label_no_change_option), :value => '') + options_for_select((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %> +

    + +
    + + + +
    + +
    <%= l(:field_notes) %> +<%= text_area_tag 'note[content]', '', :cols => 60, :rows => 10, :class => 'wiki-edit' %> +<%= wikitoolbar_for 'note_content' %> +
    +
    + +

    <%= submit_tag l(:button_submit) %>

    +<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/context_menu.html.erb b/plugins/redmine_contacts/app/views/deals/context_menu.html.erb new file mode 100644 index 0000000..fa0d775 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/context_menu.html.erb @@ -0,0 +1,54 @@ +
      + <% if !@deal.nil? %> +
    • <%= context_menu_link l(:button_edit), {:controller => 'deals', :action => 'edit', :id => @deal}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
    • + + + <% if User.current.logged? %> +
    • <%= watcher_link(@deal, User.current) %>
    • + <% end %> + + <% else %> +
    • <%= context_menu_link l(:button_edit), {:controller => 'deals', :action => 'bulk_edit', :ids => @deals.collect(&:id)}, + :class => 'icon-edit', :disabled => !@can[:edit] %>
    • + <% end %> + + <% unless @project.nil? || @project.deal_categories.empty? -%> +
    • + <%= l(:field_category) %> +
        + <% @project.deal_categories.each do |u| -%> +
      • <%= context_menu_link u.name, {:controller => 'deals', :action => 'bulk_update', :ids => @deals.collect(&:id), :deal => {'category_id' => u}, :back_url => @back}, :method => :post, + :selected => (@deal && u == @deal.category), :disabled => !@can[:edit] %>
      • + <% end -%> +
      • <%= context_menu_link l(:label_none), {:controller => 'deals', :action => 'bulk_update', :ids => @deals.collect(&:id), :deal => {'category_id' => 'none'}, :back_url => @back}, :method => :post, + :selected => (@deal && @deal.category.nil?), :disabled => !@can[:edit] %>
      • +
      +
    • + <% end -%> + + <% unless @project.nil? || @project.deal_statuses.empty? -%> +
    • + <%= l(:field_contact_status) %> +
        + <% @project.deal_statuses.each do |s| -%> +
      • <%= context_menu_link s.name, {:controller => 'deals', :action => 'bulk_update', :ids => @deals.collect(&:id), :deal => {'status_id' => s}, :back_url => @back}, :method => :post, + :selected => (@deal && s == @deal.status), :disabled => !@can[:edit] %>
      • + <% end -%> +
      +
    • + + <% end -%> + + +
    • <%= context_menu_link l(:button_delete), {:controller => 'deals', :action => 'bulk_destroy', :ids => @deals.collect(&:id), :project_id => @project, :back_url => @back}, + :method => :delete, :data => {:confirm => l(:text_are_you_sure)}, :class => 'icon-del', :disabled => !@can[:delete] %> +
    • + <% unless @deal && Redmine::VERSION.to_s >= '3.3'%> +
    • + <%= context_menu_link l(:button_filter), _project_deals_path(@project, :set_filter => 1, :ids => @deals.map(&:id).join(','), :object_type => "deal"), + :class => 'icon-list' %> +
    • + <% end %> + +
    diff --git a/plugins/redmine_contacts/app/views/deals/edit.html.erb b/plugins/redmine_contacts/app/views/deals/edit.html.erb new file mode 100644 index 0000000..ac8ae62 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/edit.html.erb @@ -0,0 +1,16 @@ +

    <%= l(:label_crm_deal_edit_information) %>

    + +<%= labelled_form_for :deal, @deal, :url => {:action => 'update', :id => @deal}, :html => {:method => :put, :id => "deal_form"} do |f| %> + <%= error_messages_for 'deal' %> +
    +
    + <%= render :partial => 'form', :locals => {:f => f} %> +
    +
    + <%= submit_tag l(:button_save) -%> +<% end -%> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= robot_exclusion_tag %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/index.api.rsb b/plugins/redmine_contacts/app/views/deals/index.api.rsb new file mode 100644 index 0000000..81c0daa --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/index.api.rsb @@ -0,0 +1,32 @@ +api.array :deals, api_meta(:total_count => @deals_count, :offset => @offset, :limit => @limit) do + @deals.each do |deal| + api.deal do + api.id deal.id + api.name deal.name + api.price deal.price + api.currency deal.currency + api.price_type deal.price_type + api.duration deal.duration + api.probability deal.probability + api.due_date deal.due_date + api.background deal.background + api.project(:id => deal.project_id, :name => deal.project.name) unless deal.project.nil? + api.status(:id => deal.status_id, :name => deal.status.name) unless deal.status.nil? + api.category(:id => deal.category_id, :name => deal.category.name) unless deal.category.nil? + api.author(:id => deal.author_id, :name => deal.author.name) unless deal.author.nil? + api.contact(:id => deal.contact_id, :name => deal.contact.name) unless deal.contact.nil? + api.assigned_to(:id => deal.assigned_to_id, :name => deal.assigned_to.name) unless deal.assigned_to.nil? + + api.array :related_contacts do + deal.related_contacts.each do |contact| + api.contact(:id => contact.id, :name => contact.name) + end + end if deal.related_contacts.any? + + render_api_custom_values deal.custom_field_values, api + + api.created_on deal.created_on + api.updated_on deal.updated_on + end + end +end diff --git a/plugins/redmine_contacts/app/views/deals/index.html.erb b/plugins/redmine_contacts/app/views/deals/index.html.erb new file mode 100644 index 0000000..f96a750 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/index.html.erb @@ -0,0 +1,151 @@ +<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> +
    + <% if !@query.new_record? && @query.editable_by?(User.current) %> + <%= link_to l(:button_contacts_edit_query), edit_crm_query_path(@query, :object_type => "deal"), :class => 'icon icon-edit' %> + <%= link_to l(:button_contacts_delete_query), crm_query_path(@query, :object_type => "deal"), :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' %> + <% end %> + <%= link_to l(:label_crm_deal_new), {:controller => 'deals', :action => 'new', :project_id => @project || Deal.allowed_target_projects.first }, :class => 'icon icon-add' if User.current.allowed_to?(:add_deals, @project, {:global => true}) && Deal.allowed_target_projects.any? %> + <%= link_to_if_authorized l(:label_crm_import), {:controller => 'deal_imports', :action => 'new', :project_id => @project}, :class => 'icon icon-import', :id => 'import_from_csv' %> +
    + +<% html_title(@query.new_record? ? l(:label_deal_plural) : @query.name) %> + +<%= form_tag({ :controller => 'deals', :action => 'index', :project_id => @project }, :method => :get, :id => 'query_form') do %> + + + +

    + + <%= @query.new_record? ? l(:label_deal_plural) : h(@query.name) %> + + + + <%= text_field_tag(:search, params[:search], :autocomplete => "off", :class => "live_search_field", :placeholder => l(:label_crm_contact_search) ) %> + + + +

    +<%= hidden_field_tag 'set_filter', '1' %> +<%= hidden_field_tag 'object_type', 'deal' %> +
    +
    "> + <%= l(:label_filter_plural) %> +
    "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
    +
    + +
    + +<%= render :partial => "crm_calendars/buttons" if deals_list_style == 'crm_calendars/crm_calendar' %> + +

    + <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> + <% if @query.new_record? && User.current.allowed_to?(:save_contacts_queries, @project, :global => true) %> + <%= link_to_function l(:button_save), + "$('#query_form').attr('action', '#{ @project ? new_project_crm_query_path(@project) : new_crm_query_path }'); submit_query_form('query_form')", + :class => 'icon icon-save' %> + + <% end %> +

    +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +
    + <% if @deals.empty? %> +

    <%= l(:label_no_data) %>

    + <% else %> + <%= render :partial => deals_list_style %> + <%= pagination_links_full @deals_pages, @deals_count %> + <% end %> +
    + + <% other_formats_links do |f| %> + <%= f.link_to 'CSV', :url => filtered_params %> + <% end if User.current.allowed_to?(:export_contacts, @project, :global => true) %> +<% end %> + +<% if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? %> + <%= context_menu %> +<% else %> + <%= context_menu url_for( {:controller => "deals", :action => "context_menu"} ) %> +<% end %> + +<% content_for :sidebar do %> + <%= call_hook(:view_deals_sidebar_top, :deals => @deals) %> + <%= render :partial => 'common/sidebar' %> + <%= render :partial => 'deals_statistics' %> + <%= render_sidebar_crm_queries('deal') %> + <%= call_hook(:view_deals_sidebar_after_statistics, :deals => @deals) %> + <%= render :partial => 'notes/last_notes', :object => @last_notes %> + <%= render :partial => 'common/recently_viewed' %> + <%= call_hook(:view_deals_sidebar_bottom, :deals => @deals) %> +<% end unless (deals_list_style == 'list_board') %> + + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + +<% end %> + diff --git a/plugins/redmine_contacts/app/views/deals/new.html.erb b/plugins/redmine_contacts/app/views/deals/new.html.erb new file mode 100644 index 0000000..c17b3bc --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/new.html.erb @@ -0,0 +1,33 @@ +

    <%= l(:label_crm_deal_new) %>

    + +<%= labelled_form_for :deal, @deal, :url => {:action => 'create', :project_id => @project}, :html => {:id => "deal_form"} do |f| %> + <%= error_messages_for 'deal' %> + <%= hidden_field_tag 'copy_from', params[:copy_from] if params[:copy_from] %> +
    +
    + <%= render :partial => 'form', :locals => {:f => f} %> +
    + + <% if false && @deal.safe_attribute?('watcher_user_ids') -%> +

    + + <%= watchers_checkboxes(@deal, @available_watchers) %> + + + <%= link_to l(:label_search_for_watchers), + {:controller => 'watchers', :action => 'new', :project_id => @deal.project}, + :remote => true, + :method => 'get' %> + +

    + <% end %> +
    + + <%= submit_tag l(:button_save) -%> + <%= submit_tag l(:button_create_and_continue), :name => 'continue' %> +<% end -%> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= robot_exclusion_tag %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/show.api.rsb b/plugins/redmine_contacts/app/views/deals/show.api.rsb new file mode 100644 index 0000000..594abbd --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/show.api.rsb @@ -0,0 +1,45 @@ +api.deal do + api.id @deal.id + api.name @deal.name + api.price @deal.price + api.currency @deal.currency + api.price_type @deal.price_type + api.duration @deal.duration + api.probability @deal.probability + api.due_date @deal.due_date + api.background @deal.background + api.project(:id => @deal.project_id, :name => @deal.project.name) unless @deal.project.nil? + api.status(:id => @deal.status_id, :name => @deal.status.name) unless @deal.status.nil? + api.category(:id => @deal.category_id, :name => @deal.category.name) unless @deal.category.nil? + api.author(:id => @deal.author_id, :name => @deal.author.name) unless @deal.author.nil? + api.contact(:id => @deal.contact_id, :name => @deal.contact.name) unless @deal.contact.nil? + api.assigned_to(:id => @deal.assigned_to_id, :name => @deal.assigned_to.name) unless @deal.assigned_to.nil? + render_api_custom_values @deal.custom_field_values, api + api.created_on @deal.created_on + api.updated_on @deal.updated_on + + api.array :related_contacts do + @deal.related_contacts.each do |contact| + api.contact(:id => contact.id, :name => contact.name) + end + end if @deal.related_contacts.any? + + if authorize_for(:notes, :show) + api.array :notes do + @deal.notes.each do |note| + api.note do + api.id note.id + api.content note.content + api.type_id note.type_id + api.author(:id => note.author_id, :name => note.author.name) unless note.author.nil? + api.created_on note.created_on + api.updated_on note.updated_on + end + end + end if include_in_api_response?('notes') && @deal.notes.present? && User.current.allowed_to?(:view_deals, @project) + end + + + call_hook(:api_deals_show) + +end diff --git a/plugins/redmine_contacts/app/views/deals/show.html.erb b/plugins/redmine_contacts/app/views/deals/show.html.erb new file mode 100644 index 0000000..8851479 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/show.html.erb @@ -0,0 +1,129 @@ +
    + <%= watcher_link(@deal, User.current) %> + <%= link_to l(:button_edit), edit_deal_path(@deal), :class => 'icon icon-edit' if User.current.allowed_to?(:edit_deals, @project) %> + <%= link_to l(:button_duplicate), new_project_deal_path(@project, :copy_from => @deal), :class => 'icon icon-duplicate' if User.current.allowed_to?(:add_deals, @project) %> + <%= link_to l(:button_delete), deal_path(@deal), :data => {:confirm => l(:text_are_you_sure)}, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:delete_deals, @project) %> +
    +

    <%= "#{l(:label_deal)} ##{@deal.id}" %>

    +
    + + + + + + <% if !@deal.price.blank? || !@deal.due_date.blank? || !@deal.probability.blank? %> + + <% end %> + + +
    <%= avatar_to(@deal, :size => "64") %> +

    <%= @deal.contact.name + ": " if @deal.contact %> <%= @deal.name %>

    +

    + <%= authoring @deal.created_on, @deal.author %>. + <% if @deal.created_on != @deal.updated_on %> + <%= l(:label_updated_time, time_tag(@deal.updated_on)).html_safe %>. + <% end %> +

    +

    <%= @deal.category %>

    + <% if @deal.status && @project.deal_statuses.any? %> +
    + <%= deal_status_tag(@deal.status) %> + <% if authorize_for('deals', 'edit') %> + + <%= link_to l(:label_crm_deal_change_status), {}, :onclick => "$('#edit_status_form').show(); $('#deal-status').hide(); return false;", :id => 'edit_status_link' %> + + <% end %> +
    + <%= form_tag( {:controller => 'deals', + :action => 'update', + :project_id => @project, + :id => @deal }, + :method => :put, + :multipart => true, + :id => "edit_status_form", + :style => "display:none; size: 100%" ) do %> + <%= select :deal, :status_id, options_for_select(collection_for_status_select, @deal.status_id.to_s), { :include_blank => false } %> + <%= submit_tag l(:button_save), :class => "button-small" %> + <%= link_to l(:button_cancel), {}, :onclick => "$('#edit_status_form').hide(); $('#deal-status').show(); return false;" %> +
    + + <% end %> + <% end %> +
    +
      + <% if !@deal.price.blank? %> +
    • <%= @deal.price_to_s %>
    • + <% end %> + + <% if !@deal.due_date.blank? %> +
    • <%= format_date(@deal.due_date) %>
    • + <% end %> + + <% if !@deal.probability.blank? %> +
    • <%= @deal.probability %>%
    • + <% end %> + +
    +
    + <% if RedmineContacts.products_plugin_installed? %> + <% if @deal.lines.present? %> + <% @lines_name = :label_deal_items %> + <%= render :partial => 'shared/product_lines', :locals => { :parent_object => @deal, :total_price => @deal.price_to_s } %> + <% end %> + <% end %> + + <%= call_hook(:view_deals_show_details_bottom, {:deal => @deal }) %> + + <% if authorize_for('notes', 'create') %> +
    + <%= render :partial => 'notes/add', :locals => {:note_source => @deal} %> + <% end %> +
    + +
    +

    <%= l(:label_crm_note_plural) %>

    +
    + <% @deal_events.each do |deal_event| %> + <% if deal_event[:object].is_a?(DealNote) %> + <%= render :partial => 'notes/note_item', :object => deal_event[:object], :locals => {:note_source => @deal} %> + <% end %> + <% if deal_event[:object].is_a?(DealProcess) %> + <%= render :partial => 'process_item', :object => deal_event[:object], :locals => {:note_source => @deal} %> + <% end %> + <% end %> +
    + +
    + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + <%= call_hook(:view_deals_sidebar_top, :deal => @deal) %> + <%= render :partial => 'attributes' %> + <%= render :partial => 'common/responsible_user', :object => @deal %> + <%= render :partial => 'deal_contacts/contacts' %> + <%= render :partial => 'deals_issues/issues' %> + <%= render :partial => 'common/notes_attachments', :object => @deal_attachments %> + + <% if !@deal.background.blank? %> +

    <%= l(:label_crm_background_info) %>

    +
    <%= textilizable(@deal, :background) %>
    + <% end %> + + <% if User.current.allowed_to?(:add_issue_watchers, @project) || + (@deal.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %> +
    + <%= render :partial => 'watchers/watchers', :locals => {:watched => @deal} %> +
    + <% end %> + + <%= render :partial => 'common/recently_viewed' %> + <%= call_hook(:view_deals_sidebar_bottom, :deal => @deal) %> +<% end %> + +<% html_title "#{l(:label_deal)} ##{@deal.id}: #{@deal.name}" %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> + +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals/update_form.js.erb b/plugins/redmine_contacts/app/views/deals/update_form.js.erb new file mode 100644 index 0000000..e490932 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/update_form.js.erb @@ -0,0 +1 @@ +$('#all_attributes').html('<%= escape_javascript(render :partial => 'form') %>'); diff --git a/plugins/redmine_contacts/app/views/deals/update_total.js.erb b/plugins/redmine_contacts/app/views/deals/update_total.js.erb new file mode 100644 index 0000000..28bd671 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals/update_total.js.erb @@ -0,0 +1,2 @@ +$('.total').html('<%= escape_javascript(render :partial => 'board_total') %>'); +$('.deals_counts').html('<%= escape_javascript(render :partial => 'board_deals_counts') %>'); diff --git a/plugins/redmine_contacts/app/views/deals_issues/_form.html.erb b/plugins/redmine_contacts/app/views/deals_issues/_form.html.erb new file mode 100644 index 0000000..07c79d4 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals_issues/_form.html.erb @@ -0,0 +1,23 @@ +<% if User.current.allowed_to?(:edit_deals, @project) %> + <% @issue.build_deals_issue(:deal_id => '') if @issue.deals_issue.blank? %> + + <%= form.fields_for :deals_issue do |f| %> +
    +
    +
    +

    + <% deal = @issue.deals_issue.deal %> + <%= f.select :deal_id, + options_for_select([[deal.try(:name), deal.try(:id)]], deal.try(:id)), + :label => l(:label_deal) %> +

    +
    +
    +
    + + <%= javascript_tag do %> + initDealSelect2('issue_deals_issue_attributes_deal_id', '<%= auto_complete_deals_path(:project_id => @project) %>', ' '); + <% end %> + <% end %> + +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals_issues/_issues.html.erb b/plugins/redmine_contacts/app/views/deals_issues/_issues.html.erb new file mode 100644 index 0000000..e36d878 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals_issues/_issues.html.erb @@ -0,0 +1,20 @@ +
    + +
    + <%= link_to l(:label_issue_new), new_project_issue_path(@project, :deal_id => @deal) if User.current.allowed_to?(:add_issues, @project) %> +
    + +

    <%= @deal_issues.count > 0 ? link_to("#{l(:label_issue_plural)} (#{@deal_issues.count})", {:controller => 'issues', + :action => 'index', + :set_filter => 1, + :f => [:deal, :status_id], + :v => {:deal => [@deal.id]}, + :op => {:deal => '=', :status_id => '*'}}) : "#{l(:label_issue_plural)}" %>

    + +<% if @deal_issues.any? %> + + <%= render :partial => 'contacts_issues/issue_item', :collection => @deal_issues %> +
    +<% end %> + +
    diff --git a/plugins/redmine_contacts/app/views/deals_issues/_show.html.erb b/plugins/redmine_contacts/app/views/deals_issues/_show.html.erb new file mode 100644 index 0000000..fd988b1 --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals_issues/_show.html.erb @@ -0,0 +1,5 @@ +<% if issue && issue.deal && issue.deal.visible? %> + <%= issue_fields_rows do |rows| + rows.left l(:label_deal), deal_tag(issue.deal).html_safe + end%> +<% end %> diff --git a/plugins/redmine_contacts/app/views/deals_pipeline/index.html.erb b/plugins/redmine_contacts/app/views/deals_pipeline/index.html.erb new file mode 100644 index 0000000..34bafac --- /dev/null +++ b/plugins/redmine_contacts/app/views/deals_pipeline/index.html.erb @@ -0,0 +1,64 @@ +

    <%= h l(:label_crm_sales_funnel) %>

    +<%= form_tag({ :controller => 'deals_pipeline', :action => 'index', :project_id => @project }, :method => :get, :id => 'query_form') do %> + <%= hidden_field_tag 'set_filter', '1' %> + <%= hidden_field_tag 'object_type', 'deal' %> +
    +
    "> + <%= l(:label_filter_plural) %> +
    "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
    +
    +
    +

    + <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> + <% if @query.new_record? && User.current.allowed_to?(:save_contacts_queries, @project, :global => true) %> + <%= link_to_function l(:button_save), + "$('#query_form').attr('action', '#{ @project ? new_project_crm_query_path(@project) : new_crm_query_path }'); submit_query_form('query_form')", + :class => 'icon icon-save' %> + + <% end %> +

    +<% end %> +<% if @deal_statuses.any? %> + + + + + + + + + + <% @deal_statuses.each_with_index do |deal_status, index| %> + <% status_scope = @processor.deals_for_status(deal_status) %> + + + + + + <% end %> + + + + + +
    <%= h l(:label_crm_deal_status) %><%= h l(:label_crm_count) %><%= h l(:label_total) %>
    + <%= deal_status_tag(deal_status) %> + + + <%= h status_scope.size %> + + + + <%= prices(status_scope) %> + +
    <%= "#{l(:label_total)} (#{@processor.count}):" %> + <%= prices(@processor.scope) %> +
    +<% end %> + +<% content_for :header_tags do %> +<%= javascript_include_tag 'select_list_move' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/_preview.html.erb b/plugins/redmine_contacts/app/views/importers/_preview.html.erb new file mode 100644 index 0000000..e49c079 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/_preview.html.erb @@ -0,0 +1,18 @@ +
    +
    + <%= l(:label_file_content_preview) %> + + + <% @import.first_rows.each do |row| %> + + <%= row.map {|c| content_tag 'td', truncate(c.to_s, :length => 50) }.join("").html_safe %> + + <% end %> +
    +
    +
    + +

    + <%= button_tag("\xc2\xab " + l(:label_previous), :name => 'previous') %> + <%= submit_tag l(:button_import) %> +

    diff --git a/plugins/redmine_contacts/app/views/importers/kernel_new.erb b/plugins/redmine_contacts/app/views/importers/kernel_new.erb new file mode 100644 index 0000000..ce87830 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/kernel_new.erb @@ -0,0 +1,12 @@ +

    <%= l(:label_crm_csv_import) %>

    + +<%= form_tag(importer_link, :multipart => true, :project => @project) do %> + <%= error_messages_for @import %> +
    + <%= l(:label_select_file_to_import) %> (CSV) +

    + <%= file_field_tag 'file' %> +

    +
    +

    <%= submit_tag l(:label_next).html_safe + " »".html_safe, :name => nil %>

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/mapping.html.erb b/plugins/redmine_contacts/app/views/importers/mapping.html.erb new file mode 100644 index 0000000..eccb53d --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/mapping.html.erb @@ -0,0 +1,12 @@ +

    <%= l(:label_crm_csv_import) %>

    + +<%= render :partial => "importers/mapping/#{@import.klass.name.downcase}" %> + +<%= javascript_tag do %> +$(document).ready(function() { + $('#import-form').submit(function(){ + $('#import-details').show().addClass('ajax-loading'); + $('#import-progress').progressbar({value: 0, max: <%= @import.total_items || 0 %>}); + }); +}); +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/mapping/_contact.html.erb b/plugins/redmine_contacts/app/views/importers/mapping/_contact.html.erb new file mode 100644 index 0000000..febf707 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/mapping/_contact.html.erb @@ -0,0 +1,10 @@ +<%= form_tag(mapping_project_contact_import_path(:id => @import, :project => @project), :id => "import-form") do %> +
    + <%= l(:label_fields_mapping) %> +
    + <%= render :partial => 'importers/mapping/contact_fields_mapping' %> +
    +
    + +<%= render :partial => 'importers/preview' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/mapping/_contact_fields_mapping.html.erb b/plugins/redmine_contacts/app/views/importers/mapping/_contact_fields_mapping.html.erb new file mode 100644 index 0000000..251ca6f --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/mapping/_contact_fields_mapping.html.erb @@ -0,0 +1,86 @@ +
    +
    +

    + + <%= mapping_select_tag @import, 'is_company' %> +

    +

    + + <%= mapping_select_tag @import, 'first_name', :required => true %> +

    +

    + + <%= mapping_select_tag @import, 'middle_name' %> +

    +

    + + <%= mapping_select_tag @import, 'last_name' %> +

    +

    + + <%= mapping_select_tag @import, 'job_title' %> +

    +

    + + <%= mapping_select_tag @import, 'company' %> +

    +

    + + <%= mapping_select_tag @import, 'phone' %> +

    +

    + + <%= mapping_select_tag @import, 'email' %> +

    +

    + + <%= mapping_select_tag @import, 'tag_list' %> +

    + <% @custom_fields.each do |field| %> +

    + + <%= mapping_select_tag @import, "cf_#{field.id}" %> +

    + <% end %> +
    + +
    +

    + + <%= mapping_select_tag @import, 'address_street' %> +

    +

    + + <%= mapping_select_tag @import, 'address_city' %> +

    +

    + + <%= mapping_select_tag @import, 'address_state' %> +

    +

    + + <%= mapping_select_tag @import, 'address_zip' %> +

    +

    + + <%= mapping_select_tag @import, 'address_country_code' %> +

    +

    + + <%= mapping_select_tag @import, 'skype_name' %> +

    +

    + + <%= mapping_select_tag @import, 'website' %> +

    +

    + + <%= mapping_select_tag @import, 'birthday' %> +

    +

    + + <%= mapping_select_tag @import, 'background' %> +

    +
    +
    + diff --git a/plugins/redmine_contacts/app/views/importers/mapping/_deal.html.erb b/plugins/redmine_contacts/app/views/importers/mapping/_deal.html.erb new file mode 100644 index 0000000..1e017fd --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/mapping/_deal.html.erb @@ -0,0 +1,10 @@ +<%= form_tag(mapping_project_deal_import_path(:id => @import, :project => @project), :id => "import-form") do %> +
    + <%= l(:label_fields_mapping) %> +
    + <%= render :partial => 'importers/mapping/deal_fields_mapping' %> +
    +
    + +<%= render :partial => 'importers/preview' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/mapping/_deal_fields_mapping.html.erb b/plugins/redmine_contacts/app/views/importers/mapping/_deal_fields_mapping.html.erb new file mode 100644 index 0000000..266b32f --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/mapping/_deal_fields_mapping.html.erb @@ -0,0 +1,51 @@ +
    +
    +

    + + <%= mapping_select_tag @import, 'name', :required => true %> +

    +

    + + <%= mapping_select_tag @import, 'background' %> +

    +

    + + <%= mapping_select_tag @import, 'currency' %> +

    +

    + + <%= mapping_select_tag @import, 'price' %> +

    + + <% @custom_fields.each do |field| %> +

    + + <%= mapping_select_tag @import, "cf_#{field.id}" %> +

    + <% end %> +
    + +
    +

    + + <%= mapping_select_tag @import, 'probability' %> +

    +

    + + <%= mapping_select_tag @import, 'status' %> +

    +

    + + <%= mapping_select_tag @import, 'contact' %> +

    +

    + + <%= mapping_select_tag @import, 'assigned_to' %> +

    +

    + + <%= mapping_select_tag @import, 'category' %> +

    +
    +
    + diff --git a/plugins/redmine_contacts/app/views/importers/new.html.erb b/plugins/redmine_contacts/app/views/importers/new.html.erb new file mode 100644 index 0000000..9cf7df9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/new.html.erb @@ -0,0 +1,18 @@ +

    <%= l(:label_crm_csv_import) %>

    + +<%= labelled_form_for @importer, :url => {:action => 'create', :project_id => @project}, :html => { :multipart => true, :id => 'import_form'} do |f| %> + <%= error_messages_for 'importer' %> +
    +

    <%= f.file_field :file, :label => l(:label_crm_csv_file), :accept => "text/csv" %>

    + <% if @importer.respond_to? :tag_list %> +

    + <%= f.text_field :tag_list, :value => "import-#{Time.now.strftime('%Y-%m-%d-%H:%M:%S')}", :label => :field_add_tags, :size => 10 %><%= tagsedit_with_source_for("#contact_import_tag_list", auto_complete_contact_tags_path(:project_id => @project)) %> +

    + <% end %> +

    + <%= f.select :quotes_type, options_for_select([[l(:label_crm_double_quotes), "\""], [l(:label_crm_single_quotes),"'"]]), :label => :label_crm_quotes_type %> +

    +
    + + <%= submit_tag l(:button_save) -%> +<% end -%> diff --git a/plugins/redmine_contacts/app/views/importers/run.html.erb b/plugins/redmine_contacts/app/views/importers/run.html.erb new file mode 100644 index 0000000..e602a09 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/run.html.erb @@ -0,0 +1,16 @@ +

    <%= l(:label_crm_csv_import) %>

    + +
    +
    0 / <%= @import.total_items.to_i %>
    +
    + +<%= javascript_tag do %> +$(document).ready(function() { + $('#import-details').addClass('ajax-loading'); + $('#import-progress').progressbar({value: 0, max: <%= @import.total_items.to_i %>}); + $.ajax({ + url: '<%= importer_run_link(@import, @project) %>', + type: 'post' + }); +}); +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/run.js.erb b/plugins/redmine_contacts/app/views/importers/run.js.erb new file mode 100644 index 0000000..50efebb --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/run.js.erb @@ -0,0 +1,11 @@ +$('#import-progress').progressbar({value: <%= @current.to_i %>}); +$('#progress-label').text("<%= @current.to_i %> / <%= @import.total_items.to_i %>"); + +<% if @import.finished? %> + window.location.href='<%= importer_show_link(@import, @project) %>'; +<% else %> + $.ajax({ + url: '<%= importer_run_link(@import, @project) %>', + type: 'post' + }); +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/settings.html.erb b/plugins/redmine_contacts/app/views/importers/settings.html.erb new file mode 100644 index 0000000..d47e4c7 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/settings.html.erb @@ -0,0 +1,26 @@ +

    <%= l(:label_crm_csv_import) %>

    + +<%= form_tag(importer_settings_link(@import, @project), :id => "import-form") do %> +
    + <%= l(:label_options) %> +

    + + <%= select_tag 'import_settings[separator]', + options_for_select([[l(:label_comma_char), ','], [l(:label_semi_colon_char), ';']], @import.settings['separator']) %> +

    +

    + + <%= select_tag 'import_settings[wrapper]', + options_for_select([[l(:label_quote_char), "'"], [l(:label_double_quote_char), '"']], @import.settings['wrapper']) %> +

    +

    + + <%= select_tag 'import_settings[encoding]', options_for_select(Setting::ENCODINGS, @import.settings['encoding']) %> +

    +

    + + <%= select_tag 'import_settings[date_format]', options_for_select(date_format_options, @import.settings['date_format']) %> +

    +
    +

    <%= submit_tag l(:label_next).html_safe + " »".html_safe, :name => nil %>

    +<% end %> diff --git a/plugins/redmine_contacts/app/views/importers/show.html.erb b/plugins/redmine_contacts/app/views/importers/show.html.erb new file mode 100644 index 0000000..47da1e0 --- /dev/null +++ b/plugins/redmine_contacts/app/views/importers/show.html.erb @@ -0,0 +1,26 @@ +

    <%= l(:label_crm_csv_import) %>

    + +<% if @import.unsaved_items.count == 0 %> +

    <%= l(:notice_import_finished, :count => @import.saved_items.count) %>

    + +
      + <% @import.saved_objects.each do |imported_object| %> +
    1. <%= importer_link_to_object imported_object %>
    2. + <% end %> + +<% else %> +

      <%= l(:notice_import_finished_with_errors, :count => @import.unsaved_items.count, :total => @import.total_items) %>

      + + + + + + + <% @import.unsaved_items.each do |item| %> + + + + + <% end %> +
      PositionMessage
      <%= item.position %><%= simple_format_without_paragraph item.message %>
      +<% end %> diff --git a/plugins/redmine_contacts/app/views/mailer/_contact.text.erb b/plugins/redmine_contacts/app/views/mailer/_contact.text.erb new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/_contact.text.erb @@ -0,0 +1 @@ + diff --git a/plugins/redmine_contacts/app/views/mailer/crm_contact_add.html.erb b/plugins/redmine_contacts/app/views/mailer/crm_contact_add.html.erb new file mode 100644 index 0000000..bd78161 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_contact_add.html.erb @@ -0,0 +1,32 @@ +<%= l(:text_crm_contact_added, :name => h(@contact.name), :author => h(@contact.author)) %> +
      + +

      <%= link_to(h("#{l(:label_contact)} ##{@contact.id}: #{@contact.name}"), @contact_url) %>

      + +
        +<% if @contact.is_company %> +
      • <%=l(:field_contact_company)%>: <%=h @contact.first_name %>
      • +
      • <%=l(:field_company_field)%>: <%=h @contact.job_title %>
      • +<% else %> +
      • <%=l(:field_contact_first_name)%>: <%=h @contact.first_name %>
      • +
      • <%=l(:field_contact_middle_name)%>: <%=h @contact.middle_name %>
      • +
      • <%=l(:field_contact_last_name)%>: <%=h @contact.last_name %>
      • +
      • <%=l(:field_contact_company)%>: <%=h @contact.company %>
      • +
      • <%=l(:field_contact_job_title)%>: <%=h @contact.job_title %>
      • +
      • <%=l(:field_birthday)%>: <%=h @contact.birthday %>
      • +<% end %> +
      • <%=l(:field_contact_phone)%>: <%=h @contact.phone %>
      • +
      • <%=l(:field_contact_email)%>: <%=h @contact.email %>
      • +
      • <%=l(:field_contact_website)%>: <%=h @contact.website %>
      • +
      • <%=l(:field_contact_skype)%>: <%=h @contact.skype_name %>
      • +
      • <%=l(:field_contact_address)%>: <%=h @contact.address %>
      • +
      • <%=l(:field_contact_tag_names)%>: <%=h @contact.tag_list %>
      • +
      • <%=l(:field_assigned_to)%>: <%=h @contact.assigned_to %>
      • +<% if @contact.respond_to?(:custom_field_values) %> + <% @contact.custom_field_values.each do |c| %> +
      • <%=h c.custom_field.name %>: <%=h show_value(c) %>
      • + <% end %> +<% end %> +
      + +<%= textilizable(@contact, :background, :only_path => false) %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_contact_add.text.erb b/plugins/redmine_contacts/app/views/mailer/crm_contact_add.text.erb new file mode 100644 index 0000000..9e585f9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_contact_add.text.erb @@ -0,0 +1,30 @@ +<%= l(:text_crm_contact_added, :name => h(@contact.name), :author => h(@contact.author)) %> + +---------------------------------------- +<%= "#{l(:label_contact)} ##{@contact.id}: #{@contact.name}" %> +<%= @contact_url %> + +<% if @contact.is_company %> +* <%=l(:field_contact_company)%>: <%=h @contact.first_name %> +* <%=l(:field_company_field)%>: <%=h @contact.job_title %> +<% else %> +* <%=l(:field_contact_first_name)%>: <%=h @contact.first_name %> +* <%=l(:field_contact_middle_name)%>: <%=h @contact.middle_name %> +* <%=l(:field_contact_last_name)%>: <%=h @contact.last_name %> +* <%=l(:field_contact_company)%>: <%=h @contact.company %> +* <%=l(:field_contact_job_title)%>: <%=h @contact.job_title %> +* <%=l(:field_birthday)%>: <%=h @contact.birthday %> +<% end %> +* <%=l(:field_contact_phone)%>: <%=h @contact.phone %> +* <%=l(:field_contact_email)%>: <%=h @contact.email %> +* <%=l(:field_contact_website)%>: <%=h @contact.website %> +* <%=l(:field_contact_skype)%>: <%=h @contact.skype_name %> +* <%=l(:field_contact_address)%>: <%=h @contact.address %> +* <%=l(:field_contact_tag_names)%>: <%=h @contact.tag_list %> +* <%=l(:field_assigned_to)%>: <%=h @contact.assigned_to %> +<% if @contact.respond_to?(:custom_field_values) %> + <% @contact.custom_field_values.each do |c| %> + * <%= c.custom_field.name %>: <%= show_value(c) %> + <% end -%> +<% end %> +<%= @contact.background %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_deal_add.html.erb b/plugins/redmine_contacts/app/views/mailer/crm_deal_add.html.erb new file mode 100644 index 0000000..47b7dd4 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_deal_add.html.erb @@ -0,0 +1,17 @@ +<%= l(:text_crm_deal_added, :name => h(@deal.full_name), :author => h(@deal.author)) %> +
      + +

      <%= link_to(h("#{l(:label_deal)} ##{@deal.id}: #{@deal.info}".html_safe), @deal_url) %>

      + +
        +
      • <%=l(:label_crm_deal_status)%>: <%=h @deal.status.name if @deal.status %>
      • +
      • <%=l(:label_crm_deal_category)%>: <%=h @deal.category %>
      • +
      • <%=l(:field_deal_contact)%>: <%=h @deal.contact.name if @deal.contact %>
      • +
      • <%=l(:field_deal_price)%>: <%=h @deal.price %>
      • +
      • <%=l(:field_assigned_to)%>: <%=h @deal.assigned_to %>
      • +<% @deal.custom_field_values.each do |c| %> +
      • <%=h c.custom_field.name %>: <%=h show_value(c) %>
      • +<% end %> +
      + +<%= textilizable(@deal, :background, :only_path => false) %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_deal_add.text.erb b/plugins/redmine_contacts/app/views/mailer/crm_deal_add.text.erb new file mode 100644 index 0000000..b639e5d --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_deal_add.text.erb @@ -0,0 +1,19 @@ +<%= l(:text_crm_deal_added, :name => h(@deal.full_name), :author => h(@deal.author)) %> + +---------------------------------------- +<%= "#{l(:label_deal)} ##{@deal.id}: #{@deal.info}".html_safe %> +<%= @deal_url %> + +* <%=l(:label_crm_deal_status)%>: <%=h @deal.status.name if @deal.status %> +* <%=l(:label_crm_deal_category)%>: <%=h @deal.category %> +* <%=l(:field_deal_contact)%>: <%=h @deal.contact.name if @deal.contact %> +* <%=l(:field_deal_price)%>: <%=h @deal.price %> +* <%=l(:field_assigned_to)%>: <%=h @deal.assigned_to %> + +<% if @deal.respond_to?(:custom_field_values) %> + <% @deal.custom_field_values.each do |c| %> + * <%= c.custom_field.name %>: <%= show_value(c) %> + <% end -%> +<% end %> + +<%= @deal.background %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.html.erb b/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.html.erb new file mode 100644 index 0000000..0e0a4db --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.html.erb @@ -0,0 +1,22 @@ +<%= l(:text_crm_deal_updated, :name => h(@deal.full_name), :author => h(@author)) %> + +
        +
      • <%=l(:text_crm_deal_status_changed, :old => (@status_was ? @status_was.name : ""), :new => (@status ? @status.name : "")) %>
      • +
      + +
      + +

      <%= link_to("#{l(:label_deal)} ##{@deal.id}: #{@deal.info}".html_safe, @deal_url) %>

      + +
        +
      • <%=l(:label_crm_deal_status)%>: <%=h @deal.status.name if @deal.status %>
      • +
      • <%=l(:label_crm_deal_category)%>: <%=h @deal.category %>
      • +
      • <%=l(:field_deal_contact)%>: <%=h @deal.contact.name if @deal.contact %>
      • +
      • <%=l(:field_deal_price)%>: <%=h @deal.price %>
      • +
      • <%=l(:field_assigned_to)%>: <%=h @deal.assigned_to %>
      • +<% @deal.custom_field_values.each do |c| %> +
      • <%=h c.custom_field.name %>: <%=h show_value(c) %>
      • +<% end %> +
      + +<%= textilizable(@deal, :background, :only_path => false) %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.text.erb b/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.text.erb new file mode 100644 index 0000000..2d6d57f --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_deal_updated.text.erb @@ -0,0 +1,18 @@ +<%= l(:text_crm_deal_updated, :name => h(@deal.full_name), :author => h(@author)) %> + +* <%=l(:text_crm_deal_status_changed, :old => (@status_was ? @status_was.name : ""), :new => (@status ? @status.name : "")) %> + +---------------------------------------- +<%= "#{l(:label_deal)} ##{@deal.id}: #{@deal.info}".html_safe %> +<%= @deal_url %> + +* <%=l(:label_crm_deal_category)%>: <%=h @deal.category %> +* <%=l(:field_deal_contact)%>: <%=h @deal.contact.name if @deal.contact %> +* <%=l(:field_deal_price)%>: <%=h @deal.price %> +* <%=l(:field_assigned_to)%>: <%=h @deal.assigned_to %> +<% if @deal.respond_to?(:custom_field_values) %> + <% @deal.custom_field_values.each do |c| %> + * <%= c.custom_field.name %>: <%= show_value(c) %> + <% end -%> +<% end %> +<%= @deal.background %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_note_add.html.erb b/plugins/redmine_contacts/app/views/mailer/crm_note_add.html.erb new file mode 100755 index 0000000..caf3213 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_note_add.html.erb @@ -0,0 +1,5 @@ +

      <%=h @note.source.project.name %>: <%= link_to(h(@note.source.name), @note_url) %>

      + +<%=h @note.author %> + +<%= textilizable(@note, :content, :only_path => false) %> diff --git a/plugins/redmine_contacts/app/views/mailer/crm_note_add.text.erb b/plugins/redmine_contacts/app/views/mailer/crm_note_add.text.erb new file mode 100755 index 0000000..308d8db --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/crm_note_add.text.erb @@ -0,0 +1,5 @@ +<%= link_to( h(@note.source.name), @note_url) %> + +<%= @note.author %> + +<%= @note.content %> diff --git a/plugins/redmine_contacts/app/views/mailer/issue_connected.html.erb b/plugins/redmine_contacts/app/views/mailer/issue_connected.html.erb new file mode 100644 index 0000000..f476f9d --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/issue_connected.html.erb @@ -0,0 +1,3 @@ +

      <%=h @contact.project.name %>: <%= link_to(h(@contact.name), @contact_url) %>

      + +<%= l(:label_crm_issue_added) %> <%= link_to(h(@issue.subject), @issue_url %> diff --git a/plugins/redmine_contacts/app/views/mailer/issue_connected.text.erb b/plugins/redmine_contacts/app/views/mailer/issue_connected.text.erb new file mode 100644 index 0000000..417a552 --- /dev/null +++ b/plugins/redmine_contacts/app/views/mailer/issue_connected.text.erb @@ -0,0 +1,3 @@ +<%=h @contact.project.name %>: <%= link_to(h(@contact.name), @contact_url) %> + +<%= l(:label_crm_issue_added) %> <%= link_to(h(@issue.subject), @issue_url %> diff --git a/plugins/redmine_contacts/app/views/my/blocks/_my_contacts.html.erb b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts.html.erb new file mode 100644 index 0000000..a8ac77e --- /dev/null +++ b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts.html.erb @@ -0,0 +1,28 @@ +

      <%= l(:label_crm_my_contact_plural) %>

      +<% contacts = Contact.visible.where(:assigned_to_id => User.current.id).limit(20) %> + +
      +
        + <% contacts.each do |contact| %> +
      • + <%= contact_tag(contact) %> + <%= "(#{contact.job_title}) " unless contact.job_title.blank? %> +
      • + <% end %> +
      +
      + +<% if contacts.length > 0 %> +

      <%= link_to l(:label_crm_contact_view_all), + :controller => 'contacts', + :action => 'index', + :set_filter => 1, + :fields => ["assigned_to_id", ""], + :operators => {"assigned_to_id"=>"="}, + :values => {"assigned_to_id" => ["me"]} %>

      +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_avatars.html.erb b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_avatars.html.erb new file mode 100644 index 0000000..e6c386e --- /dev/null +++ b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_avatars.html.erb @@ -0,0 +1,42 @@ +

      <%= l(:label_crm_my_contact_plural) %>

      +<% contacts = Contact.visible.where(:assigned_to_id => User.current.id).limit(10) %> + +<% if contacts.length > 0 %> + + <% if contacts.select{|c| !c.is_company}.any? %> +
      + <% contacts.select{|c| !c.is_company}.each do |contact| %> +
      + <%= link_to avatar_to(contact, :size => "64"), contact_path(contact), :id => "avatar" %> + <%= render_contact_tooltip(contact, :icon => true) %> +
      + <% end %> +
      + <% end %> + + <% if contacts.select{|c| c.is_company}.any? %> +
      + <% contacts.select{|c| c.is_company}.each do |contact| %> +
      + <%= link_to avatar_to(contact, :size => "64"), contact_path(contact), :id => "avatar" %> + <%= render_contact_tooltip(contact, :icon => true) %> +
      + <% end %> +
      + <% end %> + + + +

      <%= link_to l(:label_crm_contact_view_all), + :controller => 'contacts', + :action => 'index', + :set_filter => 1, + :fields => ["assigned_to_id", ""], + :operators => {"assigned_to_id"=>"="}, + :values => {"assigned_to_id" => ["me"]} %>

      +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_stats.html.erb b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_stats.html.erb new file mode 100644 index 0000000..9868459 --- /dev/null +++ b/plugins/redmine_contacts/app/views/my/blocks/_my_contacts_stats.html.erb @@ -0,0 +1,36 @@ +

      <%= l(:label_crm_my_contacts_stats) %>

      + +<% + from = Date.civil(Date.today.year, Date.today.month, 1) + to = (from >> 1) - 1 +%> + + + + + + + + + + + + <% Deal.select("#{DealStatus.table_name}.name, #{Deal.table_name}.status_id, COUNT(DISTINCT #{Deal.table_name}.price) AS count, SUM(DISTINCT #{Deal.table_name}.price) AS total_sum"). + joins("JOIN #{DealStatus.table_name} ON #{Deal.table_name}.status_id = #{DealStatus.table_name}.id"). + where({:author_id => @user.id, :created_on => from..to}). + group("#{DealStatus.table_name}.name, #{DealStatus.table_name}.color, #{Deal.table_name}.status_id").each do |status| %> + + + + + <% end %> + +
      <%= l(:label_crm_contacts_created) %><%= Contact.where(:author_id => @user.id, :created_on => from..to).count %>
      <%= l(:label_crm_deals_created) %><%= Deal.where(:author_id => @user.id, :created_on => from..to).count %>
      + <%= deal_status_tag(status.status) %> + <%= status.count %>
      + + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/my/blocks/_my_deals.html.erb b/plugins/redmine_contacts/app/views/my/blocks/_my_deals.html.erb new file mode 100644 index 0000000..85f7c76 --- /dev/null +++ b/plugins/redmine_contacts/app/views/my/blocks/_my_deals.html.erb @@ -0,0 +1,27 @@ +

      <%= l(:label_crm_my_deal_plural) %>

      +<% deals = Deal.visible.open.where(:assigned_to_id => User.current.id).limit(20) %> + +
      +
        + <% deals.each do |deal| %> +
      • + <%= deal_tag(deal) %> +
      • + <% end %> +
      +
      + +<% if deals.length > 0 %> +

      <%= link_to l(:label_crm_deal_view_all), + :controller => 'deals', + :action => 'index', + :set_filter => 1, + :fields => ["assigned_to_id", "status_id"], + :operators => {"assigned_to_id"=>"=", "status_id" => "o"}, + :values => {"assigned_to_id" => ["me"]} %>

      +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/_add.html.erb b/plugins/redmine_contacts/app/views/notes/_add.html.erb new file mode 100644 index 0000000..f06591c --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_add.html.erb @@ -0,0 +1,8 @@ +<%= form_for @note, :as => :note, :remote => true, :url => add_note_url(note_source, @project), :html => {:multipart => true, :id => "add_note_form"} do |f| %> + <%= render :partial => 'notes/form', :locals => {:f => f, :ajax_form => true} %> + + <%= submit_tag l(:button_add_note), :id => "submit_add_note_form", :class => "button-small" %> +
      + <%= link_to l(:label_crm_note_show_extras), {}, :onclick => "$('#note_attributes .extended-attributes').toggle(); $('#note_attributes').toggleClass('box'); return false;" , :id => 'show_note_form_extras', :style => "float:right;" %> +
      +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/_form.html.erb b/plugins/redmine_contacts/app/views/notes/_form.html.erb new file mode 100644 index 0000000..5965a7c --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_form.html.erb @@ -0,0 +1,22 @@ +<%= error_messages_for 'note' %> + +
      + + <%= f.text_area :content, :rows => 6, :class => 'wiki-edit' %><%# wikitoolbar_for 'note_content' %> + <% if @note && @note.custom_field_values.any? %> + + <% end %> + +
      diff --git a/plugins/redmine_contacts/app/views/notes/_last_notes.html.erb b/plugins/redmine_contacts/app/views/notes/_last_notes.html.erb new file mode 100644 index 0000000..ec4977e --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_last_notes.html.erb @@ -0,0 +1,8 @@ +<% if last_notes && last_notes.any? %> +

      <%= l(:label_crm_last_notes) %>

      +
      + <% last_notes.each do |note| %> + <%= render :partial => 'notes/note_data', :locals => {:limit => 100}, :object => note %> + <% end %> +
      +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/_note_data.html.erb b/plugins/redmine_contacts/app/views/notes/_note_data.html.erb new file mode 100644 index 0000000..9a88562 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_note_data.html.erb @@ -0,0 +1,19 @@ +<% limit = -1 if limit.blank? %> + + + + + +
      <%= link_to avatar_to(note_data.source, :size => "32"), note_source_url(note_data.source), :id => "avatar" %> +

      + <%= note_type_icon(note_data) %> + <%= link_to_source note_data.source %>, + <%= time_tag(note_data.created_on) %> + <%= l(:label_crm_time_ago) %> + <%= link_to('¶'.html_safe, {:controller => 'notes', :action => 'show', :project_id => @project, :id => note_data}, :title => l(:button_show), :class => "wiki-anchor") %> +

      +
      + <%= truncate(note_data.content, :length => limit) %> +

      <%= "#{l(:label_file_plural)}: #{note_data.attachments.collect{|a| a.filename}.join(', ')}".html_safe if note_data.attachments.any? %>

      +
      +
      diff --git a/plugins/redmine_contacts/app/views/notes/_note_header.html.erb b/plugins/redmine_contacts/app/views/notes/_note_header.html.erb new file mode 100644 index 0000000..8040b26 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_note_header.html.erb @@ -0,0 +1,15 @@ +
      + + + + + + +
      <%= link_to avatar_to(note_header.source, :size => "32"), note_source_url(note_header.source), :id => "avatar" %> +

      + <%= note_type_icon(note_header) %> + <%= l(:label_crm_note_for) %> <%= note_header.source.name %> +

      +

      <%= authoring note_header.created_on, note_header.author %>

      +
      +
      diff --git a/plugins/redmine_contacts/app/views/notes/_note_item.html.erb b/plugins/redmine_contacts/app/views/notes/_note_item.html.erb new file mode 100644 index 0000000..2f00f8c --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_note_item.html.erb @@ -0,0 +1,52 @@ +<% show_info = true if show_info.nil? %> +<% show_author = true if !show_author.nil? %> + +
      > + + + + <% if show_info %> + <% if show_author %> + + <% else %> + + <% end %> + <% end %> + + + +
      <%= link_to avatar(note_item.author, :size => "32"), note_source_url(note_item.source), :id => "avatar" %><%= link_to avatar_to(note_item.source, :size => "32"), note_source_url(note_item.source), :id => "avatar" %> +
      + <%= link_to(image_tag('edit.png'), {:controller => 'notes', :action => 'edit', :project_id => @project, :id => note_item}, :class => "delete", :title => l(:button_edit)) if note_item.editable_by?(User.current, @project) %> + + <%= link_to(image_tag('delete.png'), + {:controller => :notes, :action => 'destroy', :id => note_item, :project_id => @project}, + :remote => true, + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => "delete", :title => l(:button_delete)) if note_item.destroyable_by?(User.current, @project) %> +
      +

      + <%= note_type_icon(note_item) %> + <%= "#{note_item.subject} - " unless note_item.subject.blank? %> + <%= link_to_source(note_item.source) + "," if show_info %> + <%= authoring_note note_item.created_on, note_item.author %> + <%= link_to('¶'.html_safe, {:controller => 'notes', :action => 'show', :project_id => @project, :id => note_item}, :title => l(:button_show), :class => "wiki-anchor") %> +

      +
      + <% note_item.custom_values.each do |custom_value| %> + <% if !custom_value.value.blank? %> +

      <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>

      + <% end %> + <% end %> +
      +
      +
      + <%= note_content(note_item) %> +
      + <%= auto_contacts_thumbnails(note_item) %> + <%= render :partial => 'attachments/links', :locals => {:attachments => note_item.attachments, :options => {}} if note_item.attachments.any? %> +
      +
      + +
      diff --git a/plugins/redmine_contacts/app/views/notes/_notes_list.html.erb b/plugins/redmine_contacts/app/views/notes/_notes_list.html.erb new file mode 100644 index 0000000..05199be --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/_notes_list.html.erb @@ -0,0 +1,8 @@ +<% unless @notes.empty? %> + <% @notes.each do |note| %> + <%= render :partial => 'notes/note_data', :locals => {:limit => 1000}, :object => note %> + <% end %> + <%= pagination_links_full @notes_pages %> +<% else %> +

      <%=l(:label_no_data)%>

      +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/create.js.erb b/plugins/redmine_contacts/app/views/notes/create.js.erb new file mode 100644 index 0000000..b0083ef --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/create.js.erb @@ -0,0 +1,4 @@ +$('#notes').prepend("<%= escape_javascript(render(:partial => 'notes/note_item', :object => @note, :locals => {:note_source => @note_source})) %>") +$('#note_<%= @note.id %>').effect('highlight', {}, 1000); +$('#note_attachments').html("<%= escape_javascript(render(:partial => 'attachments/form')) %>"); +$('#add_note_form').get(0).reset(); diff --git a/plugins/redmine_contacts/app/views/notes/destroy.js.erb b/plugins/redmine_contacts/app/views/notes/destroy.js.erb new file mode 100644 index 0000000..1fb64a2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/destroy.js.erb @@ -0,0 +1 @@ +$('#note_<%= params[:id] %>').effect('fade', {}, 1000); diff --git a/plugins/redmine_contacts/app/views/notes/edit.html.erb b/plugins/redmine_contacts/app/views/notes/edit.html.erb new file mode 100644 index 0000000..53aff21 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/edit.html.erb @@ -0,0 +1,51 @@ +<%= breadcrumb link_to(@note.source.name, note_source_url(@note.source)) %> + +<%= render :partial => 'note_header', :object => @note %> + +<%= form_for @note, :url => {:controller => "notes", :action => 'update', :project_id => @project, :id => @note}, :html => { :multipart => true} do |f| %> +
      + + + + + +
      +
      <%= f.select :type_id, collection_for_note_types_select %> +
      +
      <%= f.text_field :created_on, :value => @note.created_on.to_date, :size => 15 %><%= f.text_field :note_time, :value => (@note.created_on || Time.now).to_s(:time), :size => 5 %><%= calendar_for "note_created_on" %> +
      +

      <%= f.text_area :content , :cols => 80, :rows => 8, :class => 'wiki-edit', :label=>l(:field_contact_background) %> + <%= wikitoolbar_for 'note_content' %>

      + <% if @note.custom_field_values.any? %> +
      <%= l(:label_custom_field_plural) %> + <% @note.custom_field_values.each do |value| %> +

      + <%= custom_field_tag_with_label :note, value %> +

      + <% end -%> +
      + <% end %> + <%= link_to_attachments @note, :author => false %> +
      + +
      <%= l(:label_attachment_plural) %> +

      <%= render :partial => 'attachments/form', :locals => {:container => @note} %>

      +
      +
      + <%= submit_tag l(:button_save) -%> +<% end -%> + +<% html_title "#{l(:label_crm_note_for)} #{@note.source.name}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + + <%= render :partial => 'common/recently_viewed' %> +<% end %> + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/new.html.erb b/plugins/redmine_contacts/app/views/notes/new.html.erb new file mode 100644 index 0000000..8f2d572 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/new.html.erb @@ -0,0 +1,57 @@ +<%= breadcrumb link_to(@note.source.name, note_source_url(@note.source)) %> + +<%= render :partial => 'note_header', :object => @note %> + +
      + <% form_for :note, @note, :url => {:controller => "notes", :action => 'create', :project_id => @project, :id => @note}, :html => { :multipart => true} do |f| %> + + + + + +
      +
      <%= f.select :type_id, collection_for_note_types_select, { :include_blank => true } %> +
      +
      <%= f.text_field :created_on, :size => 15 %><%= calendar_for "note_created_on" %> +
      + + + <% @note.custom_field_values.each do |value| %> +

      + <%= custom_field_tag_with_label :note, value %> +

      + <% end -%> +
      +

      <%= f.text_area :content , :cols => 80, :rows => 8, :class => 'wiki-edit', :label=>l(:field_contact_background) %> + <%= wikitoolbar_for 'note_content' %>

      + <%= link_to_attachments @note, :author => false %> +

      +
      + + <%= file_field_tag 'note_attachments[1][file]', :size => 30, :id => nil %> + <%= text_field_tag 'attachments[1][description]', '', :size => 60, :id => nil %> + + +
      + <%= link_to l(:label_add_another_file), '#', :onclick => 'addNoteFileField(); return false;' %> + (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) + +

      + <%= submit_tag l(:button_save) -%> + <% end -%> +
      + +<% html_title "#{l(:label_crm_note_for)} #{@note.source.name}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + + <%= render :partial => 'common/recently_viewed' %> +<% end %> + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/notes/show.api.rsb b/plugins/redmine_contacts/app/views/notes/show.api.rsb new file mode 100644 index 0000000..3c2e708 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/show.api.rsb @@ -0,0 +1,11 @@ +api.note do + api.id @note.id + api.source(:id => @note.source_id, :name => @note.source.name, :type => @note.source_type) unless @note.source.blank? + api.subject @note.subject + api.content @note.content + api.type_id @note.type_id + api.author(:id => @note.author_id, :name => @note.author.name) unless @note.author.nil? + render_api_custom_values @note.custom_field_values, api + api.created_on @note.created_on + api.updated_on @note.updated_on +end diff --git a/plugins/redmine_contacts/app/views/notes/show.html.erb b/plugins/redmine_contacts/app/views/notes/show.html.erb new file mode 100644 index 0000000..a1ee3e0 --- /dev/null +++ b/plugins/redmine_contacts/app/views/notes/show.html.erb @@ -0,0 +1,33 @@ +
      +<%= link_to_if_authorized(l(:button_edit), {:controller => 'notes', :action => 'edit', :project_id => @project, :id => @note}, :class => 'icon icon-edit', :title => l(:button_edit)) %> +
      + +<%= breadcrumb link_to(@note.source.name, note_source_url(@note.source)) %> + +<%= render :partial => 'note_header', :object => @note %> +<% @note.custom_values.each do |custom_value| %> + <% if !custom_value.value.blank? %> +

      <%= custom_value.custom_field.name%>: <%=h show_value(custom_value) %>

      + <% end %> +<% end %> + +
      + <%= textilizable(@note, :content) %> + <%= auto_contacts_thumbnails(@note) %> +
      + +<%= link_to_attachments @note, :author => false %> + +<% html_title "#{l(:label_crm_note_for)} #{@note.source.name}" %> + +<% content_for :sidebar do %> + <%= render :partial => 'common/sidebar' %> + + <%= render :partial => 'common/recently_viewed' %> +<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts_sidebar, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/projects/_contacts.html.erb b/plugins/redmine_contacts/app/views/projects/_contacts.html.erb new file mode 100644 index 0000000..10b75ab --- /dev/null +++ b/plugins/redmine_contacts/app/views/projects/_contacts.html.erb @@ -0,0 +1,45 @@ +<% if ContactsSetting[:contacts_show_on_projects_show, @project.id].to_i > 0 && User.current.allowed_to?(:view_contacts, @project) %> + +<% contacts = Contact.visible.by_project(@project).limit(50).order("#{Contact.table_name}.created_on DESC") %> + +<% if contacts.length > 0 %> +
      +

      <%= l(:label_contact_plural) %>

      + + <% if contacts.select{|c| !c.is_company}.any? %> +
      + <% contacts.select{|c| !c.is_company}.each do |contact| %> +
      + <%= link_to avatar_to(contact, :size => "64"), contact_path(contact), :id => "avatar" %> + <%= render_contact_tooltip(contact, :icon => true) %> +
      + <% end %> +
      + <% end %> + + <% if contacts.select{|c| c.is_company}.any? %> +
      + <% contacts.select{|c| c.is_company}.each do |contact| %> +
      + <%= link_to avatar_to(contact, :size => "64"), contact_path(contact), :id => "avatar" %> + <%= render_contact_tooltip(contact, :icon => true) %> +
      + <% end %> +
      + <% end %> + + + +

      <%= link_to l(:label_crm_contact_view_all), + :controller => 'contacts', + :action => 'index', + :project_id => project.id %>

      +
      +<% end %> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> + +<% end %> diff --git a/plugins/redmine_contacts/app/views/projects/_contacts_settings.html.erb b/plugins/redmine_contacts/app/views/projects/_contacts_settings.html.erb new file mode 100644 index 0000000..2e298c3 --- /dev/null +++ b/plugins/redmine_contacts/app/views/projects/_contacts_settings.html.erb @@ -0,0 +1,14 @@ +<%= form_tag({:controller => "contacts_settings", :action => "save", :project_id => @project, :tab => 'contacts'}, :method => :post, :class => "tabular") do %> +
      +

      + + <%= hidden_field_tag('contacts_settings[contacts_show_on_projects_show]', 0) %> + <%= check_box_tag 'contacts_settings[contacts_show_on_projects_show]', 1, ContactsSetting[:contacts_show_on_projects_show, @project.id].to_i > 0 %> +

      + + <%= call_hook(:view_contacts_project_settings_bottom, :project => @project) %> + +
      + + <%= submit_tag l(:button_save) %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/projects/_deal_statuses.html.erb b/plugins/redmine_contacts/app/views/projects/_deal_statuses.html.erb new file mode 100644 index 0000000..4fdf10f --- /dev/null +++ b/plugins/redmine_contacts/app/views/projects/_deal_statuses.html.erb @@ -0,0 +1,28 @@ +

      <%= l(:label_crm_deal_status_plural) %>

      + +<% if DealStatus.all.any? %> + <%= form_tag({:controller => "deal_statuses", :action => "assing_to_project", :project_id => @project, :tab => 'deals'}, :method => :put, :class => "tabular") do %> + + + + + + + + + <% DealStatus.order(:status_type, :position).find_each do |status| %> + + + + + + + + <% end %> + +
      <%= l(:field_name) %><%=l(:field_is_default)%><%=l(:label_crm_deal_status_type)%><%= l(:field_active) %>
           <%= h(status.name) %><%= checked_image status.is_default? %><%= status.status_type_name %> + <%= check_box_tag "deal_statuses[]", status.id , @project.deal_statuses.include?(status) %> +
      + <%= submit_tag l(:button_save) %> + <% end %> +<% end %> diff --git a/plugins/redmine_contacts/app/views/projects/_deals_settings.html.erb b/plugins/redmine_contacts/app/views/projects/_deals_settings.html.erb new file mode 100644 index 0000000..3b4068e --- /dev/null +++ b/plugins/redmine_contacts/app/views/projects/_deals_settings.html.erb @@ -0,0 +1,32 @@ +

      <%= l(:label_crm_deal_category_plural) %>

      + +<% if @project.deal_categories.any? %> + + + + + + + +<% for category in @project.deal_categories %> + <% unless category.new_record? %> + + + + + <% end %> +<% end %> + +
      <%= l(:field_name) %>
      <%= category.name %> + <% if User.current.allowed_to?(:manage_deals, @project) %> + <%= link_to l(:button_edit), edit_deal_category_path(category), :class => 'icon icon-edit' %> + <%= delete_link deal_category_path(category) %> + <% end %> +
      +<% else %> +

      <%= l(:label_no_data) %>

      +<% end %> + +

      <%= link_to_if_authorized l(:label_crm_deal_category_new), :controller => 'deal_categories', :action => 'new', :project_id => @project %>

      + +<%= render :partial => "projects/deal_statuses" %> diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_contacts.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_contacts.html.erb new file mode 100644 index 0000000..4cffdb2 --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_contacts.html.erb @@ -0,0 +1,5 @@ +<% extend ContactsHelper %> + +<%= render_tabs settings_contacts_tabs %> + +<% html_title(l(:label_settings), l(:label_contact_plural)) -%> diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_contacts_deal_statuses.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_deal_statuses.html.erb new file mode 100644 index 0000000..9fa5776 --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_deal_statuses.html.erb @@ -0,0 +1,32 @@ +
      +<%= link_to l(:label_crm_deal_status_new), {:controller => "deal_statuses", :action => 'new'}, :class => 'icon icon-add' %> +
      + +

      <%=l(:label_crm_deal_status_plural)%>

      + + + + + + + + + + +<% for status in DealStatus.where({}).order(:status_type, :position) %> + "> + + + + + + +<% end %> + +
      <%=l(:field_status)%><%=l(:field_is_default)%><%=l(:label_crm_deal_status_type)%><%=l(:button_sort)%>
           <%= link_to status.name, :controller => "deal_statuses", :action => 'edit', :id => status %><%= checked_image status.is_default? %><%= status.status_type_name %><%= stocked_reorder_link(status, 'deal_status', {:controller => "deal_statuses", :action => 'update', :id => status}, :put) %> + <%= delete_link deal_status_path(status) %> +
      + +<%= javascript_tag do %> + $(function() { $("table.list tbody").positionedItems(); }); +<% end %> diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_contacts_general.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_general.html.erb new file mode 100644 index 0000000..13affae --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_general.html.erb @@ -0,0 +1,85 @@ +

      + + <%= select_tag 'settings[name_format]', options_for_select(Contact::CONTACT_FORMATS.collect{|key, value| [key, value[:setting_order]]}.sort{|a, b| a[1] <=> b[1]}.collect{|f| [Contact.new(:first_name => 'Firstname', :last_name => 'Lastname', :middle_name => 'Middlename').name(f[0]), f[0].to_s]}, @settings["name_format"] ) %> +

      +

      + + <%= select_tag 'settings[default_list_style]', options_for_select([[l(:label_crm_list_excerpt), "list_excerpt"], [l(:label_crm_list_list), "list"], [l(:label_crm_list_cards), "list_cards"]] , @settings["default_list_style"]) %> +

      + +

      + <%= label_tag 'settings_select_companies_to_deal', l(:label_crm_select_companies) %> + <%= check_box_tag 'settings[select_companies_to_deal]', 1, @settings["select_companies_to_deal"] %> +

      + +

      + <%= label_tag 'settings_cross_project_contacts', l(:label_crm_cross_project_contacts) %> + <%= check_box_tag 'settings[cross_project_contacts]', 1, @settings["cross_project_contacts"] %> +

      + +

      + + <%= text_area_tag 'settings[post_address_format]', ContactsSetting.post_address_format, :rows => 5 %> + <%= l(:label_crm_post_address_format_macros, :macros => '%street1%, %street2%, %city%, %town%, %postcode%, %zip%, %region%, %state%, %country%') %> +

      + +

      + + <%= select_tag 'settings[default_country]', options_for_select(countries_for_select, ContactsSetting.default_country) %> +

      + +

      + + + + <%= check_box_tag 'settings[contacts_show_in_top_menu]', 1, @settings["contacts_show_in_top_menu"] %> + <%= l(:label_contact_plural) %> + + + <%= check_box_tag 'settings[deals_show_in_top_menu]', 1, @settings["deals_show_in_top_menu"] %> + <%= l(:label_deal_plural) %> + + +

      + +

      + + + + <%= check_box_tag 'settings[contacts_show_in_app_menu]', 1, @settings["contacts_show_in_app_menu"] %> + <%= l(:label_contact_plural) %> + + + <%= check_box_tag 'settings[deals_show_in_app_menu]', 1, @settings["deals_show_in_app_menu"] %> + <%= l(:label_deal_plural) %> + + +

      + + +

      + <%= label_tag 'settings_note_authoring_time', l(:label_crm_contact_note_authoring_time) %> + <%= check_box_tag 'settings[note_authoring_time]', 1, @settings["note_authoring_time"] %> +

      + + +

      + <%= label_tag 'settings_show_closed_issues', l(:label_crm_contact_show_closed_issues) %> + <%= check_box_tag 'settings[show_closed_issues]', 1, @settings["show_closed_issues"] %> +

      +

      + <%= label_tag 'settings_auto_contacts_thumbnails', l(:label_crm_thumbnails_enabled) %> + <%= check_box_tag 'settings[auto_contacts_thumbnails]', 1, @settings["auto_contacts_thumbnails"] %> +

      + +<% unless Redmine::Thumbnail.convert_available? %> +

      + + <%= text_field_tag 'settings[max_contacts_thumbnail_file_size]', @settings["max_contacts_thumbnail_file_size"], :size => 6 %> <%= l(:"number.human.storage_units.units.kb") %> +

      +<% end %> + +

      + <%= label_tag 'settings_monochrome_tags', l(:label_crm_monochrome_tags) %> + <%= check_box_tag 'settings[monochrome_tags]', 1, @settings["monochrome_tags"] %> +

      diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_contacts_hidden.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_hidden.html.erb new file mode 100644 index 0000000..e5dedc9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_hidden.html.erb @@ -0,0 +1,4 @@ +

      + + <%= check_box_tag 'settings[monochrome_tags]', 1, @settings[:monochrome_tags] %> +

      diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_contacts_tags.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_tags.html.erb new file mode 100644 index 0000000..0b5aec9 --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_contacts_tags.html.erb @@ -0,0 +1,40 @@ +<% tags = Contact.all_tag_counts(:order => :name) %> + +

      <%= l(:label_crm_tags_plural) %>

      + +<% unless tags.empty? %> + + + + + + + + + + <% tags.each do |tag| %> + hascontextmenu "> + + + + + + + <% end %> + +
      + <%= link_to image_tag('toggle_check.png'), {}, + :onclick => 'toggleCRMIssuesSelection(this); return false;', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <%= l(:field_name) %>
      <%= check_box_tag("ids[]", tag.id, false, :id => nil) %><%= tag_link(tag.name) %> + <%= link_to l(:button_edit), edit_contacts_tag_path(tag), + :class => 'icon icon-edit' %> + + <%= link_to l(:button_delete), contacts_tags_path(:ids => tag), + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => 'icon icon-del' %> +
      +<% else %> +

      <%= l(:label_no_data) %>

      +<% end %> diff --git a/plugins/redmine_contacts/app/views/settings/contacts/_money.html.erb b/plugins/redmine_contacts/app/views/settings/contacts/_money.html.erb new file mode 100644 index 0000000..fc2669e --- /dev/null +++ b/plugins/redmine_contacts/app/views/settings/contacts/_money.html.erb @@ -0,0 +1,43 @@ +

      + + <%= check_box_tag 'settings[disable_taxes]', 1, ContactsSetting.disable_taxes? %> +

      + + +

      + + <%= text_field_tag 'settings[default_tax]', ContactsSetting.default_tax, :size => 6, :maxlength => 5 %> % +

      + +

      + + <%= select_tag 'settings[tax_type]', options_for_select([[l(:label_crm_tax_type_exclusive), ContactsSetting::TAX_TYPE_EXCLUSIVE], + [l(:label_crm_tax_type_inclusive), ContactsSetting::TAX_TYPE_INCLUSIVE]], ContactsSetting.tax_type) %> +

      +
      + +

      + + <%= select_tag 'settings[default_currency]', options_for_select(all_currencies.insert(0, ['', '']), ContactsSetting.default_currency) %> +

      + +

      + + <%= text_field_tag 'settings[major_currencies]', ContactsSetting.major_currencies.join(', '), :size => 40 %> +
      + <%= l(:text_comma_separated) %> +

      + +

      + + <%= select_tag 'settings[thousands_delimiter]', options_for_select([["9 999", " "], + ["9,999", ","], + ["9.999", "."]], ContactsSetting.thousands_delimiter) %> +

      + +

      + + <%= select_tag 'settings[decimal_separator]', options_for_select([["0.00", "."], + ["0,00", ","]], ContactsSetting.decimal_separator) %> +

      + diff --git a/plugins/redmine_contacts/app/views/users/_contact.html.erb b/plugins/redmine_contacts/app/views/users/_contact.html.erb new file mode 100644 index 0000000..20c65a3 --- /dev/null +++ b/plugins/redmine_contacts/app/views/users/_contact.html.erb @@ -0,0 +1,12 @@ +<% @contact = Contact.visible.where("#{Contact.table_name}.email LIKE ?", "%#{@user.mail}%").first unless @user.mail.blank? %> + +

      <%= l(:label_contact) %>

      +<% unless @contact.blank? || !@contact.visible? %> + <%= render :partial => 'contacts/contact_card', :object => @contact %> +<% end %> + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts/assets/images/arrow_merge.png b/plugins/redmine_contacts/assets/images/arrow_merge.png new file mode 100644 index 0000000000000000000000000000000000000000..7502dbb332116d4afcc7cb432802dbe4ce7068bc GIT binary patch literal 484 zcmVTYg5=lqDRUe#ScX*(E2x=9XYzD6%FONbQIbA#VsuBq0ij+PPf#`3M zpvPY0@wz6#pQ1M#cCJ4HtBQCgjp?eMD}Ow?B1lUn;`#{$MeS% z{6Rm$gJCR2mS9S5Qr^HNj0iI6M|{5JQ7pJHG!RSBu=Xz)&p=c#F*=4sGKu^2{X2Z< zASEEw`V6Q2&NEMPuTwY#5DA1Cf aOZ5%-nC^v5N+ApY00001AYTF{sq#%{`?^>43z2^zjCoLG8oHPGJk}@P)Ff}Nlc zJ+V-1gdjvrBToYb_&=5JJoz%#Q=n#p0PUsg;0_wW6hzWF z6d(Z#0TH%uy+T9>YJjQ_4o41w#S|V(NC8qn46yPY0ExQO163;l8LV%BTT)_{lxVdB zLqhH!O}vfXt+G~AtirG?JdRPE!p4;(u<%+06sOr5$!l?%`w$ZJisblGeq3lk?M znK(IF&Lv~|%q&@!^#Q8MB|waLDd+L)A8Bgth=QmR)CiKix0+Qk`lsOTj2++4X|3MA z_x{3e%gn9zko@i4h1Ih`tu0ZJsAx1qji_-~z~4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00$sRL_t(| z0qvUGYg|Pf$Im&Nd(vBKZQ91&Xct4O1`2(UK9p3UT2KojsA!ZHMEa&6{R8^oiwOGU z1$>aI1@W~gK9rI^#M)Y+k+!+Fo7A+%CUM&)o4p-Bzmwg~Ue27o^ekj|b6_%aX6ASM z{(kd2Gqcf6)6{DDF>2X(v3(tY^$4hO1nSCcf6LO+QZF*Rx~?}+7n5;~cszdB0U#I*wl_31 zoJR3Ot|+Z*Av11%2nb)g!Y_?US-t{wb#))PX1pkCYE4AZr}yvQe>zQfSDLN^Fh4)P z3C)hm(sH@9kPqH<#p*OBt|vi4-;3$n(_~0L!1SRDtbyCQ~S21wdnQx^MeNuUWQ8wOh9Q&@pP%-IsoVu&;Hoq z`Fkbk8-gaB#jA*~NMRCP2OtuOXhcDVbUc^Ykuj=#F<1h61^}xur6ekLpcKU%W#DaD zs010B>=1&jrCt)cG{~;GPEC{R|$YnREhVM z#>;>+0J0CQf)(Ht$w~kuY$O7zVG)=POIZm(GH$9M034mD1R$9NIgur?sN7ne1M}p7 z#I;uCTE3!rNjZQBfX-p@l!yuu72$eJpv_4y(hu8uO3qk`;w&=qnUbG!31IsSNpRs| z1ZUCC!|!8mjL!mY)DB$6W#hGjvG<{UHH*IFSFuG!tONihFPnyW$}o*1cHUAkwCy*w zWz%E!`#ODRY{xS@hj{&UYHI2S zSP}IZpU?Nw`1tsvM8H?jby2djD=R>Y^5Oli%j^9d0QM2O=b4wf5TTv_(XBhldw4yF z&KEfjf3S1s&M6gPUH}q=2FmZ^P_cve4w!dP@1po9k4cR%e*Ut+$+@|O?h^nwF*!N; zHgir7~6?*oy_xcmg*qab#YC0 zH9^sKv|}ER+d3NoLj?hBav^6nZV6fkJfs`t_}eBkwl7PT!`-bszfq5Y_{F zeb{oGX~hA+N?OJnArg^<{ads}77N#^!@SSo*}0Lik$L9cLf|CBH>fA*pCNqzBPaCCI^2`0P)jYl=H(0+vzEpjCQi3liEM!tD{ z*REaL(bLoO$I#HwK5_>MG8sE5QVF$X5~=mgCJK(|hHm7{N=#<1jrH)09<}H!LQF{o_X0hfvE&QxC)^YabC%5WP+0;l>o@6 zqZ*7CACv?@Y6&DZ6l=tpC8?3MbI9PKi~yM>QBVy}$q`Tu#^x0{ zEw~c^vSnSY3iDUC-?pYG0pLPnkS(j6unyZ&R!XO?U5$hzgDQ3N-Utm047?8UAxayc z#fr#^Bt5Z9PO{VJ{q=zZ2bBM#q>O+hVw|niU2^)5FE)#WAVW3?CeC+ z7P~rc1lSQ(Q=c+BR+EuVwyXo-gibAttw%sDc-hIu^$2i6rxwQ6BcK+%>||rjN5KEJ Ws1g*DWyb0N0000Q5?pHNP6ih=t5yPA=98JOh~fJLk|&ANKv*VsNrUUhFJuyu9hv;ma*PeD|1sb z>Y+6B(ON1k?Ls1ni`{d>-PzgMnb~o6oZ0z0Gv!$Q;deOv&;R@2e1}5;prXCh96}{w zm@q_`r}zJc^cMd)A0#Bt^eOfsl|Z->eQlYT4n2kVDmx(DT>U~VF7hfxg^Xe?V5al) zynOxgXX2ZJ(A21nA@hTTM8hC(k2=Q<9`(zUz2IM1*zk>|g6OOU-+UcxM-$j~H@L>D z;4hRR%ez)LUuWzl@>xPK8D!k za&Y(WfN467V8_O|^!>g%*_GlW+GOfeGnM(-o#2~rEfTrgwGbb85N*GO*u-}z@-)<# z4C&if2(B84Ep?b|IE0Djq}b=Wx$lTJ`FB94jyR)EJNQR;5p8LNUaG2srfJNS7LS8- zTtuY(CPJmTV7G6%Xk?%-YEhw=YpOxaQI4sPUrbSJYb#_~HuimeePG`WLpqTU#!&?> zEyHUX=t;9}ZfJSgh)*k~f!%ILcXu}`Dk?BEGz5WRq3y^3?Y@r4hIGa>AkW0GcN(D` z+6P678DdG2;B-0-@yN)C(c>m*XqjmUHj)=g-85?;=j3=JwU-eqIgaq#VPjZP6buXu zKoA5olfaXHNQbjPSJ@C=vwrppvFsz2eEC^0ogT1f$`DuM_`mcXW3}REYYRfTxnNeM zR+ex@MR}N5`OPnn85Thj3{Lf|nei9p{NS07HJ>oP)4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00{O;L_t(| z0qvS;bJRu2VfonHynZa61d?3kj*1t9sxHTfg8UB7E=OXmb+$T zw_4wRK9T_F^?F&q-%r78s=kvfKaSj6zc*F?>+F;Sz<4~)27^Jiu&{8QHe|Dmtji2d z0zj7L@^l4^_!!v|C<_W{lou?*u=B@DCn8lFXs^D6_L z*NVoOlMSV|5dfJqF_0??E_qDL0WuN;AWu3jpHPKYZTG(9k2svYovS&8@qXW+!=g-b z>U2`U}@9*!IjO_J$SzD!(D!{myNN8qsk;MUu zXJmE{yH;}PPmUiHTV|q$oD%~vB>Czeyc9|TAV-(=DgZiiBw7g=!we7Q9a>x>apdF2kJ;(z zX~+*aVTn;Xetq^VEHmVFRAznYJ$?E#r~tYhl~d#}K84PvYRXYIL-ftT52KjPCbB4~ zF^nbSJU2W5P}AQMUCj(*3LgWGb49#1n~W;juLeM!acX1zxg)V&1FdF^x5s1Z+FVxH zHP(RO(V5`8iA@5)jmr@)hk(x{*D6^~5CB!4mqnM$IPNWzOH8%@LMuN9h*4ExeSPVi zV_wjZlmy*S);a5)96%d}+99VfY-eXD7!R+n0}xAuQpvcOe9w;@BVD;~$yv&&y+SK= zu*My+EP82K}K>Ghjs-(n5BZ}AeByoK2C!j03>m*Gg4Vc zuED+sLzP-3-8!<}XdR=WEeQaZL9H)2BFC4Pm$M~BMqj)^^xFZBudJ+C1TbbKfSj1! zW$|#>>$lzOdDI_fy^5S=MI6yL==H;<2w5zRm1XHrQZ*op`EWfKU6TOFXGS$zPEZEp z(;y|vcYuGsdKGlCS~>A`^}>|X;N!=S18=N%2ix19b1;4HP=JAs=g*&?D}$UkSxzs0 z`z^@fhGhr94rGaW!9x-N$T0<)RbD#80GM(aPh0hcL`S=}v9`cONz4P#ot@nf{>UeU zeKSK}Pn7V;0q|B73Ab2=70JhNMInu+K!3ms=caDJ%F%$3AuFH8%k4H%UsPLP}jPV`16nCLTR z4i&BF&@)R3IljzH$0$Dou&?0F5N#0Ols}8KD5{Kk$yXWxk+6S1dr>W^!`Ja|Rdy3 zLo48-gNW#>y;>rX!{#ZLQ>eM{k_-S&^J3%W%a`V`OFz)g%*vn~{_D*fZ#Kg}Z*SX4 z?v^rld>Ul7KIt?_1+ia=H3|o4=jF#A(ycsNV_rhJ8jr#9LF7qT?p-Q8BfQ!o@d?ckh&$uK27$8gRLOF}qr{xA2Hqd*&&9`s!nYEtz?{!oG z>iXv9X3?5EPW{F?pZ{rig<^aqF*Pln~JOX~6xtfio^9Yy?u0M~|c?A4Cb2S@FZ+ry& Y2NxP-le7-~ZU6uP07*qoM6N<$f|z*lk^lez literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/date.png b/plugins/redmine_contacts/assets/images/date.png new file mode 100644 index 0000000000000000000000000000000000000000..783c83357fdf90a1c7c024358e1d768b5c09c135 GIT binary patch literal 626 zcmV-&0*(ENP)5OC%H;f`~O(q$Q#t2<^v$A>fbmv%e#dKTwK=Ku{5lS|}<-`a#7b zzTCOnnT>at)D}AMFuOZ5&%EqFN(lyumd$2ASF6=;nM~%2?gqc@U=#|4PqkX@EBo-9 z7pD#bO_RUa>*faM`8;MYfVi$JnB-zcBFc6gjl$d!bF98Q!!!(Z1_R~P?e!pt#6CHJ9S&n_n&@=9 z%GP;!@Co4c*at+6vNz7o(6en^Q1%qHrc;1)9IRaz-$@S$Z-qdC^ds3X0NvQH;KS)D z-dh&rW&@X;1cS(45z)J&BVt+tv&GMVJ%!EiW) zLBGZW)#Z+gl-Lih&?>X3SS-S#ujQ;9JRXmIB7X)8`d6ETj)D#Q2+$s|<_b7-B9Xvq zwNfqlEp%y3$uY`h{Y$(Gn5@}sqEsq95lpAkFO5dyBmP6^H-51G4J|rN2Ujt<`2YX_ M07*qoM6N<$f4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_01wwmL_t(| z0qvXHZ&k+?htILOm^%S;4Fa2M3PK^Yh(r|n6rxIP9;(7W&|DrO_41Tdsf44xR#iir zhmfd&!b25WRcRF}krXLWh$?D@78FWATny%pxq}VZKK*@bW^K;#2d(WCR zYu2pWteL&fcCl0{6&~hN_b@Au?q3_=>j-$rE%4AUfgxW;0KEP7+fy1E8s?7}F=F2E z;lrCpjvP5v^%(6yLp6WTty{M)U%h(ujIN_MZrnI@>C&Zxot>Sh*R5OE{h;M5KB#R_ zN;glKFk$tyY15vWIC0|AQKLr97&2r?eO+A$x^!NLdwY8ew^goQyL$56xpTXZA3Ofx zsZ*zRG&eUNShHqL&x0_=eK)|yjT@hwJ$v?ZGiJgym|AtmMvShY0{)g3zfw!ZsV-*L}06hOT*ftL%6aCfRwSx zuxw1!Ei-`gCK{9=_wCrR<7YQ--rOE=47M#^~i*|ku_2p29~Xy3kl`%B`nZ7@a{umP0pzPWPM z%D;^nJ!YELd!|@a042%_3Z{iNiEaAy>6Y9`mhJ8BmN3&TvXWn3J2AowU0q!t4Ack% zItjAomp{3DOH|(V!ehYP3}(%uD0L*VHgrRW4lU?F3NT-^Kw{G1!Y{`-(Z!}J9e2hsclEbb_*gE6QX>|H4OrAWs zFm>wGLStj2MFD+y9lUlDt7VLA`=mcD&MU#o40!I+_}%a^2#e?VNgq{$pG)X^UlJ>ix^rRIqYdg(8?EIe37Tke#>1Apa+{5 zw04K1aJ)2EU;IhVv}x-@bR|6wknyuVl*dC-zHt5e^(?!=N{_!K?dNvbfgiLCBq8d& zz`WV=0w0UfNIRM+X*A5C$Qh~Uk{>GXOzu~Eb@poj9%&za^wIBQRiDKf#!10D%4%=3 z-9<$*8Y#}9$1=YUf`&`)0tzHpj7*e32R+6406Tw&y&aun{lGJ4&isyKSbVo!&~Ktp z0(+WAS_D%`!;UtSIgX~m2$bNx-|A?{tUuD)ScE=v=8VD6sm-#WZOefgdYZA$g^25)k&J%5iF!=AXx7@bYr`WXofRe5 z01BZ6+QO#8h@x4taIS>awtmM;ks{udq8_eRL&qslTb1#m9TmwAI`9Bb9p`n}+M>^P z4D~}B=1{=M1{sXwq(h^wIg1u88V;{&CD#B_ey+l32*Si7&|qo`f|)l$Relg6Z}60t zc-Yu*^yQF4h@w3Gg3G+>{9+BAz`g^Qd)U1W9k8;Hd0v!O>p^Ll^k~xYIM)EVNl*i4 zN8#N7WuA#hj|%sap(T*v+21+-a?C|YE(%^WKsOAcwOW*r1sz_<{6&^e#h7fo^JY9A zw>t2(q#5QY8qAZTw$iEM)c{?!k{bb9t5ebeVrx0e1S(_ zA8i$p*okzc<{d7_5k;Bd14*HfYXH$&=Ja){0Uov%j*&qwOg{ps6s}#nW=WCry_Obh zJ_~93p^N-%c_62fmxZyN4M=c(vS(v8rfgiwBhj~BOKM0vc*i?{{ihbt?j@ z8Y0&K3R^pu`h;N~ZBlqN`?mp%}vGoOi&ssKE`m0jFR39me>VIh(-{{@-S3C zBy5w;%bChxWplz88t~LyI*(iyzRM6Wt|+-VfFCM7v=}1>w%)vsd>ohcJ1vttt=Dl( zSq^|s0$YyYNl`CLX+ml_fb&9Kv95T#H@t=ePp9*zj_oo$IHF5NJ*OntX+%|Sxdu=K z98&mn%OiFe@`+L&c{minbF6I?;G$fKw$J{zpb;tn6WY{k7)ksc2Cn12EqA z{|pAl1(TlMo}!cweC3DR-S+Bjk9eGsE}0TGVRFkg05m#;>`*d17o#AvI)Dl!0ZL#L z#-nU?f>SzESPw!xrutwDalEe6pS=xcfbobmc>^3CRxy})nOue1ysk}F)(<*2*I8Qc_Hq<_6yA&=PuuT;;5Z)h zFvKG=ZTP@La~3fod{i>8M_zcFCqVN$=Y?>&6@@HIo^-1;@ZyYIf+ zbMWB7KPY*15{?v2;3aJaIP~hP2IMGl;wzEa-FpyGJTjo6(641-W4K{(%~@hwj5OwH zBaU9Taq?KgC#O=31=|1 zLL+ZJu}FX)JdCyv_I8=)b!0Abe5QDG;dRFPT2CXuVH~BX-6g|+qM`kU zKfLd10KUoDyLaz;J=Jx1F2I8fMiot*!KNAt3Qs?gKm?pPd7{K~016=>9=Gwq7!fR$ zNSvonpDvv|dD1gKeH7^(cmne$84U7u4-WY*bX>S}{P^*|>iBy=$uC&YSX95|o&!?y z-Me?UYG412z6tmx9xx%eMVSyr%39lT*o9St!adzR1>OsA=I4ySZw73M2+UaeIy-OZ zAY>m}*wl=3Ua2gmctm9Vm{XR}Zc_N-i!Ziy^>lsIP~UJq9hV2+H3AsOIy!dj*aj_y zZ6qxTK~lm4_8txzwlXe+A_;|OR3oaFl*5VN%}D9rwZkEGXkELEWyiRIu!K+TzOUN17{JX+Q(DJ=&>P&NB_v5 zT34~IzOK-5zT@2IpMUU zY^hLtJHq_*WxIoxRe^-3Q(FjYx!1C}LY+GDaz5VnWyNE%DgW7LpS>x$-V?7jc|Z5m zSIs8b8miWrhvWA)o!(Y2U%vcT`nuyAtYa1F7nCLRF-XLz8QSzyXtSn)!Rc|=I>1?0 z*E+XQ4|OC9ycwd)wpDGhZTt4^dkf_yPfb}LW@#Ce0bqa;l*pDXUAlB#b93`E;7DvJ z=N2wp_`Hs|jYLDbo}=K|l(hMy7#tMK+5xb;^#ei2mdXBb`O@W! z`}gnPrgO!gNO7hOaU7 zKr=;Auu8wp|3KdrtkMh3N$eIF!j=cVCju(H9s;6oKItrj+5t*ugbtk!|IKIfI!S&i ze)~muey}0mH%Wl*n*p4$a>FpK%Sk$lF4p%2EA^Geld2c!UBDz|?P2wGeLvLVcsB+$ zCq^lGi#IviYLDu`v`1H~mepP!Y&6FODSp@Et;laeHLwjU)!@R`aiz2}>w zt4WIR-y##VEJg}nFU5Hh>{KSdr0cw1Vzz5so1&&&4`8R|@eb|r*YEaJ=kpGHPh{Uy1>$2_OTI zJV|;5)dWIiCa5;-zT^IH2DtB<43go1j>3bS<10PvYXf|x-SZ^~_s~bc{{Xs@J}EGT RF!ule002ovPDHLkV1mLrRUrTX literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/email.png b/plugins/redmine_contacts/assets/images/email.png new file mode 100644 index 0000000000000000000000000000000000000000..7348aed77fe6a64c2210a202f12c6eccae7fcf24 GIT binary patch literal 641 zcmV-{0)G98P)Az`{eoOom?Tf*9)f$7n8&|1&5M4#i^32;+&E? zC3Q;bRFQN#y*%%=_V)Mfa<$xe^kB0TO;vJPkN*k(2v-CI7)OaWj?&eKPos(H4wGh_ zIC;6#q1B5SMap5{(Hc0~XO7OfqZ=x{kupu8-H&9azl`L1pTuu^Znm3EA)kCoG=JuwsyNLEtY83i->Z~j3y~F)`RA1k>zTES07po!kBVS2y#L{jCt|CMY&v{ zxmqM|`OA#P2{R&)OcQd}v0kt6_Dh#`Z$i5_;q|93je3Q^PcfR{TmBHRmr;rWahz~G z2x-&;d_O~HkmKXt5Cd#Bs?-+qj3zOiUdU24KowBIUPg(gPNmxqX)Fiia~V*$y;5L( zrGNmU;81MA$F2k%oeUXQ@}N%bXz=qOij$4IYk4W=jfhDxfCz{PGXe-#ge#VfYTyoj zh4JvDePrW{lf(Oux2xG;VZmlSvDU+Qf@i=O!B`MLglhttCUHDIKkc7 zW)_+xtDiB-6!n?!)Xg|`JEwcU@0{;^SEcpYt8y%7M)9}@rIY6NZsVunRmoqVEaVTv629F8_h`M8$4N zCfYWF0(^M;6yi%PA$J`om}?pg%D5t$Z1^<3b5r|4kk-u)2T zH*bYNiIhSW#3?U`;X`eZsVL6JxbP$%hW$gxYj%TSeO{e0fs$2Anb%E#yVqf2ATDd~QQb0iF!!Wvb z9;GHOYtRA3A5685GPoc$QwBR{44fdn-%f&|_}%kKmx=vV@a|w4d?1G z{A&|1yzGF~TZ8KHTl_r*T3@s<=A;pUk6t|eFbv0%74?%+OlCdki0e@06yy3x1%Foo zZA8IP3kLWJ+!ohD8&$%-Xu;s38IP9-L1$fP3CnTSerf$50rgDty6Lr^*!3d)Z5bO| z-!b-f1e)3AZ8+nCGAzU8!LrRAB^VQ3$5hYdKYA07xLVs{S`MaSy~ut|Ll;xQn;C~< zTE=EZ#ZWme+JuKV0b`<5t|FFt=DRug$%=#6R&beFSRGVu!=XktGv>d{p(*$PrItbs zC%{@V&UayoG-zyPh^uwrJq0zft&6HKksgOU)PPF&4Gy0B97SX;1Yuth&KvTM@H8_D zI|j&k17#5C^Q5`mXHM}n5yer~ z1v@0vf}XSMShQ`!KZ;`Qm@#H#oe^ U8%xtYa{vGU07*qoM6N<$f()ing#Z8m literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/money_dollar.png b/plugins/redmine_contacts/assets/images/money_dollar.png new file mode 100644 index 0000000000000000000000000000000000000000..59af163824c44be62dfbb06df9e71769563a33e1 GIT binary patch literal 630 zcmV-+0*U>JP)!DK;9Y=n` ztyZT|d*HtN4z^AtY@ehNsx1Av5@058(+4Yyzw@m$wtszqQ04yGbJhSW-PK+Sn9kpy z6c5&sKH5aCB||O zo)n!;Re(Lq>NlD(Y>~Mm^&rNjF{+zqQYLCFW%O8ObHbFoO{oHG8H;vpFN|zULKkF} znE;czLyw9^s&5nLicTfoh(h`)V!g47a4i6T`6XNhM{_uCpf3fOH)+aUm{Pd%0$HCQ zD`b=e=_a}-!2e?bhS(t8*Bx}jxG|(Bs-8zXGwpDVhfpKF$YwKX2WZW0hO6n{GpNkbaQ6*W`Eo;3}_R5=XK< z0YsI0n43^|QWPahLjCo*dA`zf@Kp|7fal9I=-yn{U+4i*ot*tN)m|#kBeuVS2r=(P z$&2x{rKt;Dqx=3$7Mb}P++9YHsIwW%;gx^mfN*2lS#X?@Zq5b--LhKUKO1nouBZGU QkpKVy07*qoM6N<$f-OlOxc~qF literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/money_euro.png b/plugins/redmine_contacts/assets/images/money_euro.png new file mode 100644 index 0000000000000000000000000000000000000000..b322ba9296ace62bce4768154d1d451b9df80066 GIT binary patch literal 605 zcmV-j0;2tiP)sEfkJ^DF^$1U^B4aT>wGyd_KRG3vZ26WF5%bR^MyEuu}!xvI7 zQ4}Uoe0wYvMUju7*jEvjh~~QmnJ%%90SQCmB9*gJSni~-bU}(xuT=+5g+%PHky=$b ztAtNLSnMEvw{i+rv0BowTGVPSi~WZvat{y|&Z~_L5?u;UhnclI<{p)vqxEUT<*BQz zvfi)z!DrXF*rA9oQM7h$yYT+pnbu*xRfYK$;(Lka*052{Pob5$<(3yn4BoU+h_@+x zjWTEN9@;zO2(O!)yifUEJ2BIPhREJWCnrPQoj|d77oF@Qw9+?S2`ddhQ_U#es#KOk rI$x!XulBQYygdf5{#9;h@W1*4h?>zEyRGj_00000NkvXXu0mjfeSH?O literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/money_pound.png b/plugins/redmine_contacts/assets/images/money_pound.png new file mode 100644 index 0000000000000000000000000000000000000000..b71136463797cbd9bbee7601fd27ee7f4c6bff02 GIT binary patch literal 565 zcmV-50?Pe~P)~-Etk+scx`tkipse7HYewM-t*f6pyf05Vqlz=?Cx-0Cb?Vi*;@osiZSU+ z^MlKA`@6uHPjYa_ijgecBXjczsZ>Mi@*n=3>DY{yjc@Ll<4u5-uIkC+1B5~qe(Bm+ z;PP}WTC5-BG(>Ycrr0cP2>e(Vov||w@?{NPQqcRC1e-Pfm5JGwcv|N(6|e~fJ(I-# zDloE^U=A;IM9j24k1+6jQtwQ(^~)4tx?RN8H5RtL3JiJUu1H?PK~{sA>T3EHzIy}* zmsSzKc)+h{$!Hqb5*0IDhghg#C#B+dtZYJMtdcTDmQ(ayfK6N@D61f+)`{O^TU5>F zD|q=51{Xg7Sk*m2oNVGRwv#G4M?*~0FL87wP4|u}*gRCwJ{%hOFyI^y+h_dwx=&C* zPO9MKJFG`;;0{tS0gCx8iaCD)9m71|a0S+0Nv4YZV;!NihP@*VyRrsOxz2{eQ&bu=6;*0K>sc1K0N)?)o3UDjKX9=-hzDU3LGfw^jbPZm~2VXaK~yc|hm3 z{CAn)*q^jA?tbg8#{c#mw*7bm5g7w*Eoj-@@V|X;%m4lZz5h$r7rnQvH5VY-01z(O zRPcYwp-KOH_jUizSe|yDU^IgLKQC~9>87Ip#TyF#*Ke==Kk2}P|I-gm{ok^^`G5BE zjQ?57(*Nfz&v}W{fPhK<4So}R8+`k`8^k&t94FzhQIz{|S5h|M&0i`(L}E z>VL}or2k3t6aNEUxDTfRZf$N2uB|Q&Am@h73_Z}ZtNVZUvW(9`QvwfIS6Ve#mRmMh zl$#Uh1-n|?hMZ;D|HEg7-M6f;B+d&I4Tx0C)j~RL^S@K@|QrZmG~B2wH0nvUrdpNm;9CMbtL^5n^i$+aIn^?(HA4aZWV5ov6ELTdbo0FI&wK{O>*+w4vx20?>!`FrQsdJlnHR>OPy zcd~b_n$otK2Za4V;76L-DzNVtaSB-y0*E}{p()372;bw_^6ZZ}PI-92wGS&j#91PI zKs7DSe@(bk%_Y-7gGe}(^>I=@oY#w#*Bu9GZf3^F5WP>3rn}7Ut74&?PWBFvy`A)a zPP5)V!Xd&78LdA?xQ(9mjMYElVd13a#D+Z_7&Y|xU=_C-srWU*6kiZcC!$nw*)9$7 zn6CX+@=AhmkT}X@VSsa5NKe;HZuq)~1$`#h6R+ZTR#D-3j}vF!)ZOnz+5)dI4jl{{ z44Mr{P!L4~VVJN`K!!XTF*LGrKO?IK8z<8w`3e3jI8lUGNUta*C8 zn(P`s>{pjD=7Kek#B;Fw@hxAK%$F&Q6vg9J^Xf~4by_hu-=A!MJ3Znq&n~srbFGPs zH&&aMXZ>nO`|hf|ljc?VPhR!${AbO?W8x_>CU%PFA&Hm8F7cAsOREdwU~R_;ot1_u z(ruCYB-LPGn!NQdT|ZlRy+(fw^-+`=%+gee_kY4FWHg<*4sZI8+sFJD270UUORdLHO0nA4V) z%{fwsET5CQ>B?eK%uw4yQc~9?*JVo2}ze(;aRcp*ceL#HUJSllrgm5wQKR zQu+C;QrUh^8rFfA`ftFz{YAidi-`aL010qNS#tmY3ljhU3ljkVnw%H_00$~bL_t(| z0qt5_XdG1-o}Hb&C4s~wwS=0an+wsXEi@`hMWte;YQ+a%1i^}rf(5CH2ntfX5GWKy z@j;~!4f;@D;!8nH1qqF)rGzcHW74ECvDnm*CVTb!PIBUQcV=g2XV2Pp_k^=&=3M{p zJKy=|%$aynsgzdEK`&?U#9kqwqJT1Ipt1zYTmTg(Xp~R^#^U1Qofz+(WHR{xQm?M- z9s~`0kyJbp*S)&;LL!m)aCLR{WJgEGdMRux83A*1b1wnVYk@$Z&F}YnGQs&ZzXoC& z5RZpKp>~uX&8(y1-cy2|pfNi;`-Wi{?*@ZG;dJGgGNlR_pa_w!udk=#@%URicI@~7 z8A`XR$_bdCpFil)JtqVHfFS`3lomfEgApV!KGoXV`n^TfqGT(549?8V+yX0)_za%_ zPXg>BmH?N6=JWY9sOG(;rKOspAhqa3DFGPsLCE@{O-E;8LUuMuVjHj5y9+7^XV)lR zjxqu!CME)y!RNTH^O1fgB&q-eFus^sR?K^41i;}{!_a+_06XcMm2WENiDClI>`NH| zn0+rm)fF=P(#TCxZd@OL8FanRfP%arBx~OwNphRIHqgod7_MU-Qw3I4^3C_5A_;Ka zk=cOCmH=kIShIFxDgd*e1mldl8x^2!Y_lJ;33Q#OOaU-F1rB0vz!M$%;7gGb`Llmz zK44{KMGFK2R6s%d0JdWwj~iENew8Uen41zyz?J1tK*CiAHjR}LfPDbvKksG{S?lCV>gu1mZYNZm9ovR|styDIyl|=IhjbpgBA9N_Ega?HW*wL zDPn7~ZZ7B%>qobp{aI|$d2jmPBq*;`10-LBOr>CcPv%cz}oB+xn zC%m8FgGjgC8!|~@9T3oo&qSvd78V`>0ZMID<^v*;h##~5j6dN2s47%-D?B>awg8xG zR!IVn#Cev;ZvzPLgOtyko0}(dwJWr&=md<7jWxpfJuv8L9E`qBE~r%4u|>WsNh#-R|Ej~A%NQt(D(cNzA(P-ZhhZ_ zCl|meeY>@92A+8Y&bD5|tKsX?RyF4HB@4>95{t%$Q1B}V{}`XqE^bxI^P&&}Jn+!3 zV({lNJ6-tF8={dkif?b+@Q7=1B9^BWIjYINnKDupGV-1w7LRFA#W*fcgNWB#T3Y@V z1$q29AwZv+n%ajMJqBstflt_m1k70`Xa<=|_ETj z>FMElV$P5k0cX#itpngA80gJlD5zsDguEr#oFlj_v?4NdY!ZNPu&~Udt*x!mC!E0Y zJlxgQ^-nGnYzY`19uDKJ?l^!S6dpYm7weA$(Ks4nWn`{_Rg}>ds)0mcNl+0d;3!|g zPV|b^csUP60looi>eH&Ks>j*!+SRoUuvmKH)iy)+nS+p|QU(&hCk~e+pF&7qfHOV~ z8@{x%Bj+v)3?8bfsdrz!s8&%se(8zpH5r`@<0|!=n(dp{8$nI(6>XtHPQh2sFU}lN=7B82C!y42(KczXw39YqId8A8ulLKm^wg99 z0IkLP53^!=hw0bpom@}Xl62Zi-^Djdgu3PBWp86+W0t9=1YEjwsiwBJwh7OF;ZU+# zl`zBYUP9~;4a0|1DZRbDJI#H{r&yhz}`>TPv47sYykfMz!^=S-Slf(hkNd7 zw;YBn$CIEs7ziH2{Z3T-1M!-WMF2|w92gjw0b{-6P)A1 zOEJ*J2%-6P-uFsN(314vc)81apZC3Y?}U^RyNqtPD}x|FyWJM~eFHm=EXyz)4*%_2 z`9-hS>nKH+LWpCvJW_K{ee2+Qy$;K=iUCm+-8LExR4SE?bDE}l5@MQWF@R2<=i#hh zg==L9Gagssz>e{B{#CAk4$LM@iPnXHWk?Xw`LOUEW#s_FFc8NvgbK8&R^1S*OrR37 zn*ts~sNlPI6{FG%N)TkF%_zHVFy=yhaAh z=X2P$4d3_SI1Uzzg$DC~VGUH@|Ab+N@1p=a9KakdI4{r9Fsj0rE)+ zb~J|8lW*Tz02$`Zaxk^6sfOa5!ghcM1=puT<{WmU!JK!6{4kb3fZrJ*ROjE>@quHyr}*Cr;=Aa64! zj|O-=Sx^U(hAq-h=kq`Q2V>dc0LCnFFu6l&F;*d8IpiIHH4yNNu{%yB`v73}L;K|S z5BXjfZJO%>P`FNNQN^m$1zjx5c5Y{17VpEWq!t0|e{#P8+?shbj%;~D00000NkvXX Hu0mjf+-WMY literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/telephone.png b/plugins/redmine_contacts/assets/images/telephone.png new file mode 100644 index 0000000000000000000000000000000000000000..cecc436fbdcc58e9c40b9dc52112c89f3654cace GIT binary patch literal 791 zcmV+y1L*vTP)B~2~XTCyZ1O;cQ)B1udZC+u$StgkNJem9(+80{W^ak4$r zssW%66N!3M{XR?-QICiq>aimHe*MqdrL?~k9;}zz&5;}{t9NlCTfU+yw_XaW)Ch4whj=bo>I#_Vs6a)#}JGIWNaL~IW&KV7+rh@w6q zqeyD<^tSp;x2rf+j4_xmzWv1qE+$Zq3342N^(Z@89#MUG1~^fiDr4c0`S}IXK@XQ% zHvj1p{<^sONvG-0)W=J?X0zSO<%?$h<`V07dt^>zfOIP#7t)x%Dqs7rGa}Kl|zX00F>> zFMph>SIQr>nsqXP@`0y)H!7nnulA@`>O|$nn~&zf+b7SPczgGU&P=O-3Bd%670gqB zR&A7>`vby4iK&BQ)w#D$eGU*@`syR!nVp%dR;q05T11s%^d!K0PpKG@#4)`+p;(<_ zv{pF*5CSBb1{+(u+}e0Rk~WyXvPhg{I8jn7SZhg>lq^YESze(ODM8Q=9|N4VSWz

      @N(X?>z#D+p<{P{rM3vX4CIy4~Gmb;Yggc8d!hY?{n=W1EM`)J=Qv zQADe#7X`5(;=xOyUc3n6;~-k8AbOCZB0Y$DP%3y+K__juiWCnHBs2fa|9$`aOs;+B z%DRnp1VOCK?2__$Z}Y$9HTd6L|Nac#8j)N?9aafd6bBLs&FY3^#!z}-9xB?v;U}<_ zAQqSOLJ<|SdqmYTf{L#b^b8xL38FRb*^1f=5!nrUbTdXx-?>kbx)!7MN3u-TPQkLi zYtVrmgY5-%uvZl{D!zkk^+ZfyK%|hK(Pz4%7o%o)MSS+JX^NatLA^0*)~RAPN2V+X zl93=2P?;!0MnMpS!W_qKCs_tCG{e$>4FFCIu_D8ebBn^(9IYhgrSzOFEXAlYLbgcL zl}aU82?Z^uhX#Tm_#7-7z#0K}z(k4{Fx|~n1_`>VquWTgOwwmmx~+Z`qp+v5AsBWx zJ5Ox7bBV$!qdmo@L6D&h!;fo5+eLZ!+l>Xa-NJwk={$6;en-XiC~dBSaqj+Y$XCSP zhlm?Kj6C%~HL>9q1qDTOSMa!vjq`F%BM$iPcgCkJY`5lqh(wK8-xDJ5H{U z-IG&4kByzZ)$%cL{qo4?Pn|=XTEA{=7@VGX+C1@YsXvHcjIm1njY}^_uBJP7KO+u} z2|e{=*YHq)$ literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/images/user_suit.png b/plugins/redmine_contacts/assets/images/user_suit.png new file mode 100755 index 0000000000000000000000000000000000000000..b3454e15fb60fe8704a574b0ac35c4d0c902d738 GIT binary patch literal 748 zcmV|@r@oVseQC_m=rUfa}difiP=7Q#%M zshc`Wm&;AX{0?nQQx{}VMAAhV>V;rJ@&0>zF9w3lqKhA#^YHwCJm(xv5P+cnVawvp ztLfDxoZV2f8Bnl7AZ12EoSxeFR|2V6RIW~mf*~;sdI}9M6f$-Qvg7taoFz1FDj&57n{Mq4D7Y_TdR9C(EO|SzKHEmo zPOn#{+hEQ;;thBNzT#Oh`*z)EZQ(_1cpLEJT{q48TPx~`^I%-$NUju1T~|0)V}~FC6c#zk#8D6LRk;) zT0D?8Orf&Nhs26LtPbknDLzd41-|Ax#58f}=OyC^;x)af?Do-=b~%#^24dF_nR2<@_W67)1?cUEu^twUb)isDIFhhds}=iK3R>bPJ|0e?$zQO{gfSb6 zAmw}l&+|y1)FDbOh~LB|8AsbZWx#U1r$+A&&@Nx#xl}^&O@y|4t-qIL8GGyFgudOB zMUBefj6^S-TgKXw9~(9fXO}l9u&h~_&1U!^@!0B?~s|2G5wL<6_)Q&2rqn4Ysi zYDkz=-^k9mUKoqT&@0!tb`xM{bJg4sMG?(rlN1C@+IG7gZnyh)fD-9^wOWPO>qRga zgu~$&m^wiaP%IV^2m}y`M05a#41+*G!W1YTk2ASkP8&9ThNn^~CX>l%T{(BmzyFOt XHe4|Dt4X literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts/assets/javascripts/contacts.js b/plugins/redmine_contacts/assets/javascripts/contacts.js new file mode 100644 index 0000000..0f8e468 --- /dev/null +++ b/plugins/redmine_contacts/assets/javascripts/contacts.js @@ -0,0 +1,247 @@ +// Copyright (c) 2011-2013 Kirill Bezrukov + +var noteFileFieldCount = 1; + +function addNoteFileField() { + if (noteFileFieldCount >= 10) return false + noteFileFieldCount++; + var f = document.createElement("input"); + f.type = "file"; + f.name = "note_attachments[" + noteFileFieldCount + "][file]"; + f.size = 30; + var d = document.createElement("input"); + d.type = "text"; + d.name = "note_attachments[" + noteFileFieldCount + "][description]"; + d.size = 60; + + p = document.getElementById("note_attachments_fields"); + p.appendChild(document.createElement("br")); + p.appendChild(f); + p.appendChild(d); +} + +function updateCustomForm(url, form) { + $.ajax({ + url: url, + type: 'post', + data: $(form).serialize() + }); +} + +function toggleContact(event, element) +{ + if (event.shiftKey==1) + { + if (element.checked) { + checkAllContacts($$('.contacts.index td.checkbox input')); + } + else + { + uncheckAllContacts($$('.contacts.index td.checkbox input')); + } + } + else + { + Element.up(element, 'tr').toggleClassName('context-menu-selection'); + } +} + + +// Observ field function + +(function( $ ){ + + jQuery.fn.observe_field = function(frequency, callback) { + + frequency = frequency * 100; // translate to milliseconds + + return this.each(function(){ + var $this = $(this); + var prev = $this.val(); + + var check = function() { + if(removed()){ // if removed clear the interval and don't fire the callback + if(ti) clearInterval(ti); + return; + } + + var val = $this.val(); + if(prev != val){ + prev = val; + $this.map(callback); // invokes the callback on $this + } + }; + + var removed = function() { + return $this.closest('html').length == 0 + }; + + var reset = function() { + if(ti){ + clearInterval(ti); + ti = setInterval(check, frequency); + } + }; + + check(); + var ti = setInterval(check, frequency); // invoke check periodically + + // reset counter after user interaction + $this.bind('keyup click mousemove', reset); //mousemove is for selects + }); + + }; + + $.fn.insertAtCaret = function (myValue) { + + return this.each(function() { + + //IE support + if (document.selection) { + + this.focus(); + sel = document.selection.createRange(); + sel.text = myValue; + this.focus(); + + } else if (this.selectionStart || this.selectionStart == '0') { + + //MOZILLA / NETSCAPE support + var startPos = this.selectionStart; + var endPos = this.selectionEnd; + var scrollTop = this.scrollTop; + this.value = this.value.substring(0, startPos)+ myValue+ this.value.substring(endPos,this.value.length); + this.focus(); + this.selectionStart = startPos + myValue.length; + this.selectionEnd = startPos + myValue.length; + this.scrollTop = scrollTop; + + } else { + + this.value += myValue; + this.focus(); + } + }); + }; + + +})( jQuery ); + +function setupDeferredTabs(url) { + $('body').on('click', '.tab-header', function(e){ + tab = $(e.target); + $('.tab-placeholder').removeClass('active'); + name = tab.data('name'); + partial = tab.data('partial'); + placeholder = $('#tab-placeholder-' + name); + placeholder.addClass('active'); + + if (!placeholder.is('.loaded')) { + url = url + $.ajax(url, { + data: {tab_name: name, partial: partial}, + complete: function(){ + placeholder.addClass('loaded') + //replaces current URL with the "href" attribute of the current link + //(only triggered if supported by browser) + if ("replaceState" in window.history) { + window.history.replaceState(null, document.title, tab.attr('href')); + } + return undefined; + }, + dataType: 'script' + }) + } + else { + if ("replaceState" in window.history) { + window.history.replaceState(null, document.title, tab.attr('href')); + } + } + }) +}; + +function selectAllOptions(id) { + var select = $('#'+id); + select.children('option').attr('selected', true); +} + + +function submit_query_form(id) { + selectAllOptions("selected_columns"); + $('#'+id).submit(); +} + +//replaces redmine default method showTab() beacuse of compatibility Redmine 3.1+ +function showContactTab(name, url) { + $('div#content .tab-content').hide(); + $('div.tabs a').removeClass('selected'); + $('#tab-content-' + name).show(); + $('#tab-' + name).addClass('selected'); + if ("replaceState" in window.history) { + window.history.replaceState(null, document.title, url); + } + return false; +} + +function tooglePriceField() { + $('#deal_price').prop( "disabled", $('.line:visible').size() > 0 ); +} + +function toggleCRMIssuesSelection(el) { + var boxes = $(el).parents('form').find('input[type=checkbox]'); + var all_checked = true; + boxes.each(function(){ if (!$(this).prop('checked')) { all_checked = false; } }); + boxes.each(function(){ + if (all_checked) { + $(this).removeAttr('checked'); + $(this).parents('tr').removeClass('context-menu-selection'); + } else if (!$(this).prop('checked')) { + $(this).prop('checked', true); + $(this).parents('tr').addClass('context-menu-selection'); + } + }); +} + +function toogleDealItems(el) { + $(el).closest('p').hide(); + $('.deal_items').show(); +} + +function uploadAvatar(element) { + var fileSpan = $('', { id: 'attachments_0'}); + $('#contact_data #attachments_fields').html(''); + fileSpan.append( + $('', { type: 'hidden', class: 'filename', name: 'attachments[0][filename]'} ).val(element.files[0].name), + $('', { type: 'hidden', class: 'description', name: 'attachments[0][description]' } ).val('avatar'), + $('', { type: 'hidden', class: 'token', name: 'attachments[0][token]' } ) + ).appendTo('#contact_data #attachments_fields'); + ajaxUpload(element.files[0], 0, fileSpan, element); + return 0; +} + + +function initDealSelect2(id, url, placeholder) { + $(function () { + $('select#' + id).select2({ + ajax: { + url: url, + dataType: 'json', + delay: 250, + data: function (params) { + return { q: params.term }; + }, + processResults: function (data, params) { + return { results: data }; + }, + cache: true + }, + placeholder: placeholder, + allowClear: true, + minimumInputLength: 0, + width: '60%', + templateResult: function (option) { + return $('' + option.avatar + ' ' + option.text + ''); + } + }); + }); +}; diff --git a/plugins/redmine_contacts/assets/javascripts/contacts_autocomplete.js b/plugins/redmine_contacts/assets/javascripts/contacts_autocomplete.js new file mode 100644 index 0000000..7719050 --- /dev/null +++ b/plugins/redmine_contacts/assets/javascripts/contacts_autocomplete.js @@ -0,0 +1,46 @@ +function initContactsAutocomplete(fieldName, sourceUrl, selectUrl) { + var fieldId = '#' + fieldName.split('[').join('_').split('__').join('_').split(']').join(''); + var spanId = fieldId + '_selected_contact'; + var linkId = fieldId + '_edit_link'; + + function selectContact( contact ) { + if (!selectUrl || !selectUrl.length) { + $(spanId).text( contact.name ); + $(spanId).show(); + $(spanId).scrollTop( 0 ); + $(fieldId).hide(); + $(fieldId).val( contact.id ); + $(linkId).show(); + $(fieldId + '_add_link').hide(); + } else { + $.ajax({ + url: selectUrl, + type: 'POST', + data: {id: contact.id} + }); + } + }; + + $(fieldId).autocomplete({ + source: sourceUrl, + search: function(){$(this).addClass('ajax-loading');}, + response: function(){$(this).removeClass('ajax-loading');}, + change: function(event,ui){ + $(this).val((ui.item ? ui.item.id : '')); + }, + select: function( event, ui ) { + selectContact( ui.item ? + ui.item: + 'Nothing selected, input was ' + this.value); + return false; + }, + minLength: 0 + }) + // .focus(function(){$(this).autocomplete("search");}) + .data('ui-autocomplete')._renderItem = function( ul, item ) { + return $('

    3. ') + .append('' + item.avatar + ' ' + item.name + (item.company.length != 0 ? ' (' + item.company + ') ' : '') + '') + .appendTo( ul ); + }; + +} diff --git a/plugins/redmine_contacts/assets/javascripts/contacts_select2.js b/plugins/redmine_contacts/assets/javascripts/contacts_select2.js new file mode 100644 index 0000000..bb29193 --- /dev/null +++ b/plugins/redmine_contacts/assets/javascripts/contacts_select2.js @@ -0,0 +1,41 @@ +var oldToggleFilter = window.toggleFilter; + +window.toggleFilter = function(field) { + oldToggleFilter(field); + return transform_to_select2(field); +} + +function filterFormatState (opt) { + var $opt = $('' + opt.avatar + ' ' + opt.text + ''); + return $opt; +}; + +function transform_to_select2(field){ + field_format = availableFilters[field]['field_format']; + field = field.replace('.', '_'); + initialized_select2 = $('#tr_' + field + ' .values .select2'); + if (initialized_select2.size() == 0 && $.inArray(field_format, field_formats) >= 0) { + $('#tr_' + field + ' .toggle-multiselect').hide(); + $('#tr_' + field + ' .values .value').attr('multiple', 'multiple'); + $('#tr_' + field + ' .values .value').select2({ + ajax: { + url: contact_filter_urls[field_format], + dataType: 'json', + delay: 250, + data: function (params) { + return { q: params.term }; + }, + processResults: function (data, params) { + return { results: data }; + }, + cache: true + }, + placeholder: ' ', + minimumInputLength: 1, + width: '60%', + templateResult: filterFormatState + }).on('select2:open', function (e) { + $(this).parent('span').find('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val(''); + }); + } +} diff --git a/plugins/redmine_contacts/assets/javascripts/jquery.colorPicker.min.js b/plugins/redmine_contacts/assets/javascripts/jquery.colorPicker.min.js new file mode 100755 index 0000000..5065c16 --- /dev/null +++ b/plugins/redmine_contacts/assets/javascripts/jquery.colorPicker.min.js @@ -0,0 +1,26 @@ +/** + * Really Simple Color Picker in jQuery + * + * Licensed under the MIT (MIT-LICENSE.txt) licenses. + * + * Copyright (c) 2008-2012 + * Lakshan Perera (www.laktek.com) & Daniel Lacy (daniellacy.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */(function(a){var b,c,d=0,e={control:a('
       
      '),palette:a('
      '),swatch:a('
       
      '),hexLabel:a(''),hexField:a('')},f="transparent",g;a.fn.colorPicker=function(b){return this.each(function(){var c=a(this),g=a.extend({},a.fn.colorPicker.defaults,b),h=a.fn.colorPicker.toHex(c.val().length>0?c.val():g.pickerDefault),i=e.control.clone(),j=e.palette.clone().attr("id","colorPicker_palette-"+d),k=e.hexLabel.clone(),l=e.hexField.clone(),m=j[0].id,n;a.each(g.colors,function(b){n=e.swatch.clone(),g.colors[b]===f?(n.addClass(f).text("X"),a.fn.colorPicker.bindPalette(l,n,f)):(n.css("background-color","#"+this),a.fn.colorPicker.bindPalette(l,n)),n.appendTo(j)}),k.attr("for","colorPicker_hex-"+d),l.attr({id:"colorPicker_hex-"+d,value:h}),l.bind("keydown",function(b){if(b.keyCode===13){var d=a.fn.colorPicker.toHex(a(this).val());a.fn.colorPicker.changeColor(d?d:c.val())}b.keyCode===27&&a.fn.colorPicker.hidePalette()}),l.bind("keyup",function(b){var d=a.fn.colorPicker.toHex(a(b.target).val());a.fn.colorPicker.previewColor(d?d:c.val())}),a('
      ').append(k).appendTo(j),j.find(".colorPicker_hexWrap").append(l),a("body").append(j),j.hide(),i.css("background-color",h),i.bind("click",function(){a.fn.colorPicker.togglePalette(a("#"+m),a(this))}),b&&b.onColorChange?i.data("onColorChange",b.onColorChange):i.data("onColorChange",function(){}),c.after(i),c.bind("change",function(){c.next(".colorPicker-picker").css("background-color",a.fn.colorPicker.toHex(a(this).val()))}),c.val(h).hide(),d++})},a.extend(!0,a.fn.colorPicker,{toHex:function(a){if(a.match(/[0-9A-F]{6}|[0-9A-F]{3}$/i))return a.charAt(0)==="#"?a:"#"+a;if(!a.match(/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/))return!1;var b=[parseInt(RegExp.$1,10),parseInt(RegExp.$2,10),parseInt(RegExp.$3,10)],c=function(a){if(a.length<2)for(var b=0,c=2-a.length;b0)return;a.fn.colorPicker.hidePalette()},hidePalette:function(){a(document).unbind("mousedown",a.fn.colorPicker.checkMouse),a(".colorPicker-palette").hide()},showPalette:function(c){var d=b.prev("input").val();c.css({top:b.offset().top+b.outerHeight(),left:b.offset().left}),a("#color_value").val(d),c.show(),a(document).bind("mousedown",a.fn.colorPicker.checkMouse)},togglePalette:function(d,e){e&&(b=e),c=d,c.is(":visible")?a.fn.colorPicker.hidePalette():a.fn.colorPicker.showPalette(d)},changeColor:function(c){b.css("background-color",c),b.prev("input").val(c).change(),a.fn.colorPicker.hidePalette(),b.data("onColorChange").call(b,a(b).prev("input").attr("id"),c)},previewColor:function(a){b.css("background-color",a)},bindPalette:function(c,d,e){e=e?e:a.fn.colorPicker.toHex(d.css("background-color")),d.bind({click:function(b){g=e,a.fn.colorPicker.changeColor(e)},mouseover:function(b){g=c.val(),a(this).css("border-color","#598FEF"),c.val(e),a.fn.colorPicker.previewColor(e)},mouseout:function(d){a(this).css("border-color","#000"),c.val(b.css("background-color")),c.val(g),a.fn.colorPicker.previewColor(g)}})}}),a.fn.colorPicker.defaults={pickerDefault:"FFFFFF",colors:["000000","993300","333300","000080","333399","333333","800000","FF6600","808000","008000","008080","0000FF","666699","808080","FF0000","FF9900","99CC00","339966","33CCCC","3366FF","800080","999999","FF00FF","FFCC00","FFFF00","00FF00","00FFFF","00CCFF","993366","C0C0C0","FF99CC","FFCC99","FFFF99","CCFFFF","99CCFF","FFFFFF"],addColors:[]}})(jQuery) \ No newline at end of file diff --git a/plugins/redmine_contacts/assets/javascripts/tag-it.js b/plugins/redmine_contacts/assets/javascripts/tag-it.js new file mode 100755 index 0000000..e1c74c1 --- /dev/null +++ b/plugins/redmine_contacts/assets/javascripts/tag-it.js @@ -0,0 +1,588 @@ +/* +* jQuery UI Tag-it! +* +* @version v2.0 (06/2011) +* +* Copyright 2011, Levy Carneiro Jr. +* Released under the MIT license. +* http://aehlke.github.com/tag-it/LICENSE +* +* Homepage: +* http://aehlke.github.com/tag-it/ +* +* Authors: +* Levy Carneiro Jr. +* Martin Rehfeld +* Tobias Schmidt +* Skylar Challand +* Alex Ehlke +* +* Maintainer: +* Alex Ehlke - Twitter: @aehlke +* +* Dependencies: +* jQuery v1.4+ +* jQuery UI v1.8+ +*/ +(function($) { + + $.widget('ui.tagit', { + options: { + allowDuplicates : false, + caseSensitive : true, + fieldName : 'tags', + placeholderText : null, // Sets `placeholder` attr on input field. + readOnly : false, // Disables editing. + removeConfirmation: false, // Require confirmation to remove tags. + tagLimit : null, // Max number of tags allowed (null for unlimited). + + // Used for autocomplete, unless you override `autocomplete.source`. + availableTags : [], + + // Use to override or add any options to the autocomplete widget. + // + // By default, autocomplete.source will map to availableTags, + // unless overridden. + autocomplete: {}, + + // Shows autocomplete before the user even types anything. + showAutocompleteOnFocus: false, + + // When enabled, quotes are unneccesary for inputting multi-word tags. + allowSpaces: false, + + // The below options are for using a single field instead of several + // for our form values. + // + // When enabled, will use a single hidden field for the form, + // rather than one per tag. It will delimit tags in the field + // with singleFieldDelimiter. + // + // The easiest way to use singleField is to just instantiate tag-it + // on an INPUT element, in which case singleField is automatically + // set to true, and singleFieldNode is set to that element. This + // way, you don't need to fiddle with these options. + singleField: false, + + // This is just used when preloading data from the field, and for + // populating the field with delimited tags as the user adds them. + singleFieldDelimiter: ',', + + // Set this to an input DOM node to use an existing form field. + // Any text in it will be erased on init. But it will be + // populated with the text of tags as they are created, + // delimited by singleFieldDelimiter. + // + // If this is not set, we create an input node for it, + // with the name given in settings.fieldName. + singleFieldNode: null, + + // Whether to animate tag removals or not. + animate: true, + + // Optionally set a tabindex attribute on the input that gets + // created for tag-it. + tabIndex: null, + + // Event callbacks. + beforeTagAdded : null, + afterTagAdded : null, + + beforeTagRemoved : null, + afterTagRemoved : null, + + onTagClicked : null, + onTagLimitExceeded : null, + + + // DEPRECATED: + // + // /!\ These event callbacks are deprecated and WILL BE REMOVED at some + // point in the future. They're here for backwards-compatibility. + // Use the above before/after event callbacks instead. + onTagAdded : null, + onTagRemoved: null, + // `autocomplete.source` is the replacement for tagSource. + tagSource: null + // Do not use the above deprecated options. + }, + + _create: function() { + // for handling static scoping inside callbacks + var that = this; + + // There are 2 kinds of DOM nodes this widget can be instantiated on: + // 1. UL, OL, or some element containing either of these. + // 2. INPUT, in which case 'singleField' is overridden to true, + // a UL is created and the INPUT is hidden. + if (this.element.is('input')) { + this.tagList = $('
        ').insertAfter(this.element); + this.options.singleField = true; + this.options.singleFieldNode = this.element; + this.element.addClass('tagit-hidden-field'); + } else { + this.tagList = this.element.find('ul, ol').andSelf().last(); + } + + this.tagInput = $('').addClass('ui-widget-content'); + + if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled'); + + if (this.options.tabIndex) { + this.tagInput.attr('tabindex', this.options.tabIndex); + } + + if (this.options.placeholderText) { + this.tagInput.attr('placeholder', this.options.placeholderText); + } + + if (!this.options.autocomplete.source) { + this.options.autocomplete.source = function(search, showChoices) { + var filter = search.term.toLowerCase(); + var choices = $.grep(this.options.availableTags, function(element) { + // Only match autocomplete options that begin with the search term. + // (Case insensitive.) + return (element.toLowerCase().indexOf(filter) === 0); + }); + if (!this.options.allowDuplicates) { + choices = this._subtractArray(choices, this.assignedTags()); + } + showChoices(choices); + }; + } + + if (this.options.showAutocompleteOnFocus) { + this.tagInput.focus(function(event, ui) { + that._showAutocomplete(); + }); + + if (typeof this.options.autocomplete.minLength === 'undefined') { + this.options.autocomplete.minLength = 0; + } + } + + // Bind autocomplete.source callback functions to this context. + if ($.isFunction(this.options.autocomplete.source)) { + this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this); + } + + // DEPRECATED. + if ($.isFunction(this.options.tagSource)) { + this.options.tagSource = $.proxy(this.options.tagSource, this); + } + + this.tagList + .addClass('tagit') + .addClass('ui-widget ui-widget-content ui-corner-all') + // Create the input field. + .append($('
      • ').append(this.tagInput)) + .click(function(e) { + var target = $(e.target); + if (target.hasClass('tagit-label')) { + var tag = target.closest('.tagit-choice'); + if (!tag.hasClass('removed')) { + that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)}); + } + } else { + // Sets the focus() to the input field, if the user + // clicks anywhere inside the UL. This is needed + // because the input field needs to be of a small size. + that.tagInput.focus(); + } + }); + + // Single field support. + var addedExistingFromSingleFieldNode = false; + if (this.options.singleField) { + if (this.options.singleFieldNode) { + // Add existing tags from the input field. + var node = $(this.options.singleFieldNode); + var tags = node.val().split(this.options.singleFieldDelimiter); + node.val(''); + $.each(tags, function(index, tag) { + that.createTag(tag, null, true); + addedExistingFromSingleFieldNode = true; + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = $(''); + this.tagList.after(this.options.singleFieldNode); + } + } + + // Add existing tags from the list, if any. + if (!addedExistingFromSingleFieldNode) { + this.tagList.children('li').each(function() { + if (!$(this).hasClass('tagit-new')) { + that.createTag($(this).text(), $(this).attr('class'), true); + $(this).remove(); + } + }); + } + + // Events. + this.tagInput + .keydown(function(event) { + // Backspace is not detected within a keypress, so it must use keydown. + if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') { + var tag = that._lastTag(); + if (!that.options.removeConfirmation || tag.hasClass('remove')) { + // When backspace is pressed, the last tag is deleted. + that.removeTag(tag); + } else if (that.options.removeConfirmation) { + tag.addClass('remove ui-state-highlight'); + } + } else if (that.options.removeConfirmation) { + that._lastTag().removeClass('remove ui-state-highlight'); + } + + // Comma/Space/Enter are all valid delimiters for new tags, + // except when there is an open quote or if setting allowSpaces = true. + // Tab will also create a tag, unless the tag input is empty, + // in which case it isn't caught. + if ( + (event.which === $.ui.keyCode.ENTER) + || + (event.which == $.ui.keyCode.TAB && that.tagInput.val() !== '') + || + ( + event.which == $.ui.keyCode.SPACE && + that.options.allowSpaces !== true && + ( + $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || + ( + $.trim(that.tagInput.val()).charAt(0) == '"' && + $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' && + $.trim(that.tagInput.val()).length - 1 !== 0 + ) + ) + ) + ) { + // Enter submits the form if there's no text in the input. + if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { + event.preventDefault(); + } + + // Autocomplete will create its own tag from a selection and close automatically. + if (!(that.options.autocomplete.autoFocus && that.tagInput.data('autocomplete-open'))) { + that.tagInput.autocomplete('close'); + that.createTag(that._cleanedInput()); + } + } + }).blur(function(e){ + // Create a tag when the element loses focus. + // If autocomplete is enabled and suggestion was clicked, don't add it. + if (!that.tagInput.data('autocomplete-open')) { + that.createTag(that._cleanedInput()); + } + }); + + // Autocomplete. + if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { + var autocompleteOptions = { + select: function(event, ui) { + that.createTag(ui.item.value); + // Preventing the tag input to be updated with the chosen value. + return false; + } + }; + $.extend(autocompleteOptions, this.options.autocomplete); + + // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, + // while tagSource is left null by default. + autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; + + this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen.tagit', function(event, ui) { + that.tagInput.data('autocomplete-open', true); + }).bind('autocompleteclose.tagit', function(event, ui) { + that.tagInput.data('autocomplete-open', false); + }); + + this.tagInput.autocomplete('widget').addClass('tagit-autocomplete'); + } + }, + + destroy: function() { + $.Widget.prototype.destroy.call(this); + + this.element.unbind('.tagit'); + this.tagList.unbind('.tagit'); + + this.tagInput.removeData('autocomplete-open'); + + this.tagList.removeClass([ + 'tagit', + 'ui-widget', + 'ui-widget-content', + 'ui-corner-all', + 'tagit-hidden-field' + ].join(' ')); + + if (this.element.is('input')) { + this.element.removeClass('tagit-hidden-field'); + this.tagList.remove(); + } else { + this.element.children('li').each(function() { + if ($(this).hasClass('tagit-new')) { + $(this).remove(); + } else { + $(this).removeClass([ + 'tagit-choice', + 'ui-widget-content', + 'ui-state-default', + 'ui-state-highlight', + 'ui-corner-all', + 'remove', + 'tagit-choice-editable', + 'tagit-choice-read-only' + ].join(' ')); + + $(this).text($(this).children('.tagit-label').text()); + } + }); + + if (this.singleFieldNode) { + this.singleFieldNode.remove(); + } + } + + return this; + }, + + _cleanedInput: function() { + // Returns the contents of the tag input, cleaned and ready to be passed to createTag + return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); + }, + + _lastTag: function() { + return this.tagList.find('.tagit-choice:last:not(.removed)'); + }, + + _tags: function() { + return this.tagList.find('.tagit-choice:not(.removed)'); + }, + + assignedTags: function() { + // Returns an array of tag string values + var that = this; + var tags = []; + if (this.options.singleField) { + tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); + if (tags[0] === '') { + tags = []; + } + } else { + this._tags().each(function() { + tags.push(that.tagLabel(this)); + }); + } + return tags; + }, + + _updateSingleTagsField: function(tags) { + // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter + $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); + }, + + _subtractArray: function(a1, a2) { + var result = []; + for (var i = 0; i < a1.length; i++) { + if ($.inArray(a1[i], a2) == -1) { + result.push(a1[i]); + } + } + return result; + }, + + tagLabel: function(tag) { + // Returns the tag's string label. + if (this.options.singleField) { + return $(tag).find('.tagit-label:first').text(); + } else { + return $(tag).find('input:first').val(); + } + }, + + _showAutocomplete: function() { + this.tagInput.autocomplete('search', ''); + }, + + _findTagByLabel: function(name) { + var that = this; + var tag = null; + this._tags().each(function(i) { + if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { + tag = $(this); + return false; + } + }); + return tag; + }, + + _isNew: function(name) { + return !this._findTagByLabel(name); + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + _effectExists: function(name) { + return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); + }, + + createTag: function(value, additionalClass, duringInitialization) { + var that = this; + + value = $.trim(value); + + if(this.options.preprocessTag) { + value = this.options.preprocessTag(value); + } + + if (value === '') { + return false; + } + + if (!this.options.allowDuplicates && !this._isNew(value)) { + var existingTag = this._findTagByLabel(value); + if (this._trigger('onTagExists', null, { + existingTag: existingTag, + duringInitialization: duringInitialization + }) !== false) { + if (this._effectExists('highlight')) { + existingTag.effect('highlight'); + } + } + return false; + } + + if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { + this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); + return false; + } + + var label = $(this.options.onTagClicked ? '' : '').text(value); + + // Create tag. + var tag = $('
      • ') + .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') + .addClass(additionalClass) + .append(label); + + if (this.options.readOnly){ + tag.addClass('tagit-choice-read-only'); + } else { + tag.addClass('tagit-choice-editable'); + // Button for removing the tag. + var removeTagIcon = $('') + .addClass('ui-icon ui-icon-close'); + var removeTag = $('\xd7') // \xd7 is an X + .addClass('tagit-close') + .append(removeTagIcon) + .click(function(e) { + // Removes a tag when the little 'x' is clicked. + that.removeTag(tag); + }); + tag.append(removeTag); + } + + // Unless options.singleField is set, each tag has a hidden input field inline. + if (!this.options.singleField) { + var escapedValue = label.html(); + tag.append(''); + } + + if (this._trigger('beforeTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + tags.push(value); + this._updateSingleTagsField(tags); + } + + // DEPRECATED. + this._trigger('onTagAdded', null, tag); + + this.tagInput.val(''); + + // Insert tag. + this.tagInput.parent().before(tag); + + this._trigger('afterTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }); + + if (this.options.showAutocompleteOnFocus && !duringInitialization) { + setTimeout(function () { that._showAutocomplete(); }, 0); + } + }, + + removeTag: function(tag, animate) { + animate = typeof animate === 'undefined' ? this.options.animate : animate; + + tag = $(tag); + + // DEPRECATED. + this._trigger('onTagRemoved', null, tag); + + if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + var removedTagLabel = this.tagLabel(tag); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + + if (animate) { + tag.addClass('removed'); // Excludes this tag from _tags. + var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; + + var thisTag = this; + hide_args.push(function() { + tag.remove(); + thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)}); + }); + + tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); + } else { + tag.remove(); + this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); + } + + }, + + removeTagByLabel: function(tagLabel, animate) { + var toRemove = this._findTagByLabel(tagLabel); + if (!toRemove) { + throw "No such tag exists with the name '" + tagLabel + "'"; + } + this.removeTag(toRemove, animate); + }, + + removeAll: function() { + // Removes all tags. + var that = this; + this._tags().each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); +})(jQuery); diff --git a/plugins/redmine_contacts/assets/stylesheets/colorPicker.css b/plugins/redmine_contacts/assets/stylesheets/colorPicker.css new file mode 100755 index 0000000..e4e2444 --- /dev/null +++ b/plugins/redmine_contacts/assets/stylesheets/colorPicker.css @@ -0,0 +1,30 @@ +div.colorPicker-picker { + height: 16px; + width: 16px; + padding: 0 !important; + border: 1px solid #ccc; + cursor: pointer; + line-height: 16px; +} + +div.colorPicker-palette { + width: 200px; + position: absolute; + background-color: #fff; + border: 1px solid #ccc; + padding: 5px; + z-index: 9999; +} + div.colorPicker_hexWrap {width: 100%; float:left; padding: 5px;} + div.colorPicker_hexWrap label {font-size: 95%; color: #2F2F2F; margin: 5px 2px; width: 25%} + div.colorPicker_hexWrap input {font-size: 95%; width: 65%; } + +div.colorPicker-swatch { + height: 16px; + width: 16px; + border: 1px solid #000; + margin: 2px; + float: left; + cursor: pointer; + line-height: 12px; +} \ No newline at end of file diff --git a/plugins/redmine_contacts/assets/stylesheets/contacts.css b/plugins/redmine_contacts/assets/stylesheets/contacts.css new file mode 100644 index 0000000..5cb8bc4 --- /dev/null +++ b/plugins/redmine_contacts/assets/stylesheets/contacts.css @@ -0,0 +1,732 @@ +div.contact {background:#f4e9f2; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} +div.deal {background:#edfff2; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} +form#add_issue_form {background:#ffffff; display: block; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7; width: 92%;} + +/**********************************************************************/ +/* CONTACTS COMMON +/**********************************************************************/ +span.contact {display: inline-block;} + +/********************/ +/* CONTACT ISSUES */ +/********************/ + +#contact_issues img.gravatar { + vertical-align: middle; + margin-left: 5px; +} + +div.contact-issues { + margin-right: 4px; +} + +div.contact-issues td.done_checkbox { + width: 10px; + vertical-align: top; + padding-top: 4px; +} + +div.contact-issues td.issue_subject { + vertical-align: top; + width: 100%; +} + +table.list.issue td.contacts img.gravatar{ + vertical-align: middle; + margin: 0 4px 2px 0; +} + +div.contact-issues h3 a { + color: inherit; +} + +#contacts_for_issue {height: 200px; overflow:auto;} +#contacts_for_issue label {display: block;} +input#contact_search{width:90%;} + + +/********************/ +/* ADD NOTE FORM */ +/********************/ + +div.add-note {margin-left: 5px; margin-right: 6px; margin-bottom: 20px;} +div.add-note p {margin-bottom: 0px; margin-top: 5px;} +.note-custom-fields label {margin-right: 10px;} + +/*div#contact_list table.index tbody tr:hover { background-color:#ffffdd; } */ + +div#sidebar div.contextual { + margin-right: 8px; +} + + +/* note-data*/ + +span.note-info { + font-size: 11px; + margin: 0px; + padding: 0px; + line-height: 1.5; + color: rgb(119, 119, 119); +} + + +table.subject_header { + width: 100%; +} + + +div.contact.details table.subject_header h1 { + margin-bottom: 0px; +} + +table.subject_header td.avatar { + vertical-align: top; + vertical-align: top; + text-align: right; + width: 57px; +} + +table.subject_header td.subject_info { + padding-left: 15px; + padding-right: 15px; + border-left: 1px solid #D7D7D7; + min-width: 200px; + width: 200px; +} + +table.subject_header td.subject_info ul { + list-style: none; + padding-left: 0px; +} + +table.subject_header td.subject_info li { + line-height: 20px; + white-space: nowrap; +} + + +h4.contacts_header {border-bottom: 0px;} + + + +/********************/ +/* Search */ +/********************/ +h2.contacts_header {border-bottom: 0px;} + + +div.filters h2 {margin-bottom: 5px;} +div.filters h2 .scope_title a { color: #444; text-decoration: none; } +div.filters h2 .scope_title a:hover { text-decoration: none; display: inline; color: #aaa;} + +div.filters .tags span.tag-label-color a { line-height: 0; } + +div.search_and_filters { margin-bottom: 10px; } + +div.live_search { font-size: 16px; } + +div.filters .live_search {position:relative;} + +.live_search input.live_search_field { font-size: 16px; } + +input#search:focus::-webkit-input-placeholder { color: lightgray;} +input#search:focus:-moz-placeholder { color: lightgray;} + + +/**************************************************************/ +/* TAGS */ +/**************************************************************/ + +.tag_list { + margin-top: 5px; + display: inline-block; +} + +span.tag-label-color { + background-color: #759FCF; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 3px; + padding: 2px 4px; + display: inline-block; + font-size: 10px; + margin: 0px 0px 5px 2px; +} + +.tag-label .tag-count, +.tag-label-color .tag-count { + font-size: .75em; + margin-left: .5em; +} + +span.tag-label-color:hover { + background-color: #9DB9D5; +} + +span.tag-label-color a, span.tag-label-color > span { + text-decoration: none; + font-family: Verdana, sans-serif; + font-weight: normal; + color: white; +} + +#edit_tags_form.box { + margin: 1px 5px 0px 0px; +} + +#edit_tags_form.box label { + margin-right: 5px; + font-weight: bold; +} + +#edit_tags_form.box #contact_tags { + margin-bottom: 10px; +} + +div#tags_data span.contextual {float: none; padding-left: 0px;} +#tags_data:hover #edit_tags_link {opacity: 1;} +#edit_tags_link {opacity: 0;} + + +span.tag-label, span.tag-input { + display: inline-block; +} + + +#responsible_user ul {margin: 0; padding: 0;} +#responsible_user li {list-style-type:none; margin: 3px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#responsible_user select {width: 95%; display: block;} +#responsible_user a.delete {opacity: 0.4;} +#responsible_user a.delete:hover {opacity: 1;} +#responsible_user img.gravatar { + vertical-align: middle;margin: 0 4px 2px 0; +} + +#edit_tags_form .contacts-tags-edit ul.tagit { + padding: 5px; + margin: 2px; +} + +/* Edit tags Tag-it*/ + +.contacts-tags-edit ul.tagit { + border: 1px solid #d7d7d7; + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + -khtml-border-radius: 0px; + border-radius: 0px; + background: white; + padding: 0px; + margin: 0px; + line-height: 1.5em; +} + +.contacts-tags-edit ul.tagit li.tagit-choice { + font-weight: normal; + -moz-border-radius: 0px; + border-radius: 0px; + -webkit-border-radius: 0px; + font-size: 11px; + color: inherit; + padding-top: 0px; + padding-bottom: 0px; + background-color: #f7f7f7; + margin: 1px; +} + +.contacts-tags-edit ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove { + background-color: #e5e5e5; + text-decoration: none; + color: black; +} + +.contacts-tags-edit ul.tagit li.tagit-choice a.tagit-close { + text-decoration: none; +} + +.contacts-tags-edit ul.tagit li.tagit-choice .tagit-close { + right: .4em; +} + +.contacts-tags-edit ul.tagit li.tagit-choice .ui-icon { + display: none; +} + +.contacts-tags-edit ul.tagit li.tagit-choice .tagit-close .text-icon{ + display: inline; +} + +.contacts-tags-edit ul.tagit li.tagit-choice .tagit-close .text-icon:hover { + color: black; +} + +.contacts-tags-edit ul.tagit li.tagit-new input { + font-size: 11px; + background: white; + margin-bottom: 2px; + margin-left: 2px; +} + +.contacts-tags-edit ul.tagit li.tagit-new { + padding: 0px; +} + + +/**************************************************************/ +/* CONTACTS_TABLE_LIST */ +/**************************************************************/ + +table.list.contacts td.id { + padding-right: 6px; + padding-left: 6px; +} + +table.index tbody tr.group td { + padding: 0.8em 0 0.5em 0.3em; + font-weight: bold; + border-bottom: 1px solid #CCC; +} + +table.list.contacts tr td a { + color: #666; +} + +table.list.contacts td.name, +table.list td.emails, +table.list td.phones { + white-space: nowrap; +} + +table.list td.address, +table.list td.tags, +table.list td.street1, +table.list td.street2, +table.list td.background { + text-align: left; +} + +table.list tr td.tags {white-space: normal;} + +/**************************************************************/ +/* TABLE LIST */ +/**************************************************************/ +table.list td.contacts, +table.list td.deal {text-align: left; white-space: normal;} +table.list td.contact {text-align: left} + +/**************************************************************/ +/* CONTACTS_DEALS_LIST */ +/**************************************************************/ + + +table.contacts.index { + border-top: 1px solid rgb(239, 239, 239); + border-right: 1px solid rgb(239, 239, 239); + border-left: 1px solid rgb(239, 239, 239); + width: 100%; + border-spacing: 0px 0px; + margin-bottom: 4px; +} + +table.contacts.index tr.selected{ background-color: #507AAA !important; color: #F8F8F8 !important;} +/*table.contacts.index tr.selected a { color: #F8F8F8 !important; } */ +table.contacts.index tr.context-menu-selection h2 { color: #F8F8F8 !important; } +table.contacts.index tr.context-menu-selection td { color: #F8F8F8 !important; } + +table.contacts.index tr.context-menu-selection span.tag-label-color { border: 1px solid #EFEFEF; padding: 2px 3px; } + +table.contacts.index tbody tr:hover { background-color:#ffffdd; } + +table.contacts.index td { + vertical-align: top; + color: #666; + padding: 8px 0; + border-bottom: 1px solid #efefef; +} + +table.contacts.index td.checkbox { padding: 12px 0px 0px 5px; } +table.contacts.index td.name { width: 50%; padding-right: 10px;} +table.contacts.index td.info { width: 50%; padding-right: 5px; } +table.contacts.index td.avatar {padding: 10px 10px 10px 5px;} + +table.contacts.index th {padding: 5px 10px 5px 0px; border-bottom: 1px solid #efefef; vertical-align: top;} +table.contacts.index th.title {text-align: right;} +table.contacts.index th.sum {text-align: left;} + +table.contacts.deals.index td.avatar {padding: 10px 10px 10px 10px;} + + +/*table.contacts.index td.avatar {margin-left: 50px;}*/ + +table.contacts.index td.name h1 { font-size: 20px; font-weight: normal; + margin: 0; + padding: 0; +} + +table.contacts.index td.name h1.deal_name { + font-size: 16px; + font-weight: normal; + margin: 0; + padding: 0; +} + +table.contacts.index td.name h1.selected { background-color: #ffb;} +table.contacts.index td.name h2.selected { background-color: #ffb;} + +table.contacts.index td.name h2 { + font-size: 13px; + color: #777; + font-weight: normal; + line-height: 140%; + padding: 0; + margin: 0; + background: none; + border: none; +} + +div.Right td.name div.info { + padding-top: 3px; + margin-left: 42px; +} +/*#edit_tags_form {display: block; margin-top: 10px;}*/ +.notes div.contextual {vertical-align: top;} + +/*Deals*/ + +div#deal-status { + margin-top: 4px; +} + +div.deal-sum { + margin-bottom: 4px; +} + +table.related_deals td.name h4 { letter-spacing: -1px; margin: 0px 0 4px 0; padding: 0; line-height: 1.1em;} + +div#deal-status span.contextual {float: none; padding-left: 0px;} + + +/**********************************************************************/ +/* DEAL LIST +/**********************************************************************/ + +.deal_list table.list tr.deal a {color: #666;} +.deal_list table.list td.name {text-align: left; white-space: normal} +.deal_list table.list th.sum.deals-sum { text-align: right; white-space: nowrap; width: 10%;} + +/* lost */ +.deal_list tr.odd.status-type-2, .deal_list table.list tbody tr.odd.status-type-2:hover { color: #900; } +.deal_list tr.odd.status-type-2 { background: #FEE; } +.deal_list tr.even.status-type-2, .deal_list table.list tbody tr.even.status-type-2:hover { color: #900; } +.deal_list tr.even.status-type-2 { background: #FFF2F2; } +.deal_list tr.status-type-2 a { color: #900; } +.deal_list tr.odd.status-type-2 td, .deal_list tr.even.status-type-2 td { border-color: #FCC;} + +/* won */ +.deal_list tr.odd.status-type-1, .deal_list table.list tbody tr.odd.status-type-1:hover { color: #005F00; } +.deal_list tr.odd.status-type-1 { background: #EBFEEB; } +.deal_list tr.even.status-type-1, .deal_list table.list tbody tr.even.status-type-1:hover { color: #005F00; } +.deal_list tr.even.status-type-1 { background: #DFFFDF; } +.deal_list tr.status-type-1 a { color: #005F00; } +.deal_list tr.odd.status-type-1 td, .deal_list tr.even.status-type-1 td { border-color: #9FCF9F; } + +table.deals.index { margin-bottom: 0px;} +table.deals.index.total { border-top: 0px;} + +/**********************************************************************/ +/* DEALS BOARD +/**********************************************************************/ +table.list.deal-board tbody tr, +table.list.deal-board tbody tr:hover {background-color: white;} +.deal-board .deal-card, +.deal-board .sortable-placeholder { + padding: 5px; + border: solid 1px #d5d5d5; + background-color: white; + margin: 5px; +} +.deal-board .deal-card {background-color: #ffffdd;} +.deal-board .deal-status-col {text-align: left; white-space: normal;vertical-align: top} +.deal-board .avatar {float: left; margin-right: 5px;} +.deal-board p.name { + margin-bottom: 5px; + font-weight: bold; +} +.deal-board .info { + border-top: 1px solid #d5d5d5; + padding-top: 5px; +} +.deal-board td.deal-status-col.won .deal-card {background: #DFFFDF; border-color: #9FCF9F;} +.deal-board td.deal-status-col.lost .deal-card {background: #FEE; border-color: #FCC;} + +.deal-board .sortable-placeholder { + background-color: #E9F8FD; + border: solid 1px #E9F8FD; + height: 65px; +} + +.deal-board .deal-card.ui-sortable-helper, .deal-board .deal-card.draggable-active { + -moz-transform: rotate(5deg); /* Ð”Ð»Ñ Firefox */ + -ms-transform: rotate(5deg); /* Ð”Ð»Ñ IE */ + -webkit-transform: rotate(5deg); /* Ð”Ð»Ñ Safari, Chrome, iOS */ + -o-transform: rotate(5deg); /* Ð”Ð»Ñ Opera */ + transform: rotate(5deg); + box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.05); + -webkit-box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.05); +} + +.deal-board .deal-card.draggable-active { + width: 250px; +} + +/**************************************************************/ +/* ICONS +/**************************************************************/ + +#admin-menu a.contacts { background-image: url(../images/vcard.png);} + +.icon-contact { background-image: url(../images/vcard.png); } +.icon-import { background-image: url(../images/bullet_go.png); } +.icon-merge { background-image: url(../images/arrow_merge.png); } + +.icon-add-deal { background-image: url(../images/money.png); } +.icon-company-contact { background-image: url(../images/user_suit.png); } +.icon-link-break { background-image: url(../../../images/link_break.png); } +.icon-call { background-image: url(../images/phone.png); } +.icon-meeting { background-image: url(../images/calendar_view_day.png); } +.icon-vcard { background-image: url(../images/vcard.png); } +.icon-phone { background-image: url(../images/telephone.png); } +.icon-email { background-image: url(../images/email.png); } + +.icon-money-dollar { background-image: url(../images/money_dollar.png); } +.icon-rosette { background-image: url(../images/rosette.png); } +.icon-date { background-image: url(../images/date.png); } +.icon-clock-red { background-image: url(../images/clock_red.png); } +.icon-money-euro { background-image: url(../images/money_euro.png); } +.icon-money-pound { background-image: url(../images/money_pound.png); } +.icon-money-yen { background-image: url(../images/money_yen.png); } + +/**************************************************************/ +/* CONTACT_CARD */ +/**************************************************************/ + +div#sidebar div.contact.card { + margin-right: 10px; + overflow-x: hidden; +} + +div.contact.card table.subject_header td.name { + padding-left: 4px; + padding-top: 0px; +} + +div.contact.card table.subject_header td.name h2 { + border-bottom: none; + padding-top: 0px; + margin: 0px; +} + +div.contact.card table.subject_header h2 a { + font-size: 20px; + font-weight: normal; +} + +.small-card div.contact.card table.subject_header td.name h2 a { + font-size: 16px; + font-weight: inherit; + color: inherit; +} + +.small-card div.contact.card table.subject_header td.avatar img { + width: 50px; + height: 50px; +} + +/**************************************************************/ +/* THUMBNAILS */ +/**************************************************************/ + +div.wiki img.tumbnail { + border: 1px solid #D7D7D7; + height: 150px; + padding: 4px; + vertical-align: middle; + width: 150px; + background: white; +} + +/**************************************************************/ +/* EMPLOYEE */ +/**************************************************************/ +#company_contacts table.note_data { + width: 250px; + float: left; +} + +#company_contacts table.note_data td.name { + width: auto; +} + +/**************************************************************/ +/* NOTE_DATA */ +/**************************************************************/ + +table.note_data, table.related_deals { + width: 100%; +} + +table.note_data td.name div.wiki { + margin: 5px 0px 0px 0px; +} + +table.note_data .wiki.note p { + margin: inherit; +} + +table.note_data .wiki.note div.attachments p { + margin: 4px 0 2px 0; + display: inline-block; + white-space: nowrap; +} + +table.note_data td.avatar { + padding-right: 4px; + padding-left: 0px; +} + +table.note_data td.name { + padding-left: 0px; + vertical-align: top; + width: 100%; +} + +table.note_data .content.preview { + margin: 5px 0px 0px 0px; + font-size: 0.9em; + color: #808080; + font-style: italic; +} + +table.note_data a.delete:hover {opacity: 1;} +table.note_data a.delete {opacity: 0.4;} + +table.note_data td.name h4 { + letter-spacing: -1px; + margin: 7px 0 0 0; + padding: 0; + line-height: 1.1em; +} +table.note_data h4 { margin-top: 5px; margin-bottom: 0px;} +table.note_data td.avatar { vertical-align: top; width: 38px; padding-top: 10px;} + +table.note_data:hover h4>a.wiki-anchor { display: inline; color: #ddd;} + +.wiki.note a.wiki-anchor {display: none;} + +div.note_data_header table.note_data { margin-bottom: 5px;} + +h2.note_title { margin-bottom: 0px;} + +/* Thumbnails */ + +div.wiki p.thumbnails {margin-top: 12px;} + +img.thumbnail { + border: 1px solid #D7D7D7; + padding: 4px; + margin: 4px; + vertical-align: middle; +} + +/**************************************************************/ +/* CONTACTS_DUBLICATES */ +/**************************************************************/ + +#duplicates ul {margin: 0; padding: 0;} +#duplicates li {list-style-type:none; margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#duplicates img.gravatar {vertical-align: middle; margin: 0 4px 2px 0;} + +#duplicates ul.box {padding: 10px; background-color: #FFEBC1;} + + +/**************************************************************/ +/* CONTACTS_PROJECTS */ +/**************************************************************/ + +#contact_projects ul {margin: 0; padding: 0;} +#contact_projects li {list-style-type:none; margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#contact_projects select {width: 95%; display: block;} +#contact_projects a.delete {opacity: 0.4;} +#contact_projects a.delete:hover {opacity: 1;} +#contact_projects img.gravatar {vertical-align: middle; margin: 0 4px 2px 0;} + + +/**************************************************************/ +/* SALES_FUNNEL */ +/**************************************************************/ + +table.sales-funnel td.sales-funnel {text-align: center;} +table.sales-funnel td.sales-funnel span.tag-label-color { display: block; margin: auto;} +table.sales-funnel td.sales-funnel.index_0 { width: 50%;} +table.sales-funnel td.count { text-align: center; } +table.sales-funnel td.total { text-align: right; } +table.sales-funnel tr.deal_status_type-1 { background-color: #ECFFF5; } +table.sales-funnel tr.deal_status_type-2 { background-color: #FFECEC; } + +/**************************************************************/ +/* CONTACT_DATA */ +/**************************************************************/ +.avatar img.gravatar, +#avatar img.gravatar { + vertical-align: middle; +} + +div.contact.details.private .avatar img.gravatar { + border: solid 1px #EB7272; +} + +.tooltip span.tip.contact { + line-height: 11px !important; + font-size: 9px !important; + width: 200px !important; +} + +/**************************************************************/ +/* CONTACT ATTRIBUTES SIDEBAR */ +/**************************************************************/ +table.contact.attributes .gravatar { + vertical-align: middle; + margin: 0 0.5em 0 0; +} + +table.sidebar.attributes td, table.sidebar.attributes th { + padding: inherit; + text-align: left; +} + +.tab-placeholder { + display: none; +} + +.tab-placeholder.active { + display: block !important; +} + +ul.ui-autocomplete { + z-index: 10100; +} + +div.ui-dialog { + z-index: 999 !important; +} + +#issue_contacts ul {margin: 0; padding: 0;} +#issue_contacts li {list-style-type:none; margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} +#issue_contacts select {width: 95%; display: block;} +#issue_contacts a.delete {opacity: 0.4;} +#issue_contacts a.delete:hover {opacity: 1;} +#issue_contacts img.gravatar {vertical-align: middle; margin: 0 4px 2px 0;} + +.hidden { display: none; } +.select2-results ul.select2-results__options li.loading-results { display: none; } diff --git a/plugins/redmine_contacts/assets/stylesheets/contacts_sidebar.css b/plugins/redmine_contacts/assets/stylesheets/contacts_sidebar.css new file mode 100644 index 0000000..6d82a8c --- /dev/null +++ b/plugins/redmine_contacts/assets/stylesheets/contacts_sidebar.css @@ -0,0 +1,9 @@ +#sidebar{ float: right; width: 30%; position: relative; z-index: 9; min-height: 600px; padding: 0; margin: 0;} +* html #sidebar{ width: 30%; } +#sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } +* html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } + +#content { width: 67%; background-color: #fff; margin: 0px; z-index: 10; } +* html #content{ width: 67%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} +html>body #content { min-height: 600px; overflow: visible; } +* html body #content { height: 600px; } /* IE */ \ No newline at end of file diff --git a/plugins/redmine_contacts/assets/stylesheets/jquery.tagit.css b/plugins/redmine_contacts/assets/stylesheets/jquery.tagit.css new file mode 100755 index 0000000..fdddc6c --- /dev/null +++ b/plugins/redmine_contacts/assets/stylesheets/jquery.tagit.css @@ -0,0 +1,54 @@ +ul.tagit { + padding: 1px 5px; + overflow: auto; + margin-left: inherit; /* usually we don't want the regular ul margins. */ + margin-right: inherit; +} +ul.tagit li { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit li.tagit-choice { + padding: .2em 18px .2em .5em; + position: relative; + line-height: inherit; +} +ul.tagit li.tagit-new { + padding: .25em 4px .25em 0; +} + +ul.tagit li.tagit-choice a.tagit-label { + cursor: pointer; + text-decoration: none; +} +ul.tagit li.tagit-choice .tagit-close { + cursor: pointer; + position: absolute; + right: .1em; + top: 50%; + margin-top: -8px; +} + +/* used for some custom themes that don't need image icons */ +ul.tagit li.tagit-choice .tagit-close .text-icon { + display: none; +} + +ul.tagit li.tagit-choice input { + display: block; + float: left; + margin: 2px 5px 2px 0; +} +ul.tagit input[type="text"] { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + border: none; + margin: 0; + padding: 0; + width: inherit; + background-color: inherit; + outline: none; +} \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/az.yml b/plugins/redmine_contacts/config/locales/az.yml new file mode 100644 index 0000000..499bbff --- /dev/null +++ b/plugins/redmine_contacts/config/locales/az.yml @@ -0,0 +1,265 @@ +# encoding: utf-8 +# +# Translated by Saadat Mutallimova +# Data Processing Center of the Ministry of Communication and Information Technologies +# +az: + contacts_title: ÆlaqÉ™ mÉ™lumatları + + label_crm_recently_viewed: Son baxılanlar + label_crm_gravatar_enabled: ÆlaqÉ™ mÉ™lumatları üçün Gravatar istifadÉ™ etmÉ™k + label_crm_thumbnails_enabled: QeydlÉ™rdÉ™ tÉ™svirlÉ™rin miniatürlÉ™rini É™ks etdirmÉ™k + label_crm_max_thumbnail_file_size: Æks olunan miniatürün maksimal ölçüsü + label_crm_view_all_contacts: Bütün É™laqÉ™ mÉ™lumatlarlı + label_crm_background_info: ÆlavÉ™ mÉ™lumatlar + label_crm_company: ŞöbÉ™ + label_contact_plural: ÆlaqÉ™ mÉ™lumatları + label_crm_contact_edit_information: ÆlaqÉ™ mÉ™lumatına görÉ™ informasiyanı redaktÉ™ etmÉ™k + label_crm_edit_tags: TeqlÉ™ri redaktÉ™ etmÉ™k + label_crm_contact_view: Baxış + label_crm_contact_list: Siyahı + label_crm_contact_new: Yeni É™laqÉ™ mÉ™lumatı + label_crm_at_company: + label_crm_last_notes: Son qeydlÉ™r + label_crm_tags_plural: TeqlÉ™r + label_crm_multi_tags_plural: Bir neçə teqin seçimi + label_crm_single_tag_mode: Bir teq + label_crm_multiple_tags_mode: Bir neçə teq + label_crm_contact_tag: Teq + label_crm_time_ago: geriyÉ™ + label_crm_add_note_plural: Qeydi É™lavÉ™ etmÉ™k + label_crm_note_plural: QeydlÉ™r + + label_crm_add_tags_rule: vergül üzÉ™rindÉ™n bir neçə teq + label_crm_contact_search: Ada görÉ™ axtarış + label_crm_note_for: üçün qeyd + label_crm_show_on_map: XÉ™ritÉ™dÉ™ göstÉ™rmÉ™k + label_crm_add_another_phone: telefon É™lavÉ™ etmÉ™k + label_crm_remove: silmÉ™k + label_crm_related_contacts: ÆlaqÉ™li É™laqÉ™ mÉ™lumatları + label_crm_assigned_to: MÉ™sul ÅŸÉ™xs + label_crm_issue_added: Tapşırıq É™lavÉ™ olunub + label_crm_add_emails_rule: vergül üzÉ™rindÉ™n bir neçə email + label_crm_add_phones_rule: vergül üzÉ™rindÉ™n bir neçə telefon + label_crm_add_employee: Yeni É™mÉ™kdaÅŸ + label_crm_merge_duplicate_plural: BirləşdirmÉ™k + label_crm_duplicate_plural: Mümkün dublikatlar + label_crm_duplicate_for_plural: üçün mümkün dublikatlar + label_crm_add_tag: + teq É™lavÉ™ etmÉ™k + + label_crm_note_show_extras: ÆlavÉ™ olaraq (tip, tarix, fayl) + label_crm_note_hide_extras: ParametrlÉ™ri gizlÉ™tmÉ™k + label_crm_note_added: Qeyd müvÉ™ffÉ™qiyyÉ™tlÉ™ É™lavÉ™ olundu + label_crm_note_read_more: (bütün qeyd) + label_crm_invoice_import: CSV-dÉ™n hesabı yüklÉ™mÉ™k + + label_deal_plural: RazılaÅŸma + label_crm_contractor_plural: RazılaÅŸmanın iÅŸtirakçıları + label_deal: RazılaÅŸma + label_crm_deal_new: Yeni razılaÅŸma + label_crm_deal_edit_information: RazılaÅŸma üzrÉ™ informasiyanı redaktÉ™ etmÉ™k + label_crm_deal_change_status: Statusu dÉ™yiÅŸmÉ™k + label_crm_statistics: Statistika + + label_crm_deal_status_new: Yeni + label_crm_deal_status_first_contact: Birinci É™laqÉ™ mÉ™lumatı + label_crm_deal_status_negotiations: Danışıqlar + label_crm_deal_status_pending: QÉ™rarın qÉ™bul edilmÉ™si + label_crm_deal_status_won: Qazanılıb + label_crm_deal_status_lost: RÉ™dd edilib + label_crm_deals_import: CSV-dÉ™n razılaÅŸmanı yüklÉ™mÉ™k + + label_crm_created_on: Yaratma tarixi + + field_note_date: Qeyd tarixi + + field_deal_name: Ad + field_deal_background: TÉ™svir + field_deal_contact: RazılaÅŸmanın iÅŸtirakçısı + field_deal_price: MÉ™bləğ + field_price: MÉ™bləğ + + field_contact_avatar: Foto + field_contact_is_company: ŞöbÉ™ + field_contact_name: Ad + field_contact_last_name: Soyad + field_contact_first_name: Ad + field_contact_middle_name: Atasının adı + field_contact_job_title: VÉ™zifÉ™ + field_contact_company: ŞöbÉ™ + field_contact_address: Ünvan + field_contact_phone: Telefon + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_background: ÆlavÉ™ mÉ™lumat + field_contact_status: Status + field_contact_tag_names: TeqlÉ™r + field_first_name: Ad + field_last_name: Soyad + field_company: ŞöbÉ™ + field_birthday: Ad günü + field_contact_department: ŞöbÉ™ + + field_company_field: FÉ™aliyyÉ™t növü + + field_color: RÉ™ng + + button_add_note: Qeydi É™lavÉ™ etmÉ™k + notice_successful_save: Yadda saxlama müvÉ™ffÉ™qiyyÉ™tlÉ™ yerinÉ™ yetirildi + notice_successful_add: Yaratma müvÉ™ffÉ™qiyyÉ™tlÉ™ yerinÉ™ yetirildi + notice_unsuccessful_save: Yadda saxlamaq mümkün deyildir + notice_successful_merged: BirləşmÉ™ müvÉ™ffÉ™qiyyÉ™tlÉ™ yerinÉ™ yetirildi + + notice_merged_warning: Bu É™laqÉ™ mÉ™lumatları ilÉ™ baÄŸlı olan bütün layihÉ™lÉ™r, qeydlÉ™r, teqlÉ™r vÉ™ tapşırıqlar aÅŸağıdakı siyahıda seçilmiÅŸlÉ™rÉ™ É™lavÉ™ edilÉ™cÉ™k, bu É™laqÉ™ mÉ™lumatı isÉ™ silinÉ™cÉ™k. + + project_module_contacts: ÆlaqÉ™ mÉ™lumatları + + permission_view_contacts: ÆlaqÉ™ mÉ™lumatlarına baxış + permission_edit_contacts: ÆlaqÉ™ mÉ™lumatlarının redaktÉ™si + permission_delete_contacts: ÆlaqÉ™ mÉ™lumatlarının silinmÉ™si + permission_view_deals: SaziÅŸlÉ™rÉ™ baxış + permission_edit_deals: SaziÅŸlÉ™rin redaktÉ™si + permission_delete_deals: SaziÅŸlÉ™rin silinmÉ™si + permission_add_notes: QeydlÉ™rin É™lavÉ™ edilmÉ™si + permission_delete_notes: QeydlÉ™rin silinmÉ™si + permission_delete_own_notes: Şəxsi qeydlÉ™rin silinmÉ™si + + # 2.0.0 + label_crm_deal_category: SaziÅŸin kateqoriyası + label_crm_deal_category_plural: SaziÅŸlÉ™rin kateqoriyaları + label_crm_deal_category_new: Yeni kateqoriya + text_deal_category_destroy_assignments: Kateqoriyanın tÉ™yinatının silinmÉ™si + text_deal_category_destroy_question: "Bir neçə saziÅŸ (%{count}) назначено в данную категорию. Siz nÉ™ etmÉ™k istÉ™yirsiniz?" + text_deal_category_reassign_to: Bu kateqoriya üçün saziÅŸlÉ™ri tÉ™krar tÉ™yin etmÉ™k + text_deals_destroy_confirmation: 'Siz seçilÉ™n saziÅŸlÉ™ri silmÉ™k istÉ™diyinizÉ™ É™minsinizmi?' + label_crm_deal_status_plural: SaziÅŸlÉ™rin statusları + label_crm_deal_status: SaziÅŸin statusu + field_deal_status_is_closed: BaÄŸlanıb + label_crm_deal_status_new: Yeni + permission_manage_contacts: Soraq kitabçalarının sazlanması + label_crm_sales_funnel: Satışların filtri + label_crm_period: MüddÉ™t + label_crm_count: Miqdar + + #2.0.1 + label_crm_user_format: Adın formatı + label_crm_my_contact_plural: MÉ™nÉ™ tÉ™yin olunmuÅŸ É™laqÉ™ mÉ™lumatları + label_crm_my_deal_plural: MÉ™nÉ™ tÉ™yin olunmuÅŸ açıq saziÅŸlÉ™r + label_crm_contact_view_all: Bütün É™laqÉ™ mÉ™lumatlarına baxış + label_crm_deal_view_all: Bütün saziÅŸlÉ™rÉ™ baxış + + #2.0.2 + label_crm_bulk_edit_selected_contacts: SeçilmiÅŸ bütün É™laqÉ™ mÉ™lumatlarını redaktÉ™ etmÉ™k + label_crm_bulk_edit_selected_deals: SeçilmiÅŸ bütün saziÅŸlÉ™ri redaktÉ™ etmÉ™k + label_crm_bulk_send_mail_selected_contacts: SeçilmiÅŸ É™laqÉ™ mÉ™lumatlarına mÉ™ktub göndÉ™rmÉ™k + field_add_tags: TeqlÉ™ri É™lavÉ™ etmÉ™k + field_delete_tags: TeqlÉ™ri çıxartmaq + label_crm_send_mail: MÉ™ktub göndÉ™rmÉ™k + error_empty_email: Email boÅŸ ola bilmÉ™z + permission_send_contacts_mail: MÉ™ktubları göndÉ™rmÉ™k + field_mail_from: GöndÉ™rÉ™n ÅŸÉ™xs + text_email_macros: Makroslar %{macro} + field_message: MÉ™lumat + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: ÆlaqÉ™ mÉ™lumatı + field_age: YaÅŸ + label_crm_vcf_import: vCard-dan daxil etmÉ™k + label_crm_mail_from: + permission_import_contacts: ÆlaqÉ™ mÉ™lumatlarını daxil etmÉ™k + + #2.1.0 + field_company_name: ŞöbÉ™nin adı + label_crm_recently_added_contacts: Yeni daxil edilÉ™n É™laqÉ™ mÉ™lumatları + label_crm_created_by_me: TÉ™rÉ™fimdÉ™n yaradılan É™laqÉ™ mÉ™lumatları + my_contacts: MÉ™nim É™laqÉ™ mÉ™lumatlarım + my_deals: MÉ™nim saziÅŸlÉ™rim + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: ZÉ™ng + label_crm_note_type_meeting: Görüş + field_deal_currency: Valyuta + label_crm_my_contacts_stats: ÆlaqÉ™ mÉ™lumatlarına görÉ™ bu aya olan statistika + label_crm_contacts_created: ÆlaqÉ™ mÉ™lumatları É™lavÉ™ olunub + label_crm_deals_created: SaziÅŸlÉ™r É™lavÉ™ olunub + my_contacts_avatars: MÉ™nim É™laqÉ™ mÉ™lumatlarımın ÅŸÉ™killÉ™ri + my_contacts_stats: ÆlaqÉ™ mÉ™lumatları üzrÉ™ statistika + label_crm_add_into: É™lavÉ™ etmÉ™k + label_crm_delete_from: silmÉ™k + label_crm_show_deaks_tab: SaziÅŸ niÅŸanını göstÉ™rmÉ™k + label_crm_show_on_projects_show: LayihÉ™nin xülasÉ™sindÉ™ É™laqÉ™ mÉ™lumatlarını göstÉ™rmÉ™k + + #2.2.1 + label_crm_contacts_show_in_list: Siyahıda göstÉ™rmÉ™k + + #2.3.0 + label_crm_module_plural: Modullar + label_crm_list_partial_style: ÆlaqÉ™ mÉ™lumatlarının siyahısını É™ks etdirmÉ™k + label_crm_list_excerpt: Siyahı + label_crm_list_cards: Kartoçkalarla + label_crm_list_list: CÉ™dvÉ™l ilÉ™ + field_contacts: ÆlaqÉ™ mÉ™lumatı + field_companies: ŞöbÉ™ + label_crm_added_by: ÆlavÉ™ edib + label_crm_contact_note_authoring_time: QeydlÉ™rin vaxtını göstÉ™rmÉ™k + label_crm_contact_issues_filters: Tapşırıqlara görÉ™ filtrlÉ™r + label_crm_csv_import: CSV-dÉ™n daxil etmÉ™k + label_crm_upload_encoding: Faylın kodlaÅŸdırılması + label_crm_csv_file: CSV fayl + label_crm_csv_separator: Ayırıcı + field_middle_name: Atasının adı + field_job_title: VÉ™zifÉ™ + field_company: ŞöbÉ™ + field_address: Ünvan + field_phone: Telefon + field_email: Email + field_tags: TeqlÉ™r + field_is_company: ŞöbÉ™ hesab edilir + field_contact_full_name: Tam adı + field_last_note: Son qeyd + button_contacts_edit_query: SorÄŸunu redaktÉ™ etmÉ™k + button_contacts_delete_query: SorÄŸunu silmÉ™k + permission_manage_public_contacts_queries: Ümumi sorÄŸuların idarÉ™ edilmÉ™si + permission_add_deals: SaziÅŸlÉ™rin É™lavÉ™ edilmÉ™si + permission_add_contacts: ÆlaqÉ™ mÉ™lumatlarının É™lavÉ™ edilmÉ™si + permission_save_contacts_queries: SorÄŸuların yadda saxlanılması + + #2.3.3 + label_crm_contact_show_in_app_menu: Proqram menyusunda É™lavÉ™lÉ™ri göstÉ™rmÉ™k + + #2.3.4 + label_crm_contact_show_closed_issues: BaÄŸlı tapşırıqları göstÉ™rmÉ™k + + #3.0.0 + label_crm_import: Daxil etmÉ™k + label_contact_note_plural: ÆlaqÉ™ mÉ™lumatlarının qeydlÉ™ri + label_deal_note_plural: SaziÅŸlÉ™rin qeydlÉ™ri + label_crm_contact_all_note_plural: Bütün qeydlÉ™r + error_unable_delete_deal_status: SaziÅŸin statusunu silmÉ™k mümkün deyildir + label_crm_contacts_hidden: ÆlavÉ™ parametrlÉ™r + + #3.1.0 + label_crm_contact_added: ÆlaqÉ™ mÉ™lumatı É™lavÉ™ olunub + label_crm_note_added: Qeyd É™lavÉ™ olunub + label_crm_deal_added: SaziÅŸ É™lavÉ™ olunub + label_crm_deal_updated: SaziÅŸ dÉ™yiÅŸdirilib + text_crm_contact_added: "Yeni É™laqÉ™ mÉ™lumatı yaradılıb %{name} (%{author})." + text_crm_deal_added: "Yeni saziÅŸ yaradılıb %{name} (%{author})." + text_crm_deal_status_changed: "SaziÅŸin statusu %{old}-dan %{new}-a dÉ™yiÅŸdirilib" + text_crm_deal_updated: "%{name} saziÅŸi (%{author}) tÉ™rÉ™findÉ™n yenilÉ™nib." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: CSV-dÉ™n daxil etmÉ™k + permission_import_deals: SaziÅŸlÉ™ri daxil etmÉ™k + label_crm_single_quotes: "TÉ™k dırnaq iÅŸarÉ™si (')" + label_crm_double_quotes: "Cüt dırnaq iÅŸarÉ™si (\")" + label_crm_quotes_type: Dırnaq iÅŸarÉ™lÉ™rinin tipi + label_crm_contacts_visibility: GörünmÉ™ dÉ™rÉ™cÉ™si + label_crm_contacts_visibility_project: LayihÉ™lÉ™rdÉ™ hüquqlar görÉ™ + label_crm_contacts_visibility_public: KütlÉ™vi + label_crm_contacts_visibility_private: Şəxsi + permission_view_private_contacts: Şəxsi É™laqÉ™ mÉ™lumatlarına baxış + text_crm_error_on_line: "%{line}sÉ™trindÉ™ sÉ™hv: %{error}" \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/cs.yml b/plugins/redmine_contacts/config/locales/cs.yml new file mode 100644 index 0000000..52c6897 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/cs.yml @@ -0,0 +1,144 @@ +# encoding: utf-8 +cs: + contacts_title: Kontakty + + label_crm_recently_viewed: Nedávno zobrazené + label_crm_gravatar_enabled: Použít gravatar + label_crm_view_all_contacts: Zobraz vÅ¡echny kontakty + label_crm_background_info: Poznámky o kontaktu + label_crm_company: SpoleÄnost + label_contact_plural: Kontakty + label_crm_contact_edit_information: Úprava kontaktních informací + label_crm_edit_tags: Upravit Å¡títky + label_crm_contact_view: Zobrazit + label_crm_contact_list: Seznam + label_crm_contact_new: Nový + label_crm_at_company: v + label_crm_last_notes: Poslední poznámky + label_crm_tags_plural: Å títky + label_crm_multi_tags_plural: Vyberte více Å¡títků + label_crm_single_tag_mode: Jeden Å¡títek + label_crm_multiple_tags_mode: Více Å¡títků + label_crm_contact_tag: Å títek + label_crm_time_ago: pÅ™ed + label_crm_add_note_plural: PÅ™idat poznámky k + label_crm_note_plural: Poznámky + + label_crm_add_tags_rule: oddÄ›lit Äárkami + label_crm_contact_search: Vyhledat podle jména + label_crm_note_for: Poznámka pro + label_crm_show_on_map: Zobrazit na mapÄ› + label_crm_add_another_phone: pÅ™idat telefon + label_contact_note_plural: VÅ¡echny poznámky + label_crm_remove: smazat + label_crm_related_contacts: Související kontakty + label_crm_assigned_to: OdpovÄ›dný + label_crm_issue_added: Událost pÅ™idána + label_crm_add_emails_rule: oddÄ›lit Äárkami + label_crm_add_phones_rule: oddÄ›lit Äárkami + + label_crm_note_show_extras: PokroÄilé (soubory, datum) + label_crm_note_hide_extras: Skrýt pokroÄilé + label_crm_note_added: Poznámka úspěšnÄ› pÅ•idána + + label_deal_plural: Dohody + label_crm_contractor_plural: ZprostÅ™edkovatelé + label_deal: Dohoda + label_crm_deal_new: Nová dohoda + label_crm_deal_edit_information: Upravit informace o dohodÄ› + label_crm_deal_change_status: ZmÄ›nit stav + label_crm_statistics: Statistiky + + field_note_date: Datum poznámky + + field_deal_name: Jméno + field_deal_background: Poznámky k nabídce + field_deal_contact: Kontakt + field_deal_price: Suma + field_price: Suma + + field_contact_avatar: Avatar + field_contact_is_company: Kontakt na spoleÄnost + field_contact_name: Jméno + field_contact_last_name: Příjmení + field_contact_first_name: Jméno + field_contact_middle_name: ProstÅ™ední jméno + field_contact_job_title: Pracovní pozice + field_contact_company: SpoleÄnost + field_contact_address: Adresa + field_contact_phone: Telefon + field_contact_email: Email + field_contact_website: Webová stránka + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Poznámky ke kontaktu + field_contact_tag_names: Å títky + field_first_name: Jméno + field_last_name: Příjmení + field_company: SpoleÄnost + field_birthday: Narozeniny + field_contact_department: OddÄ›lení + + field_company_field: Obor Äinnosti + + button_add_note: PÅ™idat poznámku + notice_successful_save: Uloženo + notice_successful_add: VytvoÅ™eno + notice_unsuccessful_save: Chyba pÅ™i uložení + + project_module_contacts: Kontakty + + permission_view_contacts: Zobrazit kontakty + permission_edit_contacts: Upravit kontakty + permission_delete_contacts: Smazat kontakty + permission_view_deals: Zobrazit dohody + permission_edit_deals: Upravit dohody + permission_delete_deals: Smazat dohody + permission_add_notes: PÅ™idat poznámky + permission_delete_notes: Smazat poznámky + permission_delete_own_notes: Smazat vlastní poznámky + + # 2.0.0 + label_crm_deal_category: Kategorie dohody + label_crm_deal_category_plural: Nabídky dohod + label_crm_deal_category_new: Nová kategorie + text_deal_category_destroy_assignments: Odebrat pÅ™iÅ™azení ke kategorii + text_deal_category_destroy_question: "NÄ›které dohody (%{count}) jsou pÅ™iÅ™azeny této kategorii. Co chcete udÄ›lat?" + text_deal_category_reassign_to: PÅ™idÄ›lit dohody k této kategorii + text_deals_destroy_confirmation: 'Opravdu chcete smazat vybrané dohody?' + label_crm_deal_status_plural: Stavy dohod + label_crm_deal_status: Status dohody + field_deal_status_is_closed: UzavÅ™ený + label_crm_deal_status_new: Nový + permission_manage_contacts: VýÄty + label_crm_sales_funnel: PÅ™ehled obchodu + label_crm_period: Období + label_crm_count: PoÄet + + #2.0.1 + label_crm_user_format: Formát jména kontaktu + label_crm_my_contact_plural: Kontakty pÅ™iÅ™azené mÄ› + label_crm_my_deal_plural: OtevÅ™ené dohody pÅ™iÅ™azené mÄ› + label_crm_contact_view_all: Zobrazit vÅ¡echny kontakty + label_crm_deal_view_all: Zobrazit vÅ¡echny dohody + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Upravit vÅ¡echny vybrané kontakty + label_crm_bulk_edit_selected_deals: Upravit vÅ¡echny vybrané dohody + label_crm_bulk_send_mail_selected_contacts: Odeslat email vybraným kontaktům + field_add_tags: PÅ™idat Å¡títy + field_delete_tags: Smazat Å¡títky + label_crm_send_mail: Odeslat email + error_empty_email: Email nemůže být prázdný + permission_send_contacts_mail: Odeslat email + field_mail_from: Z adresy + text_email_macros: Dostupná makra %{macro} + field_message: Zpráva + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Kontakt + field_age: VÄ›k + label_crm_vcf_import: Importovat z vCard + label_crm_mail_from: Od + permission_import_contacts: Importovat kontakty \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/da.yml b/plugins/redmine_contacts/config/locales/da.yml new file mode 100644 index 0000000..6f539b4 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/da.yml @@ -0,0 +1,227 @@ +# encoding: utf-8 +da: + contacts_title: Kontakter + + label_crm_recently_viewed: Senest vist + label_crm_gravatar_enabled: Brug Gravatar + label_crm_thumbnails_enabled: Vis miniaturebilleder i noter + label_crm_max_thumbnail_file_size: Maks filstørrelse pÃ¥ miniaturebilleder + label_crm_view_all_contacts: Vis alle kontakter + label_crm_background_info: Baggrundsinformation + label_crm_company: Virksomhed + label_contact_plural: Kontakter + label_crm_contact_edit_information: Redigerer kontaktinformation + label_crm_edit_tags: Rediger tags + label_crm_contact_view: Vis + label_crm_contact_list: Liste + label_crm_contact_new: Ny kontakt + label_crm_at_company: hos + label_crm_last_notes: Seneste noter + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Vælg flere tags + label_crm_single_tag_mode: Enkelt tag + label_crm_multiple_tags_mode: Flere tags + label_crm_contact_tag: Tag + label_crm_time_ago: siden + label_crm_add_note_plural: Tilføj note til + label_crm_note_plural: Noter + + label_crm_add_tags_rule: del med komma + label_crm_contact_search: Søg efter navn + label_crm_note_for: Note for + label_crm_show_on_map: Vis pÃ¥ kort + label_crm_add_another_phone: tilføj telefon + label_contact_note_plural: Alle noter + label_crm_remove: slet + label_crm_related_contacts: Relaterede kontakter + label_crm_assigned_to: Ansvarlig + label_crm_issue_added: Sag tilføjet + label_crm_add_emails_rule: del med komma + label_crm_add_phones_rule: del med komma + label_crm_add_employee: Ny medarbejder + label_crm_merge_duplicate_plural: Sammenflet + label_crm_duplicate_plural: Mulige kopier + label_crm_duplicate_for_plural: Mulige kopier for + label_crm_add_tag: Tilføj ny... + + label_crm_note_show_extras: Avanceret (type, dato, filer) + label_crm_note_hide_extras: Skjul avanceret + label_crm_note_added: Noten er blevet tilføjet + label_crm_note_read_more: (læs mere) + + label_deal_plural: Tilbud + label_crm_contractor_plural: Kontakter + label_deal: Tilbud + label_crm_deal_new: Nyt tilbud + label_crm_deal_edit_information: Rediger tilbudsinformation + label_crm_deal_change_status: Ændre status + label_crm_statistics: Statistik + + label_crm_deal_status_new: Ny + label_crm_deal_status_first_contact: Første kontakt + label_crm_deal_status_negotiations: Forhandlinger + label_crm_deal_status_pending: Afventer + label_crm_deal_status_won: Vundet + label_crm_deal_status_lost: Tabt + + label_crm_created_on: Oprettet d. + + field_note_date: Note dato + + field_deal_name: Navn + field_deal_background: Baggrund + field_deal_contact: Kontakt + field_deal_price: Sum + field_price: Sum + + field_contact_avatar: Billede + field_contact_is_company: Virksomhed + field_contact_name: Navn + field_contact_last_name: Efternavn + field_contact_first_name: Fornavn + field_contact_middle_name: Mellemnavn + field_contact_job_title: Stilling + field_contact_company: Virksomhed + field_contact_address: Addresse + field_contact_phone: Telefon + field_contact_email: Email + field_contact_website: Hjemmeside + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Baggrund + field_contact_tag_names: Tags + field_first_name: Fornavn + field_last_name: Efternavn + field_company: Virksomhed + field_birthday: Fødselsdag + field_contact_department: Afdeling + + + field_company_field: Branche + + field_color: Farve + + button_add_note: Tilføj note + notice_successful_save: Gemt + notice_successful_add: Oprettet + notice_unsuccessful_save: Problemer med at gemme + notice_successful_merged: Sammenflettet uden problemer + + notice_merged_warning: Alle noter, projekter, tags og opgaver knyttet til denne person vil blive flyttet til nedenstÃ¥ende valg. Herefter vil kontakten blive slettet. + + project_module_contacts: Kontakter + + permission_view_contacts: Vis kontakter + permission_edit_contacts: Ret kontakter + permission_delete_contacts: Slet kontakter + permission_view_deals: Vis tilbud + permission_edit_deals: Ret tilbud + permission_delete_deals: Slet tilbud + permission_add_notes: Tilføj noter + permission_delete_notes: Slet noter + permission_delete_own_notes: Slet egne noter + + # 2.0.0 + label_crm_deal_category: Tilbudskategori + label_crm_deal_category_plural: Tilbudskategorier + label_crm_deal_category_new: Ny kategori + text_deal_category_destroy_assignments: Slet tildelte kategorier + text_deal_category_destroy_question: "Et antalt tilbud (%{count}) er tildelt denne kategori. Vil vil du gøre?" + text_deal_category_reassign_to: Tildel disse tilbud til denne kategori + text_deals_destroy_confirmation: 'Er du sikker pÃ¥ du vil slette de(t) valgte tilbud?' + label_crm_deal_status_plural: Tilbudsstatus + label_crm_deal_status: Tilbudsstatus + field_deal_status_is_closed: Lukket + label_crm_deal_status_new: Ny + permission_manage_contacts: Tællinger + label_crm_sales_funnel: Salgstragt + label_crm_period: Periode + label_crm_count: Antal + + #2.0.1 + label_crm_user_format: Kontaktformat + label_crm_my_contact_plural: Kontakter tildelt til mig + label_crm_my_deal_plural: Ã…bne tilbud tildelt til mig + label_crm_contact_view_all: Vis alle kontakter + label_crm_deal_view_all: Vis alle tilbud + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Ret alle valgte kontakter + label_crm_bulk_edit_selected_deals: Ret alle valgte tilbud + label_crm_bulk_send_mail_selected_contacts: Send email til valgte kontakter + field_add_tags: Tilføj tags + field_delete_tags: Slet tags + label_crm_send_mail: Send email + error_empty_email: Email kan ikke være blank + permission_send_contacts_mail: Send mail + field_mail_from: Fra adresse + text_email_macros: Tilgængelige makroer %{macro} + field_message: Besked + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Kontakt + field_age: Alder + label_crm_vcf_import: Importer fra vCard + label_crm_mail_from: Fra + permission_import_contacts: Importer kontakter + + #2.1.0 + field_company_name: Virksomhedsnavn + label_crm_recently_added_contacts: Seneste tilføjede kontakter + label_crm_created_by_me: Kontakter oprettet af mig + my_contacts: Mine kontakter + my_deals: Mine tilbud + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Opkald + label_crm_note_type_meeting: Møde + field_deal_currency: Valuta + label_crm_my_contacts_stats: Kontaktens statistik for denne mÃ¥ned + label_crm_contacts_created: Kontakter oprettet + label_crm_deals_created: Tilbud oprettet + my_contacts_avatars: Mine kontakters fotos + my_contacts_stats: Mine kontakter statistikker + label_crm_add_into: Tilføj til + label_crm_delete_from: Slet fra + label_crm_show_deaks_tab: Vis tilbudsfane + label_crm_show_on_projects_show: Vis kontakter pÃ¥ projektoversigt + + #2.2.1 + label_crm_contacts_show_in_list: Vis i liste + + #2.3.0 + label_crm_module_plural: Moduler + label_crm_list_partial_style: Kontaktliste udseende + label_crm_list_excerpt: Uddragsliste + label_crm_list_cards: Kort + label_crm_list_list: Tabel + field_contacts: Kontakt + field_companies: Virksomhed + label_crm_added_by: Tilføjet af + label_crm_contact_note_authoring_time: Vis notetidspunkt + label_crm_contact_issues_filters: Sager filtreret + label_crm_csv_import: Importer kontakter fra CSV + label_crm_upload_encoding: Filkodning + label_crm_csv_file: CSV fil + label_crm_csv_separator: Adskiller + field_middle_name: Mellemnavn + field_job_title: Stilling + field_company: Virksomhed + field_address: Adresse + field_phone: Telefon + field_email: Email + field_tags: Tags + field_last_note: Seneste note + field_is_company: Er virksomhed + field_contact_full_name: Fulde navn + button_contacts_edit_query: Ret forespørgsel + button_contacts_delete_query: Slet forespørgsel + permission_manage_public_contacts_queries: Administrer offentlige forespørgsler + permission_add_deals: Tilføj tilbud + permission_add_contacts: Tilføj kontakter + permission_save_contacts_queries: Gem forespørgsler + + #2.3.3 + label_crm_contact_show_is_app_menu: Vis faner i app-menuen \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/de.yml b/plugins/redmine_contacts/config/locales/de.yml new file mode 100644 index 0000000..bb0810e --- /dev/null +++ b/plugins/redmine_contacts/config/locales/de.yml @@ -0,0 +1,578 @@ +# encoding: utf-8 +de: + contacts_title: Kontakte + + label_crm_recently_viewed: Kürzlich angesehen + label_crm_gravatar_enabled: Gravatar benutzen + label_crm_thumbnails_enabled: Bildvorschau in Anmerkungen anzeigen + label_crm_max_thumbnail_file_size: Max. Größe der Bildvorschau + label_crm_view_all_contacts: Alle Kontakte anzeigen + label_crm_background_info: Hintergrundinformationen + label_crm_company: Unternehmen + label_contact_plural: Kontakte + label_crm_contact_edit_information: Kontaktinformation bearbeiten + label_crm_edit_tags: Tags bearbeiten + label_crm_contact_view: Anzeigen + label_crm_contact_list: Auflisten + label_crm_contact_new: Neu + label_crm_at_company: bei + label_crm_last_notes: Letzte Anmerkungen + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Mehrere Tags auswählen + label_crm_single_tag_mode: Einzelner Tag + label_crm_multiple_tags_mode: Mehrere Tags + label_crm_contact_tag: Tag + label_crm_time_ago: her + label_crm_add_note_plural: Anmerkung hinzufügen + label_crm_note_plural: Anmerkungen + + label_crm_add_tags_rule: Mit Kommata trennen + label_crm_contact_search: Nach Namen suchen + label_crm_note_for: Anmerkung für + label_crm_show_on_map: Auf Karte anzeigen + label_crm_add_another_phone: Telefon hinzufügen + label_crm_remove: löschen + label_crm_related_contacts: Ähnliche Kontakte + label_crm_assigned_to: Verantwortlich + label_crm_issue_added: Ticket hinzugefügt + label_crm_add_emails_rule: durch Komma separieren + label_crm_add_phones_rule: durch Komma separieren + label_crm_add_employee: Neuer Angestellter + label_crm_merge_duplicate_plural: Zusammenführen + label_crm_duplicate_plural: Mögliche Duplikate + label_crm_duplicate_for_plural: Mögliche Duplikate für + label_crm_add_tag: Neu hinzufügen... + + label_crm_note_show_extras: Erweiterte Optionen (Dateien, Datum) + label_crm_note_hide_extras: Erweiterte Optionen ausblenden + label_crm_note_added: Die Anmerkung wurde hinzugefügt + label_crm_note_read_more: (weiter lesen) + + label_deal_plural: Verkaufspotentiale + label_crm_contractor_plural: Verkaufskontakte + label_deal: Verkaufspotential + label_crm_deal_new: Neues Verkaufspotential + label_crm_deal_edit_information: Verkaufspotential bearbeiten + label_crm_deal_change_status: Status ändern + label_crm_statistics: Statistiken + + label_crm_deal_status_new: Neu + label_crm_deal_status_first_contact: Erster Kontakt + label_crm_deal_status_negotiations: Verhandlungen + label_crm_deal_status_pending: ausstehend + label_crm_deal_status_won: Gewonnen + label_crm_deal_status_lost: Verloren + + label_crm_created_on: Erstellt + + field_note_date: Datum der Anmerkung + + field_deal_name: Name + field_deal_background: Hintergrundinformation + field_deal_contact: Kontakt + field_deal_price: Preis + field_price: Preis + + field_contact_avatar: Avatar + field_contact_is_company: Unternehmen + field_contact_name: Name + field_contact_last_name: Nachname + field_contact_first_name: Vorname + field_contact_middle_name: Zwischenname + field_contact_job_title: Berufsbezeichnung + field_contact_company: Unternehmen + field_contact_address: Adresse + field_contact_phone: Telefon + field_contact_email: E-Mail + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Hintergrundinformationen + field_contact_tag_names: Tags + field_first_name: Name + field_last_name: Nachname + field_company: Unternehmen + field_birthday: Geburtstag + field_contact_department: Abteilung + + + field_company_field: Industrie + + field_color: Farbe + + button_add_note: Anmerkung hinzufügen + notice_successful_save: Gespeichert + notice_successful_add: Erfolgreich hinzugefügt + notice_unsuccessful_save: Konnte nicht gespeichert werden + notice_successful_merged: Erfolgreich zusammengeführt + + notice_merged_warning: Alle Anmerkungen, Projekte, Tags und Aufgaben die diesem Kontakt zugeordnet sind, werden entsprechend verschoben. Anschliessend wird der Kontakt gelöscht. + + project_module_contacts: Kontakte + + permission_view_contacts: Kontakte anzeigen + permission_edit_contacts: Kontakte bearbeiten + permission_delete_contacts: Kontakte löschen + permission_view_deals: Verkaufspotentiale anzeigen + permission_edit_deals: Verkaufspotential bearbeiten + permission_delete_deals: Verkaufspotential löschen + permission_add_notes: Anmerkungen hinzufügen + permission_delete_notes: Anmerkungen löschen + permission_delete_own_notes: Eigene Anmerkungen löschen + + # 2.0.0 + label_crm_deal_category: Vertriebskategorie + label_crm_deal_category_plural: Vertriebskategorien + label_crm_deal_category_new: Neue Vertriebskategorie + text_deal_category_destroy_assignments: Kategoriezuweisungen entfernen + text_deal_category_destroy_question: "Einige Verkaufspotentiale (%{count}) sind dieser Kategorie zugeordnet. Was wollen Sie tun?" + text_deal_category_reassign_to: Neuzuweisung der Verkaufspotential zu dieser Kategorie + text_deals_destroy_confirmation: 'Sind Sie sicher, dass Sie die ausgewählten Verkaufspotentiale löschen wollen?' + label_crm_deal_status_plural: Status + label_crm_deal_status: Status + field_deal_status_is_closed: Geschlossen + label_crm_deal_status_new: Neu + permission_manage_contacts: Aufzählungen + label_crm_sales_funnel: Verkaufskanal + label_crm_period: Dauer + label_crm_count: Anzahl + + #2.0.1 + label_crm_user_format: Format Kontaktname + label_crm_my_contact_plural: Mir zugeordnete Kontakte + label_crm_my_deal_plural: Mir zugeordnete offene Verkaufspotentiale + label_crm_contact_view_all: Alle Kontakte anzeigen + label_crm_deal_view_all: Alle Verkaufspotentiale anzeigen + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Alle ausgewählten Kontakte bearbeiten + label_crm_bulk_edit_selected_deals: Alle ausgewählten Verkaufspotentiale bearbeiten + label_crm_bulk_send_mail_selected_contacts: Sende E-Mail zu ausgewählten Kontakten + field_add_tags: Tags hinzufügen + field_delete_tags: Tags löschen + label_crm_send_mail: Sende E-Mail + error_empty_email: E-Mail darf nicht leer sein + permission_send_contacts_mail: Sende E-Mail + field_mail_from: Absender + text_email_macros: Verfügbare Makros %{macro} + field_message: Nachricht + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Kontakt + field_age: Alter + label_crm_vcf_import: Import von vCard + label_crm_mail_from: Von + permission_import_contacts: Kontakte importieren + + #2.1.0 + field_company_name: Unternehmen + label_crm_recently_added_contacts: Neuste Kontakte + label_crm_created_by_me: Kontakte hinzugefügt von mir + my_contacts: Meine Kontakte + my_deals: Meine Verkaufspotentiale + + #2.2.0 + label_crm_note_type_email: E-Mail + label_crm_note_type_call: Anruf + label_crm_note_type_meeting: Besprechung + field_deal_currency: Währung + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Hinzugefügte Kontakte + label_crm_deals_created: Hinzugefügte Verkaufspotentiale + my_contacts_avatars: Meine Kontaktfotos + my_contacts_stats: Statistiken zu Kontakten + label_crm_add_into: Hinzufügen in + label_crm_delete_from: Löschen von + label_crm_show_deaks_tab: Verkaufspotentiale-Reiter anzeigen + label_crm_show_on_projects_show: Kontakte auf Projekt-Übersichtsseite anzeigen + + #2.2.1 + label_crm_contacts_show_in_list: In Liste anzeigen + + #2.3.0 + label_crm_module_plural: Module + label_crm_list_partial_style: Kontakt-Listenansicht + label_crm_list_excerpt: Einspalte Liste + label_crm_list_cards: Karten + label_crm_list_list: Tabelle + field_contacts: Kontakt + field_companies: Unternehmen + label_crm_contact_note_authoring_time: Zeige Anmerkung mit Uhrzeit + label_crm_contact_issues_filters: Ticketfilter + label_crm_csv_import: Kontakte von CSV importieren + label_crm_upload_encoding: Datei Encoding + label_crm_csv_file: CSV Datei + label_crm_csv_separator: Separator + field_middle_name: Zwischenname + field_job_title: Berufsbezeichnung + field_company: Unternehmen + field_address: Adresse + field_phone: Telefon + field_email: E-Mail + field_tags: Tags + field_last_note: Letzte Anmerkung + field_is_company: Ist Unternehmen + field_contact_full_name: Vollständiger Name + button_contacts_edit_query: Abfrage bearbeiten + button_contacts_delete_query: Abfrage löschen + permission_manage_public_contacts_queries: Öffentliche Abfragen verwalten + permission_add_deals: Verkaufspotential hinzufügen + permission_add_contacts: Kontakte hinzufügen + permission_save_contacts_queries: Abfrage speichern + + #2.3.3 + label_crm_contact_show_in_app_menu: Menüeinträge in der Anwendung anzeigen + + #2.3.4 + label_crm_contact_show_closed_issues: Geschlossene Tickets anzeigen + + #3.0.0 + label_crm_import: Importieren + label_contact_note_plural: Kontaktanmerkungen + label_deal_note_plural: Deal Anmerkungen + label_crm_contact_all_note_plural: Alle Anmerkungen + error_unable_delete_deal_status: Deal Status kann nicht gelöscht werden + label_crm_contacts_hidden: Verborgene Einstellungen + #3.1.0 + label_crm_contact_added: Kontakt hinzugefügt + label_crm_note_added: Kommentar hinzugefügt + label_crm_deal_added: Verkaufspotential hinzugefügt + label_crm_deal_updated: Verkaufspotential aktualisiert + text_crm_contact_added: "Kontakt %{name} wurde von %{author} hinzugefügt." + text_crm_deal_added: "Verkaufspotential %{name} wurde von %{author} hinzugefügt." + text_crm_deal_status_changed: "Status der Verkaufspotential von %{old} auf %{new} geändert" + text_crm_deal_updated: "Verkaufspotential %{name} wurde von %{author} geändert." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Import von CSV + permission_import_deals: Verkaufspotentiale importieren + label_crm_single_quotes: "Einfache Anführungszeichen (')" + label_crm_double_quotes: "Doppelte Anführungszeichen (\")" + label_crm_quotes_type: Anführungszeichentyp + label_crm_contacts_visibility: Sichtbarkeit + label_crm_contacts_visibility_project: nach Projektberechtigungen + label_crm_contacts_visibility_public: Öffentlich + label_crm_contacts_visibility_private: Privat + permission_view_private_contacts: Private Kontakte anzeigen + text_crm_error_on_line: "Fehler in der Zeile %{line}: %{error}." + + #3.2.0 + label_crm_probability: Wahrscheinlichkeit + label_crm_deal_status_type: Statustyp + label_crm_select_companies: Filtere Firmen in Verkaufspotentiale + label_crm_expected_revenue: Erwarte Einnahmen + label_crm_deal_due_date: Fälligkeit + + #3.2.2 + label_crm_show_deals_in_top_menu: Zeige Verkaufspotentiale im oberen Menü + label_crm_show_details: Zeige Details + label_crm_has_deals: Hat Verkaufspotentiale + label_crm_has_open_issues: Ticket öffnen + label_crm_note: Hinweis + + #3.2.5 + notice_failed_to_save_contacts: "Speichern von %{count} Kontakt(en) von %{total} fehlgeschlagen: %{ids}." + + #3.2.6 + project_module_deals: Verkaufspotentiale + permission_manage_deals: Verkaufspotentiale verwalten + label_crm_deals_from_subprojects: Verkaufspotentiale für Unterprojekte anzeigen + label_crm_megre_tags: Tags zusammenführen + label_crm_monochrome_tags: Schwarzweiß Tags + + #3.2.7 + label_crm_address: Adresse + label_crm_street1: Straße 1 + label_crm_street2: Straße 2 + label_crm_city: Ort + label_crm_region: Bundesland + label_crm_postcode: PLZ + label_crm_country: Land + label_crm_countries: + AF: Afghanistan + AX: Alandinseln + AL: Albanien + DZ: Algerien + UM: Amerikanisch-Ozeanien + AS: Amerikanisch-Samoa + VI: Amerikanische Jungferninseln + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarktis + AG: Antigua und Barbuda + AR: Argentinien + AM: Armenien + AW: Aruba + AZ: Aserbaidschan + AU: Australien + BS: Bahamas + BH: Bahrain + BD: Bangladesch + BB: Barbados + BY: Belarus + BE: Belgien + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivien + BA: Bosnien und Herzegowina + BW: Botsuana + BV: Bouvetinsel + BR: Brasilien + VG: Britische Jungferninseln + IO: Britisches Territorium im Indischen Ozean + BN: Brunei Darussalam + BG: Bulgarien + BF: Burkina Faso + BI: Burundi + CL: Chile + CN: China + CK: Cookinseln + CR: Costa Rica + CI: Côte d’Ivoire + CD: Demokratische Republik Kongo + KP: Demokratische Volksrepublik Korea + DE: Deutschland + DM: Dominica + DO: Dominikanische Republik + DJ: Dschibuti + DK: Dänemark + EC: Ecuador + SV: El Salvador + ER: Eritrea + EE: Estland + FK: Falklandinseln + FJ: Fidschi + FI: Finnland + FR: Frankreich + GF: Französisch-Guayana + PF: Französisch-Polynesien + TF: Französische Süd- und Antarktisgebiete + FO: Färöer + GA: Gabun + GM: Gambia + GE: Georgien + GH: Ghana + GI: Gibraltar + GD: Grenada + GR: Griechenland + GL: Grönland + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard- und McDonald-Inseln + HN: Honduras + IN: Indien + ID: Indonesien + IQ: Irak + IR: Iran + IE: Irland + IS: Island + IM: Isle of Man + IL: Israel + IT: Italien + JM: Jamaika + JP: Japan + YE: Jemen + JE: Jersey + JO: Jordanien + KY: Kaimaninseln + KH: Kambodscha + CM: Kamerun + CA: Kanada + CV: Kap Verde + KZ: Kasachstan + QA: Katar + KE: Kenia + KG: Kirgisistan + KI: Kiribati + CC: Kokosinseln + CO: Kolumbien + KM: Komoren + CG: Kongo + HR: Kroatien + CU: Kuba + KW: Kuwait + LA: Laos + LS: Lesotho + LV: Lettland + LB: Libanon + LR: Liberia + LY: Libyen + LI: Liechtenstein + LT: Litauen + LU: Luxemburg + MG: Madagaskar + MW: Malawi + MY: Malaysia + MV: Malediven + ML: Mali + MT: Malta + MA: Marokko + MH: Marshallinseln + MQ: Martinique + MR: Mauretanien + MU: Mauritius + YT: Mayotte + MK: Mazedonien + MX: Mexiko + FM: Mikronesien + MC: Monaco + MN: Mongolei + ME: Montenegro + MS: Montserrat + MZ: Mosambik + MM: Myanmar + NA: Namibia + NR: Nauru + NP: Nepal + NC: Neukaledonien + NZ: Neuseeland + NI: Nicaragua + NL: Niederlande + AN: Niederländische Antillen + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolkinsel + "NO": Norwegen + MP: Nördliche Marianen + OM: Oman + TL: Osttimor + PK: Pakistan + PW: Palau + PS: Palästinensische Gebiete + PA: Panama + PG: Papua-Neuguinea + PY: Paraguay + PE: Peru + PH: Philippinen + PN: Pitcairn + PL: Polen + PT: Portugal + PR: Puerto Rico + KR: Republik Korea + MD: Republik Moldau + RW: Ruanda + RO: Rumänien + RU: Russische Föderation + RE: Réunion + SB: Salomonen + ZM: Sambia + WS: Samoa + SM: San Marino + SA: Saudi-Arabien + SE: Schweden + CH: Schweiz + SN: Senegal + RS: Serbien + CS: Serbien und Montenegro + SC: Seychellen + SL: Sierra Leone + ZW: Simbabwe + SG: Singapur + SK: Slowakei + SI: Slowenien + SO: Somalia + HK: Sonderverwaltungszone Hongkong + MO: Sonderverwaltungszone Macao + ES: Spanien + LK: Sri Lanka + BL: St. Barthélemy + SH: St. Helena + KN: St. Kitts und Nevis + LC: St. Lucia + MF: St. Martin + PM: St. Pierre und Miquelon + VC: St. Vincent und die Grenadinen + SD: Sudan + SR: Suriname + SJ: Svalbard und Jan Mayen + SZ: Swasiland + SY: Syrien + ST: São Tomé und Príncipe + ZA: Südafrika + GS: Südgeorgien und die Südlichen Sandwichinseln + TJ: Tadschikistan + TW: Taiwan + TZ: Tansania + TH: Thailand + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad und Tobago + TD: Tschad + CZ: Tschechische Republik + TN: Tunesien + TM: Turkmenistan + TC: Turks- und Caicosinseln + TV: Tuvalu + TR: Türkei + UG: Uganda + UA: Ukraine + ZZ: Unbekannte oder ungültige Region + HU: Ungarn + UY: Uruguay + UZ: Usbekistan + VU: Vanuatu + VA: Vatikanstadt + VE: Venezuela + AE: Vereinigte Arabische Emirate + US: Vereinigte Staaten + GB: Vereinigtes Königreich + VN: Vietnam + WF: Wallis und Futuna + CX: Weihnachtsinsel + EH: Westsahara + CF: Zentralafrikanische Republik + CY: Zypern + EG: Ägypten + GQ: Äquatorialguinea + ET: Äthiopien + AT: Österreich + label_crm_cross_project_contacts: Projektübergreifende Kontakte erlauben + label_crm_list_board: Board + label_crm_default_list_style: Standard-Listenansicht + label_crm_show_in_top_menu: Im oberen Menü anzeigen + label_crm_show_in_app_menu: In App Menü anzeigen + label_crm_money_settings: Währung + label_crm_disable_taxes: Steuern deaktiviert + label_crm_default_tax: Standard Steuersatz + label_crm_tax_type: Steuerart + label_crm_tax_type_inclusive: Steuer eingeschlossen + label_crm_tax_type_exclusive: Steuer ausgeschlossen + label_crm_default_currency: Standard Währung + label_crm_thousands_delimiter: Tausendertrennzeichen + label_crm_decimal_separator: Dezimaltrennzeichen + + #3.2.10 + label_crm_add_contact_plural: Kontakt hinzufügen + label_crm_search_for_contact: Kontakt suchen + label_crm_major_currencies: Aktive Währungen + + #3.2.11 + label_crm_post_address_format: Postadress-Format + label_crm_post_address_format_macros: "Adress-Format Makros: %{macros}" + + #3.2.14 + label_crm_last_year: Letztes Jahr + + #3.2.15 + permission_export_contacts: Export contacts and deals + + #3.4.0 + label_crm_deal_contact: Deal's contact + + #3.4.1 + label_crm_default_country: Default country + label_attribute_of_contact: "Kontaktname %{name}" + label_crm_contact_country: Contact's country + label_crm_contact_city: Contact's city \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/el.yml b/plugins/redmine_contacts/config/locales/el.yml new file mode 100644 index 0000000..a3f6acd --- /dev/null +++ b/plugins/redmine_contacts/config/locales/el.yml @@ -0,0 +1,592 @@ +# encoding: utf-8 +el: + contacts_title: Επαφές + + label_crm_recently_viewed: ΠÏοβλήθηκαν Ï€Ïόσφατα + label_crm_gravatar_enabled: ΧÏήση Gravatar + label_crm_thumbnails_enabled: Show image thumbnails in notes + label_crm_max_thumbnail_file_size: Max thumbnailed image size + label_crm_view_all_contacts: ΠÏοβολή όλων των πελατών + label_crm_background_info: ΠληÏοφοÏίες φόντου + label_crm_company: ΕταιÏεία + label_contact_plural: Επαφές + label_crm_contact_edit_information: ΔιόÏθωση ΠληÏοφοÏιών Επαφής + label_crm_edit_tags: ΔιόÏθωση ετικετών + label_crm_contact_view: ΠÏοβολή + label_crm_contact_list: Λίστα + label_crm_contact_new: Îέα επαφή + label_crm_at_company: στη + label_crm_last_notes: ΠÏόσφατες σημειώσεις + label_crm_tags_plural: Ετικέτες + label_crm_multi_tags_plural: Επιλογή πολλαπλών ετικετών + label_crm_single_tag_mode: Μια ετικέτα + label_crm_multiple_tags_mode: Πολλαπλές ετικέτες + label_crm_contact_tag: Ετικέτα + label_crm_time_ago: Ï€Ïιν + label_crm_add_note_plural: ΠÏοσθήκη σημείωσης σε + label_crm_note_plural: Σημειώσεις + + label_crm_add_tags_rule: διαχωÏισμός με κόμματα + label_crm_contact_search: Αναζήτηση με όνομα + label_crm_note_for: Σημείωση για + label_crm_show_on_map: ΠÏοβολή σε χάÏτη + label_crm_add_another_phone: Ï€Ïοσθήκη τηλεφώνου + label_crm_remove: διαγÏαφή + label_crm_related_contacts: Σχετικές επαφές + label_crm_assigned_to: ΥπεÏθυνος + label_crm_issue_added: Το θέμα Ï€Ïοστέθηκε + label_crm_add_emails_rule: διαχωÏισμός με κόμματα + label_crm_add_phones_rule: διαχωÏισμός με κόμματα + label_crm_add_employee: Îέος υπάλληλος + label_crm_merge_duplicate_plural: Συγχώνευση + label_crm_duplicate_plural: Πιθανά διπλότυπα + label_crm_duplicate_for_plural: Πιθανά διπλότυπα για + label_crm_add_tag: + Ï€Ïοσθήκη ετικέτας + + label_crm_note_show_extras: Για Ï€ÏοχωÏημένους (Ï„Ïπος, ημεÏομηνία, αÏχεία) + label_crm_note_hide_extras: ΑπόκÏυψη Ï€ÏοχωÏημένων + label_crm_note_added: Η σημείωση Ï€Ïοστέθηκε + label_crm_note_read_more: (διαβάστε πεÏισσότεÏα) + label_crm_invoice_import: Εισαγωγή τιμολογίων από CSV + + label_deal_plural: Συμφωνίες + label_crm_contractor_plural: Επαφές + label_deal: Συμφωνία + label_crm_deal_new: Îέα συμφωνία + label_crm_deal_edit_information: Αλλαγή στοιχείων συμφωνίας + label_crm_deal_change_status: Αλλαγή κατάστασης + label_crm_statistics: Στατιστικά + label_crm_deals_import: Εισαγωγή συμφωνιών από CSV + + label_crm_deal_status_new: Îέα + label_crm_deal_status_first_contact: ΠÏώτη επαφή + label_crm_deal_status_negotiations: ΔιαπÏαγματεÏσεις + label_crm_deal_status_pending: ΕκκÏεμεί + label_crm_deal_status_won: ΚέÏδισε + label_crm_deal_status_lost: Έχασε + + label_crm_created_on: ΔημιουÏγήθηκε στις + + field_note_date: ΗμεÏομηνία σημείωσης + field_background: Φόντο + field_currency: Îόμισμα + field_contact: Επαφή + + field_deal_name: Όνομα + field_deal_background: Φόντο + field_deal_contact: Επαφή + field_deal_price: ΣÏνολο + field_price: ΣÏνολο + + field_contact_avatar: ΦωτογÏαφία + field_contact_is_company: ΕταιÏεία + field_contact_name: Όνομα + field_contact_last_name: Επώνυμο + field_contact_first_name: Όνομα + field_contact_middle_name: Μεσαίο όνομα + field_contact_job_title: Θέση + field_contact_company: ΕταιÏεία + field_contact_address: ΔιεÏθυνση + field_contact_phone: Τηλέφωνο + field_contact_email: Email + field_contact_website: Ιστότοπος + field_contact_skype: Skype + field_contact_status: Κατάσταση + field_contact_background: Φόντο + field_contact_tag_names: Ετικέτες + field_first_name: Όνομα + field_last_name: Επώνυμο + field_company: ΕταιÏεία + field_birthday: ΗμεÏομηνία γέννησης + field_contact_department: Τμήμα + + + field_company_field: Βιομηχανία + + field_color: ΧÏώμα + + button_add_note: ΠÏοσθήκη σημείωσης + notice_successful_save: Επιτυχής αποθήκευση + notice_successful_add: Επιτυχής δημιουÏγία + notice_unsuccessful_save: ΠÏοβλήματα αποθήκευσης + notice_successful_merged: Επιτυχής συγχώνευση + + notice_merged_warning: Όλες οι σημειώσεις, έÏγα, ετικέτες και εÏγασίες που επισυνάπτονται σε αυτό το άτομο θα μετακινηθοÏν στην παÏακάτω επιλογή. Μετά, η επαφή θα διαγÏαφεί. + + project_module_contacts: Επαφές + + permission_view_contacts: ΠÏοβολή επαφών + permission_edit_contacts: ΔιόÏθωση επαφών + permission_delete_contacts: ΔιαγÏαφή επαφών + permission_view_deals: ΠÏοβολή συμφωνιών + permission_edit_deals: ΔιόÏθωση συμφωνιών + permission_delete_deals: ΔιαγÏαφή συμφωνιών + permission_add_notes: ΠÏοβολή σημειώσεων + permission_delete_notes: ΔιόÏθωση σημειώσεων + permission_delete_own_notes: ΔιαγÏαφή δικών μου σημειώσεων + + # 2.0.0 + label_crm_deal_category: ΚατηγοÏία συμφωνίας + label_crm_deal_category_plural: ΚατηγοÏίες συμφωνιών + label_crm_deal_category_new: Îέα κατηγοÏία + text_deal_category_destroy_assignments: ΑφαίÏεση αναθέσεων κατηγοÏίας + text_deal_category_destroy_question: "ΟÏισμένες συμφωνίες (%{count}) έχουν ανατεθεί σε αυτή την κατηγοÏία. Τι επιθυμείτε?" + text_deal_category_reassign_to: ΕπανεκχώÏηση συμφωνιών σε αυτή την κατηγοÏία + text_deals_destroy_confirmation: 'Είστε σίγουÏος/η οτι θέλετε να διαγÏαφεί η επιλεγμένη συμφωνία(ες);' + label_crm_deal_status_plural: ΚατηγοÏίες συμφωνίας + label_crm_deal_status: ΚατηγοÏία συμφωνίας + field_deal_status_is_closed: Κλειστή + label_crm_deal_status_new: Îέα + permission_manage_contacts: ΔιαχείÏιση επαφών + label_crm_sales_funnel: ΣυγκέντÏωση πωλήσεων + label_crm_period: ΠεÏίοδος + label_crm_count: ΑÏίθμηση + + #2.0.1 + label_crm_user_format: ΜοÏφή ονόματος επαφής + label_crm_my_contact_plural: Επαφές που έχουν ανατεθεί σε μένα + label_crm_my_deal_plural: Ανοικτές συμφωνίες που έχουν ανατεθεί σε μένα + label_crm_contact_view_all: ΠÏοβολή όλων των επαφών + label_crm_deal_view_all: ΠÏοβολή όλων των συμφωνιών + + #2.0.2 + label_crm_bulk_edit_selected_contacts: ΔιόÏθωση όλων των επιλεγμένων επαφών + label_crm_bulk_edit_selected_deals: ΔιόÏθωση όλων των επιλεγμένων συμφωνιών + label_crm_bulk_send_mail_selected_contacts: Send mail to selected επαφές + field_add_tags: ΠÏοσθήκη ετικετών + field_delete_tags: ΔιαγÏαφή ετικετών + label_crm_send_mail: Αποστολή mail + error_empty_email: Το email δε μποÏεί να είναι κενό + permission_send_contacts_mail: Αποστολή mail + field_mail_from: Από διεÏθυνση + text_email_macros: Διαθέσιμες μακÏοεντολές %{macro} + field_message: Μήνυμα + + #2.0.3 + label_crm_add_contact: ΠÏοσθήκη επαφής + label_contact: Επαφή + field_age: Ηλικία + label_crm_vcf_import: Εισαγωγή από vCard + label_crm_mail_from: Από + permission_import_contacts: Εισαγωγή επαφών + + #2.1.0 + field_company_name: Όνομα εταιÏείας + label_crm_recently_added_contacts: ΠÏόσφατα Ï€Ïοστιθέμενες επαφές + label_crm_created_by_me: Επαφές δημιουÏγημένες από εμένα + my_contacts: Οι επαφές μου + my_deals: Οι συμφωνίες μου + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Κλήση + label_crm_note_type_meeting: Συνάντηση + field_deal_currency: Îόμισμα + label_crm_my_contacts_stats: Στατιστικά επαφών μήνα + label_crm_contacts_created: ΔημιουÏγημένες επαφές + label_crm_deals_created: ΔημιουÏγημένες συμφωνίες + my_contacts_avatars: ΦωτογÏαφίες επαφών μου + my_contacts_stats: Στατιστικά επαφών + label_crm_add_into: ΠÏοσθήκη σε + label_crm_delete_from: ΔιαγÏαφή από + label_crm_show_deaks_tab: Εμφάνιση καÏτέλας συμφωνιών + label_crm_show_on_projects_show: Εμφάνιση επαφών σε επισκόπηση έÏγων + + #2.2.1 + label_crm_contacts_show_in_list: ΠÏοβολή σε λίστα + + #2.3.0 + label_crm_module_plural: Ενότητες + label_crm_list_partial_style: ΤÏπος λίστας + label_crm_list_excerpt: Λίστα αποσπασμάτων + label_crm_list_cards: ΚάÏτες + label_crm_list_list: Πίνακας + field_contacts: Επαφή + field_companies: ΕταιÏεία + label_crm_added_by: ΠÏοστέθηκε από + label_crm_contact_note_authoring_time: ΠÏοβολή ÏŽÏας σημείωσης + label_crm_contact_issues_filters: ΦίλτÏα θεμάτων + label_crm_csv_import: Εισαγωγή από CSV + label_crm_upload_encoding: Κωδικοποίηση αÏχείων + label_crm_csv_file: αÏχείο CSV + label_crm_csv_separator: ΔιαχωÏιστής + field_middle_name: Μεσαίο όνομα + field_job_title: Θέση + field_company: ΕταιÏεία + field_address: ΔιεÏθυνση + field_phone: Τηλέφωνο + field_email: Email + field_tags: Ετικέτες + field_last_note: Τελευταία σημείωση + field_is_company: Είναι εταιÏεία + field_contact_full_name: ΠλήÏες όνομα + button_contacts_edit_query: ΔιόÏθωση εÏωτήματος + button_contacts_delete_query: ΔιαγÏαφή εÏωτήματος + permission_manage_public_contacts_queries: ΔιαχείÏιση δημόσιων εÏωτημάτων + permission_add_deals: ΠÏοσθήκη συμφωνιών + permission_add_contacts: ΠÏοσθήκη επαφών + permission_save_contacts_queries: Αποθήκευση εÏωτημάτων + + #2.3.3 + label_crm_contact_show_in_app_menu: ΠÏοβολή καÏτελών στο Î¼ÎµÎ½Î¿Ï ÎµÏ†Î±Ïμογής + + #2.3.4 + label_crm_contact_show_closed_issues: ΠÏοβολή κλειστών θεμάτων + + #3.0.0 + label_crm_import: Εισαγωγή + label_contact_note_plural: Σημειώσεις επαφής + label_deal_note_plural: Σημειώσεις συμφωνίας + label_crm_contact_all_note_plural: Όλες οι σημειώσεις + error_unable_delete_deal_status: Δεν ήταν δυνατή η διαγÏαφή κατάστασης συμφωνίας + label_crm_contacts_hidden: ΚÏυφές Ïυθμίσεις + + #3.1.0 + label_crm_contact_added: Η επαφή Ï€Ïοστέθηκε + label_crm_note_added: Η σημείωση Ï€Ïοστέθηκε + label_crm_deal_added: Η συμφωνία Ï€Ïοστέθηκε + label_crm_deal_updated: Η συμφωνία ενημεÏώθηκε + text_crm_contact_added: "Η επαφή %{name} Ï€Ïοστέθηκε από %{author}." + text_crm_deal_added: "Η συμφωνία %{name} Ï€Ïοστέθηκε από %{author}." + text_crm_deal_status_changed: "Συμφωνία status changed from %{old} to %{new}" + text_crm_deal_updated: "Συμφωνία %{name} has been updated by %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Εισαγωγή από CSV + permission_import_deals: Εισαγωγή συμφωνιών + label_crm_single_quotes: "Μονά εισαγωγικά (')" + label_crm_double_quotes: "Διπλά εισαγωγικά (\")" + label_crm_quotes_type: ΤÏπος εισαγωγικών + label_crm_contacts_visibility: ΟÏατότητα + label_crm_contacts_visibility_project: Ανα δικαιώματα έÏγων + label_crm_contacts_visibility_public: Δημόσιο + label_crm_contacts_visibility_private: Ιδιωτικό + permission_view_private_contacts: ΠÏοβολή ιδιωτικών επαφών + text_crm_error_on_line: 'Σφάλμα στη γÏαμμή %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Πιθανότητα + label_crm_deal_status_type: ΤÏπος κατάστασης + label_crm_select_companies: ΦιλτÏάÏισμα εταιÏειών σε συμφωνίες + label_crm_expected_revenue: Αναμενόμενα έσοδα + label_crm_deal_due_date: ΗμεÏομηνία λήξης + + #3.2.2 + label_crm_show_deals_in_top_menu: ΠÏοβολή συμφωνιών στο πάνω Î¼ÎµÎ½Î¿Ï + label_crm_show_details: ΠÏοβολή λεπτομεÏειών + label_crm_has_deals: Έχει συμφωνίες + label_crm_has_open_issues: Ανοικτά θέματα + label_crm_note: Σημείωση + + #3.2.5 + notice_failed_to_save_contacts: "Αδυναμία αποθήκευσης %{count} συμφωνίας(ων) από %{total} επιλεγμένες: %{ids}." + + #3.2.6 + project_module_deals: Συμφωνίες + permission_manage_deals: ΔιαχείÏιση συμφωνιών + label_crm_deals_from_subprojects: ΠÏοβολή συμφωνιών από υποέÏγα + label_crm_megre_tags: Συγχώνευση ετικετών + label_crm_monochrome_tags: ΜονόχÏωμες ετικέτες + + #3.2.7 + label_crm_address: ΔιεÏθυνση + label_crm_street1: ΔÏόμος 1 + label_crm_street2: ΔÏόμος 2 + label_crm_city: Πόλη + label_crm_region: 'ΠεÏιοχή' + label_crm_postcode: 'ΤΚ' + label_crm_country: ΧώÏα + label_crm_countries: + AF: Αφγανιστάν + AL: Αλβανία + DZ: ΑλγεÏία + AS: ΑμεÏικανική Σαμόα + AD: ΑνδόÏα + AO: Ανγκόλα + AI: Ανγκουίλα + AQ: ΑνταÏκτική + AG: Αντίγκουα και ΜπαÏμποÏντα + AR: ΑÏγεντινή + AM: ΑÏμενία + AW: ΑÏοÏμπα + AU: ΑυστÏαλία + AT: ΑυστÏία + AZ: ΑζεÏμπαϊτζάν + BS: Μπαχάμες + BH: ΜπαχÏέιν + BD: Μπανγκλαντές + BB: ΜπαÏμπάντος + BY: ΛευκοÏωσία + BE: Βέλγιο + BZ: Μπελίζ + BJ: Μπένιν + BM: ΒεÏμοÏδες + BT: Μπουτάν + BO: Βολιβία + BA: Βοσνία - ΕÏζεγοβίνη + BW: Μποτσουάνα + BV: Îήσος Μπουβέ + BR: Î’Ïαζιλία + BQ: Î’Ïετανικά Έδάφη ΑνταÏκτικής + IO: Î’Ïετανικά Έδάφη Î™Î½Î´Î¹ÎºÎ¿Ï Î©ÎºÎµÎ±Î½Î¿Ï + VG: Î’Ïετανικές ΠαÏθένοι Îήσοι + BN: ΜπÏουνέι ÎταÏουσαλάμ + BG: ΒουλγαÏία + BF: ΜπουÏκίνα Φάσο + BI: ΜπουÏοÏντι + KH: Καμπότζη + CM: ΚαμεÏοÏν + CA: Καναδάς + CT: Canton and Enderbury Islands + CV: Îήσοι ΠÏάσινου ΑκÏωτηÏίου + KY: Îήσοι Κέιμαν + CF: ΚεντÏοαφÏικανική ΔημοκÏατία + TD: Τσαντ + CL: Χιλή + CN: Κίνα + CX: Îήσος ΧÏιστουγέννων + CC: Îήσοι Κόκος (Κήλινγκ) + CO: Κολομβία + KM: ΚομόÏες + CG: Κονγκό + CD: Κονγκό, Λαϊκή ΔημοκÏατία του + CK: Îήσοι Κουκ + CR: Κόστα Ρίκα + HR: ΚÏοατία + CU: ΚοÏβα + CY: ΚÏÏ€Ïος + CZ: Τσεχία + CI: Ακτή Ελεφαντόδοντος + DK: Δανία + DJ: Τζιμπουτί + DM: Îτομίνικα + DO: Δομινικανή ΔημοκÏατία + NQ: Dronning Maud Land + DD: Ανατολική ΓεÏμανία + EC: ΙσημεÏινός + EG: Αίγυπτος + SV: Ελ Î£Î±Î»Î²Î±Î´ÏŒÏ + GQ: ΙσημεÏινή Γουινέα + ER: ΕÏυθÏαία + EE: Εσθονία + ET: Αιθιοπία + FK: Îήσοι Φώκλαντ + FO: Îήσοι ΦεÏόες + FJ: Φίτζι + FI: Φινλανδία + FR: Γαλλία + GF: Γαλλική Γουιάνα + PF: Γαλλική Πολυνησία + TF: Γαλλικά Îότια Εδάφη + FQ: French Southern and Antarctic Territories + GA: Γκαμπόν + GM: Γκάμπια + GE: ΓεωÏγία + DE: ΓεÏμανία + GH: Γκάνα + GI: ΓιβÏÎ±Î»Ï„Î¬Ï + GR: Ελλάδα + GL: ΓÏοιλανδία + GD: ΓÏενάδα + GP: ΓουαδελοÏπη + GU: Γκουάμ + GT: Γουατεμάλα + GG: Guernsey + GN: Γουινέα + GW: Γουινέα-Μπισάου + GY: Γουιάνα + HT: Αϊτή + HM: Îήσοι ΧεÏντ και Μακντόναλντ + HN: ΟνδοÏÏα + HK: Χονγκ Κονγκ, Ειδική Διοικητική ΠεÏιφέÏεια της Κίνας + HU: ΟυγγαÏία + IS: Ισλανδία + IN: Ινδία + ID: Ινδονησία + IR: ΙÏάν, Ισλαμική ΔημοκÏατία του + IQ: ΙÏάκ + IE: ΙÏλανδία + IM: Isle of Man + IL: ΙσÏαήλ + IT: Ιταλία + JM: Τζαμάικα + JP: Ιαπωνία + JE: Jersey + JT: Johnston Island + JO: ΙοÏδανία + KZ: Καζακστάν + KE: Κένυα + KI: ΚιÏιμπάτι + KW: Κουβέιτ + KG: ΚιÏγιζία + LA: Λατινική ΑμεÏική + LV: Λετονία + LB: Λίβανος + LS: Λεσότο + LR: ΛιβεÏία + LY: ΛιβÏη + LI: Λιχτενστάιν + LT: Λιθουανία + LU: ΛουξεμβοÏÏγο + MO: Μακάο, Ειδική Διοικητική ΠεÏιφέÏεια της Κίνας + MK: ΠΓΔ Μακεδονίας + MG: ΜαδαγασκάÏη + MW: Μαλάουι + MY: Μαλαισία + MV: Μαλδίβες + ML: Μάλι + MT: Μάλτα + MH: Îήσοι ΜάÏσαλ + MQ: ΜαÏτινίκα + MR: ΜαυÏιτανία + MU: ΜαυÏίκιος + YT: Μαγιότ + FX: Metropolitan France + MX: Μεξικό + FM: ΜικÏονησία, Ομόσπονδες Πολιτείες της + MI: Midway Islands + MD: Μολδαβία, ΔημοκÏατία της + MC: Μονακό + MN: Μογγολία + ME: Montenegro + MS: ΜονσεÏάτ + MA: ΜαÏόκο + MZ: Μοζαμβίκη + MM: ÎœÎ¹Î±Î½Î¼Î¬Ï + NA: Îαμίμπια + NR: ÎαοÏÏου + NP: Îεπάλ + NL: Ολλανδία + AN: Ολλανδικές Αντίλλες + NT: Neutral Zone + NC: Îέα Καληδονία + NZ: Îέα Ζηλανδία + NI: ÎικαÏάγουα + NE: ÎÎ¯Î³Î·Ï + NG: ÎιγηÏία + NU: ÎιοÏε + NF: Îήσος ÎÏŒÏφολκ + KP: ΚοÏέα, Î’ÏŒÏεια + VD: North Vietnam + MP: Îήσοι Î’ÏŒÏειες ΜαÏιάνες + NO: ÎοÏβηγία + OM: Ομάν + PC: Pacific Islands Trust Territory + PK: Πακιστάν + PW: Παλάου + PS: Παλαιστινιακά Εδάφη + PA: Παναμάς + PZ: Panama Canal Zone + PG: ΠαποÏα - Îέα Γουινέα + PY: ΠαÏαγουάη + YD: People's Democratic Republic of Yemen + PE: ΠεÏÎ¿Ï + PH: Φιλιππίνες + PN: ΠίτκεÏν + PL: Πολωνία + PT: ΠοÏτογαλία + PR: ΠουέÏτο Ρίκο + QA: ÎšÎ±Ï„Î¬Ï + RO: Ρουμανία + RU: Ρωσία + RW: Ρουάντα + RE: Ρεϋνιόν + BL: Saint Barthelemy + SH: Αγία Ελένη + KN: Σαιντ Κιτς και Îέβις + LC: Αγία Λουκία + MF: Saint Martin + PM: Σαιντ Î Î¹Î­Ï ÎºÎ±Î¹ Μικελόν + VC: Άγιος Βικέντιος και ΓÏεναδίνες + WS: Σαμόα + SM: Άγιος ΜαÏίνος + SA: Σαουδική ΑÏαβία + SN: Σενεγάλη + RS: Serbia + CS: ΣεÏβία και ΜαυÏοβοÏνιο + SC: Σεϋχέλλες + SL: ΣιέÏα Λεόνε + SG: ΣιγκαποÏÏη + SK: Σλοβακία + SI: Σλοβενία + SB: Îήσοι Σολομώντος + SO: Σομαλία + ZA: Îότια ΑφÏική + GS: Îότια ΓεωÏγία και Îήσοι Îότιες Σάντουιτς + KR: ΚοÏέα, Îότια + ES: Ισπανία + LK: ΣÏι Λάνκα + SD: Σουδάν + SR: ΣουÏινάμ + SJ: Îήσοι Î£Î²Î¬Î»Î¼Ï€Î±Ï ÎºÎ±Î¹ Γιαν Μαγιέν + SZ: Σουαζιλάνδη + SE: Σουηδία + CH: Ελβετία + SY: ΣυÏία, ΑÏαβική ΔημοκÏατία της + ST: Σάο Τομέ και ΠÏίνσιπε + TW: Ταϊβάν + TJ: Τατζικιστάν + TZ: Τανζανία + TH: Ταϊλάνδη + TL: Ανατολικό Î¤Î¹Î¼ÏŒÏ + TG: Τόγκο + TK: Τοκελάου + TO: Τόνγκα + TT: ΤÏινιδάδ και Τομπάγκο + TN: Τυνησία + TR: ΤουÏκία + TM: ΤουÏκμενιστάν + TC: Îήσοι ΤεÏκς και Κάικος + TV: Î¤Î¿Ï…Î²Î±Î»Î¿Ï + UM: ΑπομακÏυσμένες Îησίδες των Ηνωμένων Πολιτειών + PU: U.S. Miscellaneous Pacific Islands + VI: ΑμεÏικανικές ΠαÏθένοι Îήσοι + UG: Ουγκάντα + UA: ΟυκÏανία + SU: Union of Soviet Socialist Republics + AE: Ηνωμένα ΑÏαβικά ΕμιÏάτα + GB: Ηνωμένο Βασίλειο + US: Ηνωμένες Πολιτείες + ZZ: Unknown or Invalid Region + UY: ΟυÏουγουάη + UZ: Ουζμπεκιστάν + VU: Βανουάτου + VA: Αγία ΈδÏα (Βατικανό) + VE: Βενεζουέλα + VN: Βιετνάμ + WK: Wake Island + WF: Îήσοι Ουαλλίς και Φουτουνά + EH: Δυτική ΣαχάÏα + YE: Υεμένη + ZM: Ζάμπια + ZW: Ζιμπάμπουε + AX: Îήσοι Aland + label_crm_cross_project_contacts: Îα επιτÏέπονται επαφές ανάμεσα σε έÏγα + label_crm_list_board: Πίνακας + label_crm_default_list_style: ΠÏοεπιλεγμένο στυλ λίστας + label_crm_show_in_top_menu: ΠÏοβολή στο πάνω Î¼ÎµÎ½Î¿Ï + label_crm_show_in_app_menu: ΠÏοβολή στο Î¼ÎµÎ½Î¿Ï ÎµÏ†Î±Ïμογής + label_crm_money_settings: ΧÏήματα + label_crm_disable_taxes: ΑπενεÏγοποίηση φόÏου + label_crm_default_tax: ΠÏοεπιλεγμένη τιμή φόÏου + label_crm_tax_type: Είδος φόÏου + label_crm_tax_type_inclusive: Με φόÏο + label_crm_tax_type_exclusive: ΧωÏίς φόÏο + label_crm_default_currency: ΠÏοεπιλεγμένο νόμισμα + label_crm_thousands_delimiter: ΟÏιοθέτηση χιλιάδων + label_crm_decimal_separator: ΔιαχωÏιστής δεκαδικών ψηφίων + + #3.2.10 + label_crm_add_contact_plural: ΠÏοσθήκη επαφών + label_crm_search_for_contact: Αναζήτηση επαφής + label_crm_major_currencies: ΚυÏιότεÏα νομίσματα + + #3.2.11 + label_crm_post_address_format: ΜοÏφή διεÏθυνσης αποστολής + label_crm_post_address_format_macros: "ΜακÏοεντολές μοÏφής διεÏθυνσης: %{macros}" + + #3.2.14 + label_crm_last_year: πέÏυσι + + #3.2.15 + permission_export_contacts: Εξαγωγή επαφών και συμφωνιών \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/en.yml b/plugins/redmine_contacts/config/locales/en.yml new file mode 100755 index 0000000..98e7b2a --- /dev/null +++ b/plugins/redmine_contacts/config/locales/en.yml @@ -0,0 +1,622 @@ +en: + contacts_title: Contacts + + label_crm_recently_viewed: Recently viewed + label_crm_gravatar_enabled: Use Gravatar + label_crm_thumbnails_enabled: Show image thumbnails in notes + label_crm_max_thumbnail_file_size: Max thumbnailed image size + label_crm_view_all_contacts: View all contacts + label_crm_background_info: Background info + label_crm_company: Company + label_contact_plural: Contacts + label_crm_contact_edit_information: Editing Contact Information + label_crm_edit_tags: Edit tags + label_crm_contact_view: View + label_crm_contact_list: List + label_crm_contact_new: New contact + label_crm_at_company: at + label_crm_last_notes: Latest notes + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Select multilpe tags + label_crm_single_tag_mode: Single tag + label_crm_multiple_tags_mode: Multiple tags + label_crm_contact_tag: Tag + label_crm_time_ago: ago + label_crm_add_note_plural: Add note to + label_crm_note_plural: Notes + + label_crm_add_tags_rule: devide by commas + label_crm_contact_search: Search by name + label_crm_note_for: Note for + label_crm_show_on_map: Show on map + label_crm_add_another_phone: add phone + label_crm_remove: delete + label_crm_related_contacts: Related contacts + label_crm_assigned_to: Responsible + label_crm_issue_added: Issue added + label_crm_add_emails_rule: divide by commas + label_crm_add_phones_rule: divide by commas + label_crm_add_employee: New employee + label_crm_merge_duplicate_plural: Merge + label_crm_duplicate_plural: Possible duplicates + label_crm_duplicate_for_plural: Possible duplicates for + label_crm_add_tag: + add tag + + label_crm_note_show_extras: Advanced (type, date, files) + label_crm_note_hide_extras: Hide advanced + label_crm_note_added: Note added + label_crm_note_read_more: (read more) + label_crm_invoice_import: Import invoices from CSV + + label_deal_plural: Deals + label_crm_contractor_plural: Contacts + label_deal: Deal + label_crm_deal_new: New deal + label_crm_deal_edit_information: Edit deal information + label_crm_deal_change_status: Change status + label_crm_statistics: Statistics + label_crm_deals_import: Import deals from CSV + + label_crm_deal_status_new: New + label_crm_deal_status_first_contact: First contact + label_crm_deal_status_negotiations: Negotiations + label_crm_deal_status_pending: Pending + label_crm_deal_status_won: Won + label_crm_deal_status_lost: Lost + + label_crm_created_on: Created on + + field_note_date: Note date + field_background: Background + field_currency: Currency + field_contact: Contact + + field_deal_name: Name + field_deal_background: Background + field_deal_contact: Contact + field_deal_price: Sum + field_price: Sum + + field_contact_avatar: Avatar + field_contact_is_company: Company + field_contact_name: Name + field_contact_last_name: Last Name + field_contact_first_name: First Name + field_contact_middle_name: Middle Name + field_contact_job_title: Job title + field_contact_company: Company + field_contact_address: Address + field_contact_phone: Phone + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Background + field_contact_tag_names: Tags + field_first_name: First name + field_last_name: Last name + field_company: Company + field_birthday: Birthday + field_contact_department: Department + + + field_company_field: Industry + + field_color: Color + + button_add_note: Add note + notice_successful_save: Saved successfully + notice_successful_add: Successfully created + notice_unsuccessful_save: Save problems + notice_successful_merged: Successfully merged + + notice_merged_warning: All the notes, projects, tags and tasks attached to this person will be moved to choosed below. The contact will then be deleted. + + project_module_contacts: Contacts + + permission_view_contacts: View contacts + permission_edit_contacts: Edit contacts + permission_delete_contacts: Delete contacts + permission_view_deals: View deals + permission_edit_deals: Edit deals + permission_delete_deals: Delete deals + permission_add_notes: Add notes + permission_delete_notes: Delete notes + permission_delete_own_notes: Delete own notes + + # 2.0.0 + label_crm_deal_category: Deal category + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: New category + text_deal_category_destroy_assignments: Remove category assignments + text_deal_category_destroy_question: "Some deals (%{count}) are assigned to this category. What do you want to do?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'Are you sure you want to delete the selected deal(s)?' + label_crm_deal_status_plural: Deal statuses + label_crm_deal_status: Deal status + field_deal_status_is_closed: Closed + label_crm_deal_status_new: New + permission_manage_contacts: Manage contacts + label_crm_sales_funnel: Sales funnel + label_crm_period: Period + label_crm_count: Count + + #2.0.1 + label_crm_user_format: Contact name format + label_crm_my_contact_plural: Contacts assigned to me + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: View all contacts + label_crm_deal_view_all: View all deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edit all selected contacts + label_crm_bulk_edit_selected_deals: Edit all selected deals + label_crm_bulk_send_mail_selected_contacts: Send mail to selected contacts + field_add_tags: Add tags + field_delete_tags: Delete tags + label_crm_send_mail: Send mail + error_empty_email: Email can not be blank + permission_send_contacts_mail: Send mail + field_mail_from: From address + text_email_macros: Avaliable macros %{macro} + field_message: Message + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Contact + field_age: Age + label_crm_vcf_import: Import from vCard + label_crm_mail_from: From + permission_import_contacts: Import contacts + + #2.1.0 + field_company_name: Company name + label_crm_recently_added_contacts: Recently added contacts + label_crm_created_by_me: Contacts created by me + my_contacts: My contacts + my_deals: My deals + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Call + label_crm_note_type_meeting: Meeting + field_deal_currency: Currency + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contacts created + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Add into + label_crm_delete_from: Delete from + label_crm_show_deaks_tab: Show deals tab + label_crm_show_on_projects_show: Show contacts on projects overview + + #2.2.1 + label_crm_contacts_show_in_list: Show in list + + #2.3.0 + label_crm_module_plural: Modules + label_crm_list_partial_style: List style + label_crm_list_excerpt: Excerpt list + label_crm_list_cards: Cards + label_crm_list_list: Table + field_contacts: Contact + field_companies: Company + label_crm_added_by: Added by + label_crm_contact_note_authoring_time: Show note time + label_crm_contact_issues_filters: Issues filters + label_crm_csv_import: Import from CSV + label_crm_upload_encoding: File encoding + label_crm_csv_file: CSV file + label_crm_csv_separator: Separator + field_middle_name: Middle Name + field_job_title: Job title + field_company: Company + field_address: Address + field_phone: Phone + field_email: Email + field_tags: Tags + field_last_note: Last note + field_is_company: Is company + field_contact_full_name: Full name + button_contacts_edit_query: Edit query + button_contacts_delete_query: Delete query + permission_manage_public_contacts_queries: Manage public queries + permission_add_deals: Add deals + permission_add_contacts: Add contacts + permission_save_contacts_queries: Save queries + + #2.3.3 + label_crm_contact_show_in_app_menu: Show tabs in app menu + + #2.3.4 + label_crm_contact_show_closed_issues: Show closed issues + + #3.0.0 + label_crm_import: Import + label_contact_note_plural: Contact notes + label_deal_note_plural: Deal notes + label_crm_contact_all_note_plural: All notes + error_unable_delete_deal_status: Unable to delete deal status + label_crm_contacts_hidden: Hidden settings + + #3.1.0 + label_crm_contact_added: Contact added + label_crm_note_added: Note added + label_crm_deal_added: Deal added + label_crm_deal_updated: Deal updated + text_crm_contact_added: "Contact %{name} has been added by %{author}." + text_crm_deal_added: "Deal %{name} has been added by %{author}." + text_crm_deal_status_changed: "Deal status changed from %{old} to %{new}" + text_crm_deal_updated: "Deal %{name} has been updated by %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Import from CSV + permission_import_deals: Import deals + label_crm_single_quotes: "Single quotes (')" + label_crm_double_quotes: "Double quotes (\")" + label_crm_quotes_type: Quotation marks type + label_crm_contacts_visibility: Visibility + label_crm_contacts_visibility_project: By projects permissions + label_crm_contacts_visibility_public: Public + label_crm_contacts_visibility_private: Private + permission_view_private_contacts: View private contacts + text_crm_error_on_line: 'Error on line %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Probability + label_crm_deal_status_type: Status type + label_crm_select_companies: Filter companies in deal + label_crm_expected_revenue: Expected revenue + label_crm_deal_due_date: Due date + + #3.2.2 + label_crm_show_deals_in_top_menu: Show deals in top menu + label_crm_show_details: Show details + label_crm_has_deals: Has deals + label_crm_has_open_issues: Open issues + label_crm_note: Note + + #3.2.5 + notice_failed_to_save_contacts: "Failed to save %{count} contact(s) on %{total} selected: %{ids}." + + #3.2.6 + project_module_deals: Deals + permission_manage_deals: Manage deals + label_crm_deals_from_subprojects: Show deals from subprojects + label_crm_megre_tags: Merge tags + label_crm_monochrome_tags: Monochrome tags + + #3.2.7 + label_crm_address: Address + label_crm_street1: Street 1 + label_crm_street2: Street 2 + label_crm_city: City + label_crm_region: 'State' + label_crm_postcode: 'ZIP' + label_crm_country: Country + label_crm_country_code: Country code + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: Cuba + CY: Cyprus + CZ: Czech Republic + CI: Côte d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong SAR China + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: Mexico + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: Poland + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: Russia + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + KR: South Korea + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syria + ST: São Tomé and Príncipe + TW: Taiwan + TJ: Tajikistan + TZ: Tanzania + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + AE: United Arab Emirates + GB: United Kingdom + US: United States + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: Vietnam + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land Islands + label_crm_cross_project_contacts: Allow cross-project contacts relations + label_crm_list_board: Board + label_crm_default_list_style: Default list style + label_crm_show_in_top_menu: Show in top menu + label_crm_show_in_app_menu: Show in app menu + label_crm_money_settings: Money + label_crm_disable_taxes: Disable taxes + label_crm_default_tax: Default tax value + label_crm_tax_type: Tax type + label_crm_tax_type_inclusive: Tax inclusive + label_crm_tax_type_exclusive: Tax exclusive + label_crm_default_currency: Default currency + label_crm_thousands_delimiter: Thousands delimiter + label_crm_decimal_separator: Decimal separator + + #3.2.10 + label_crm_add_contact_plural: Add contacts + label_crm_search_for_contact: Search for contact + label_crm_major_currencies: Major currencies + + #3.2.11 + label_crm_post_address_format: Post address format + label_crm_post_address_format_macros: "Address format macros: %{macros}" + + #3.2.14 + label_crm_last_year: last year + + #3.2.15 + permission_export_contacts: Export contacts and deals + + #3.4.0 + label_crm_deal_contact: Deal's contact + + #3.4.1 + label_crm_default_country: Default country + label_attribute_of_contact: "Contact's %{name}" + label_crm_contact_country: Contact's country + label_crm_contact_city: Contact's city + label_attribute_of_deals: "Deals's %{name}" + + #3.4.2 + permission_manage_public_deals_queries: Manage deal public queries + permission_save_deals_queries: Save deal queries + permission_manage_contact_issue_relations: Manage issue relations + + #3.4.4 + label_crm_pipeline: Pipeline + text_crm_no_deal_statuses_in_project: No deal statuses in project + + #3.4.6 + label_crm_contact_email: Contact's email + label_crm_light_free_version: CRM Light free version + label_crm_link_to_pro: Upgrade to PRO + label_crm_link_to_pro_demo: PRO version live demo + label_crm_link_to_more_plugins: Find more RedmineCRM plugins + + + text_crm_string_incorrect_format: have incorrect format + + label_deal_items: Deal items diff --git a/plugins/redmine_contacts/config/locales/es.yml b/plugins/redmine_contacts/config/locales/es.yml new file mode 100644 index 0000000..5a5594f --- /dev/null +++ b/plugins/redmine_contacts/config/locales/es.yml @@ -0,0 +1,614 @@ +# encoding: utf-8 +es: + contacts_title: Contactos + + label_crm_recently_viewed: Vistos recientemente + label_crm_gravatar_enabled: Usar Gravatar + label_crm_thumbnails_enabled: Mostrar miniaturas en las notas + label_crm_max_thumbnail_file_size: Máximo tamaño de la miniatura + label_crm_view_all_contacts: Ver todos los contactos + label_crm_background_info: Trasfondo + label_crm_company: Compañía + label_contact_plural: Contactos + label_crm_contact_edit_information: Editar la información de contacto + label_crm_edit_tags: Cambiar etiquetas + label_crm_contact_view: Ver + label_crm_contact_list: Lista + label_crm_contact_new: Nuevo + label_crm_at_company: en + label_crm_last_notes: Últimas notas + label_crm_tags_plural: Etiquetas + label_crm_multi_tags_plural: Seleccionar varias etiquetas + label_crm_single_tag_mode: Etiqueta + label_crm_multiple_tags_mode: Etiquetas + label_crm_contact_tag: Etiqueta + label_crm_time_ago: antes + label_crm_add_note_plural: Añadir nota a + label_crm_note_plural: Notas + + label_crm_add_tags_rule: separadas por comas + label_crm_contact_search: Buscar por nombre + label_crm_note_for: Nota para + label_crm_show_on_map: Mostrar en mapa + label_crm_add_another_phone: añadir teléfono + label_crm_remove: borrar + label_crm_related_contacts: Contactos relacionados + label_crm_assigned_to: Responsable + label_crm_issue_added: asunto añadido + label_crm_add_emails_rule: separados por comas + label_crm_add_phones_rule: separados por comas + label_crm_add_employee: Nuevo empleado + label_crm_merge_duplicate_plural: Unir + label_crm_duplicate_plural: Posible duplicado + label_crm_duplicate_for_plural: Posible duplicado de + label_crm_add_tag: + añadir etiqueta + + label_crm_note_show_extras: Avanzado (ficheros, fechas) + label_crm_note_hide_extras: Esconder avanzado + label_crm_note_added: Se ha añadido la nota correctamente + label_crm_note_read_more: (leer más) + label_crm_invoice_import: Importar facturas desde CSV + + label_deal_plural: Acuerdos + label_crm_contractor_plural: Contratistas + label_deal: Acuerdo + label_crm_deal_new: Nuevo acuerdo + label_crm_deal_edit_information: Cambiar información del acuerdo + label_crm_deal_change_status: Cambiar estado + label_crm_statistics: Estadísticas + label_crm_deals_import: Importar acuerdos desde CSV + + label_crm_deal_status_new: Nuevo + label_crm_deal_status_first_contact: Primer contacto + label_crm_deal_status_negotiations: Negociaciones + label_crm_deal_status_pending: Pendiente + label_crm_deal_status_won: Ganado + label_crm_deal_status_lost: Perdido + + label_crm_deal_pending: Pendiente + label_crm_deal_won: Ganado + label_crm_deal_lost: Perdido + + label_crm_created_on: Creado + field_note_date: Fecha de la nota + field_background: Trasfondo + field_currency: Moneda + field_contact: Contacto + + field_deal_name: Nombre + field_deal_background: Trasfondo + field_deal_contact: Contacto + field_deal_price: Importe + field_price: Importe + + field_contact_avatar: Avatar + field_contact_is_company: Compañía + field_contact_name: Nombre + field_contact_last_name: Apellido + field_contact_first_name: Nombre + field_contact_middle_name: Segundo nombre + field_contact_job_title: Cargo + field_contact_company: Compañía + field_contact_address: Dirección + field_contact_phone: Teléfono + field_contact_email: Email + field_contact_website: Sitio web + field_contact_skype: Skype + field_contact_status: Estado + field_contact_background: Trasfondo + field_contact_tag_names: Etiquetas + field_first_name: Nombre + field_last_name: Apellido + field_company: Compañía + field_birthday: Cumpleaños + field_contact_department: Departamento + + field_company_field: Industria + + field_color: Color + + button_add_note: Añadir nota + notice_successful_save: Guardado correctamente + notice_successful_add: Creado correctamente + notice_unsuccessful_save: Problemas al guardar + notice_successful_merged: Unido correctamente + + notice_merged_warning: Todas las notas, proyectos, etiquetas y tareas asociadas a esta persona se moverán a la persona seleccionada. El contacto será borrado. + + project_module_contacts: Contactos + + permission_view_contacts: Ver contactos + permission_edit_contacts: Editar contactos + permission_delete_contacts: Borrar contactos + permission_view_deals: Ver acuerdos + permission_edit_deals: Editar acuerdos + permission_delete_deals: Borrar acuerdos + permission_add_notes: Añadir notas + permission_delete_notes: Borrar notas + permission_delete_own_notes: Borrar notas propias + + # 2.0.0 + label_crm_deal_category: Categoría de acuerdo + label_crm_deal_category_plural: Categorías de acuerdos + label_crm_deal_category_new: Nueva categoría + text_deal_category_destroy_assignments: Eliminar asignación a categoría + text_deal_category_destroy_question: "Algunos acuerdos (%{count}) están asignados a esta categoría. ¿Qué desea hacer?" + text_deal_category_reassign_to: Reasignar acuerdo a categoría + text_deals_destroy_confirmation: '¿Está seguro de que quiere eliminar el/los acuerdo/s seleccionado/s??' + label_crm_deal_status_plural: Estados de los acuerdos + label_crm_deal_status: Estado del acuerdo + field_deal_status_is_closed: Cerrado + label_crm_deal_status_new: Nuevo + permission_manage_contacts: Listas de valores + label_crm_sales_funnel: Funnel de ventas + label_crm_period: Periodo + label_crm_count: Importe + + #2.0.1 + label_crm_user_format: Formato del nombre del contacto + label_crm_my_contact_plural: Contactos asignados a mí + label_crm_my_deal_plural: Acuerdos en marcha asignados a mí + label_crm_contact_view_all: Ver todos los contactos + label_crm_deal_view_all: Ver todos los acuerdos + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Editar todos los contactos seleccionados + label_crm_bulk_edit_selected_deals: Editar todos los acuerdos seleccionados + label_crm_bulk_send_mail_selected_contacts: Enviar mail a los contactos seleccionados + field_add_tags: Añadir etiquetas + field_delete_tags: Eliminar etiquetas + label_crm_send_mail: Enviar mail + error_empty_email: El Email no puede estar vacío + permission_send_contacts_mail: Enviar mail + field_mail_from: Dirección "De" + text_email_macros: Macros disponibles %{macro} + field_message: Mensaje + + #2.0.3 + label_crm_add_contact: Añadir contacto + label_contact: Contacto + field_age: Edad + label_crm_vcf_import: Importar desde vCard + label_crm_mail_from: De + permission_import_contacts: Importar contactos + + #2.1.0 + field_company_name: Nombre de la compañía + label_crm_recently_added_contacts: Contactos añadidos recientemente + label_crm_created_by_me: Contactos creados por mí + my_contacts: Mis contactos + my_deals: Mis acuerdos + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Llamada + label_crm_note_type_meeting: Reunión + field_deal_currency: Moneda + label_crm_my_contacts_stats: Estadísticas de mis contactos + label_crm_contacts_created: Contactos creados + label_crm_deals_created: Acuerdos creados + my_contacts_avatars: Fotos de mis contactos + my_contacts_stats: Estadísticas de mis contactos + label_crm_add_into: Añadir información + label_crm_delete_from: Eliminar de + label_crm_show_deals_tab: Mostrar pestaña de Acuerdos + label_crm_show_on_projects_show: Mostrar contactos en el Vistazo del proyecto + + #2.2.1 + label_crm_contacts_show_in_list: Mostrar en listado + + #2.3.0 + label_crm_module_plural: Módulos + label_crm_contacts_default_currency: Moneda del acuerdo por defecto + label_crm_list_partial_style: Estilo del listado de contactos + label_crm_list_excerpt: Extractos + label_crm_list_cards: Tarjetas + label_crm_list_list: Tabla + field_contacts: Contacto + field_companies: Compañía + label_crm_added_by: Añadido por + label_crm_contact_note_authoring_time: Mostrar hora de la nota + label_crm_contact_issues_filters: Filtros de tareas + label_crm_csv_import: Importar desde CSV + label_crm_upload_encoding: Codificación del fichero + label_crm_csv_file: fichero CSV + label_crm_csv_separator: Separador + field_middle_name: Segundo nombre + field_job_title: Puesto + field_company: Compañía + field_address: Dirección + field_phone: Teléfono + field_email: Email + field_tags: Etiquetas + field_last_note: Última nota + field_is_company: Es compañía + field_contact_full_name: Nombre completo + button_contacts_edit_query: Editar filtro + button_contacts_delete_query: Eliminar filtro + permission_manage_public_contacts_queries: Gestionar filtros públicos + permission_add_deals: Añadir acuerdos + permission_add_contacts: Añadir contactos + permission_save_contacts_queries: Guardar filtros + + #2.3.3 + label_crm_contact_show_in_app_menu: Mostrar pestaña Contactos en el menú secundario + + #2.3.4 + label_crm_contact_show_closed_issues: Mostrar tareas cerradas + + #3.0.0 + label_crm_import: Importar + label_contact_note_plural: Notas de contactos + label_deal_note_plural: Notas de acuerdos + label_crm_contact_all_note_plural: Todas las notas + error_unable_delete_deal_status: No es posible borrar el estado del acuerdo + label_crm_contacts_hidden: Ajustes ocultos + + #3.1.0 + label_crm_contact_added: Contacto añadido + label_crm_note_added: Nota añadida + label_crm_deal_added: Acuerdo añadido + label_crm_deal_updated: Acuerdo actualizado + text_crm_contact_added: "El contacto %{name} ha sido añadido por %{author}." + text_crm_deal_added: "El acuerdo %{name} ha sido añadido por %{author}." + text_crm_deal_status_changed: "El estado del acuerdo ha sido cambiado de %{old} a %{new}" + text_crm_deal_updated: "El acuerdo %{name} ha sido actualizado por %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Cco + label_crm_contact_import: Importar desde CSV + permission_import_deals: Importar acuerdos + label_crm_single_quotes: "Comillas simples (')" + label_crm_double_quotes: "Comillas dobles (\")" + label_crm_quotes_type: Tipo de separadores + label_crm_contacts_visibility: Visibilidad + label_crm_contacts_visibility_project: Según permisos de los proyectos + label_crm_contacts_visibility_public: Público + label_crm_contacts_visibility_private: Privado + permission_view_private_contacts: Ver contactos privados + text_crm_error_on_line: 'Error en la línea %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Probabilidad + label_crm_deal_status_type: Tipo de estado + label_crm_select_companies: Filtrar compañías en el acuerdo + label_crm_expected_revenue: Ingresos esperados + label_crm_deal_due_date: Vencimiento + + #3.2.2 + label_crm_show_deals_in_top_menu: Mostrar acuerdos en el menú superior + label_crm_show_details: Mostrar detalles + label_crm_has_deals: Tiene acuerdos + label_crm_has_open_issues: Con tareas abiertas + label_crm_note: Nota + + #3.2.5 + notice_failed_to_save_contacts: "No se pudieron guardar %{count} contacto/s de %{total} seleccionado: %{ids}." + + #3.2.6 + project_module_deals: Acuerdos + permission_manage_deals: Gestionar acuerdos + label_crm_deals_from_subprojects: Mostrar acuerdos de subproyectos + label_crm_megre_tags: Unir etiquetas + label_crm_monochrome_tags: Etiquetas monocolores + + #3.2.7 + label_crm_address: Dirección + label_crm_street1: Calle (1) + label_crm_street2: Calle (2) + label_crm_city: Ciudad + label_crm_region: Provincia/Estado + label_crm_postcode: Código postal + label_crm_country: País + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: Cuba + CY: Cyprus + CZ: Czech Republic + CI: Côte d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong SAR China + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: Mexico + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: Poland + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: Russia + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + KR: South Korea + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syria + ST: São Tomé and Príncipe + TW: Taiwan + TJ: Tajikistan + TZ: Tanzania + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + SU: Union of Soviet Socialist Republics + AE: United Arab Emirates + GB: United Kingdom + US: United States + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: Vietnam + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land Islands + label_crm_cross_project_contacts: Permitir relacionar contactos de otros proyectos + label_crm_list_board: Panel + label_crm_default_list_style: Tipo de listado por defecto + label_crm_show_in_top_menu: Mostrar en el menú superior + label_crm_show_in_app_menu: Mostrar en el menú secundario + label_crm_money_settings: Moneda + label_crm_disable_taxes: Deshabilitar impuestos + label_crm_default_tax: Tipo impositivo por defecto + label_crm_tax_type: Tipo de impuesto + label_crm_tax_type_inclusive: Impuestos incluidos + label_crm_tax_type_exclusive: Impuestos no incluidos + label_crm_default_currency: Moneda por defecto + label_crm_thousands_delimiter: Separador de miles + label_crm_decimal_separator: Separador de decimales + + #3.2.10 + label_crm_add_contact_plural: Añadir contactos + label_crm_search_for_contact: Buscar contacto + label_crm_major_currencies: Monedas principales + + #3.2.11 + label_crm_post_address_format: Formato de la dirección postal + label_crm_post_address_format_macros: "Macros del formato de la dirección: %{macros}" + + #3.2.14 + label_crm_last_year: Último año + + #3.2.15 + permission_export_contacts: Exportar contactos y acuerdos + + #3.4.0 + label_crm_deal_contact: Contacto del acuerdo + + #3.4.1 + label_crm_default_country: País por defecto + label_attribute_of_contact: "%{name} del contacto" + label_crm_contact_country: País del contacto + label_crm_contact_city: Ciudad del contacto + label_attribute_of_deals: "%{name} del acuerdo" + + #3.4.2 + permission_manage_public_deals_queries: Gestionar filtros públicos de los acuerdos + permission_save_deals_queries: Guardar filtros de los acuerdos + permission_manage_contact_issue_relations: Gestionar relaciones de las tareas de los contactos + + #3.4.4 + label_crm_pipeline: Pipeline + text_crm_no_deal_statuses_in_project: No hay estados de los acuerdos en el proyecto \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/fr.yml b/plugins/redmine_contacts/config/locales/fr.yml new file mode 100644 index 0000000..c77083e --- /dev/null +++ b/plugins/redmine_contacts/config/locales/fr.yml @@ -0,0 +1,616 @@ +fr: + contacts_title: Contacts + + label_crm_recently_viewed: Vus dernièrement + label_crm_gravatar_enabled: Utiliser Gravatar + label_crm_thumbnails_enabled: Afficher les vignettes dans les notes + label_crm_max_thumbnail_file_size: Taille max des images de vignettes + label_crm_view_all_contacts: Voir tous les contacts + label_crm_background_info: Informations / contexte + label_crm_company: Société + label_contact_plural: Liste des contacts + label_crm_contact_edit_information: Édition des informations du contact + label_crm_edit_tags: Éditer les étiquettes + label_crm_contact_view: Voir + label_crm_contact_list: Lister + label_crm_contact_new: Nouveau contact + label_crm_at_company: à + label_crm_last_notes: Dernières notes + label_crm_tags_plural: Étiquettes + label_crm_multi_tags_plural: Sélectionner plusieurs étiquettes + label_crm_single_tag_mode: Étiquette unique + label_crm_multiple_tags_mode: Étiquettes multiples + label_crm_contact_tag: Étiquette + label_crm_time_ago: il y à + label_crm_add_note_plural: Ajouter une note à + label_crm_note_plural: Notes + + label_crm_add_tags_rule: séparés par des virgule + label_crm_contact_search: Rechercher par nom + label_crm_note_for: Note pour + label_crm_show_on_map: Afficher sur la carte + label_crm_add_another_phone: ajouter numéro de téléphone + label_crm_remove: supprimer + label_crm_related_contacts: Contacts associés + label_crm_assigned_to: Responsable + label_crm_issue_added: Demande ajoutée + label_crm_add_emails_rule: séparé par des virgules + label_crm_add_phones_rule: séparé par des virgules + label_crm_add_employee: Nouveau membre + label_crm_merge_duplicate_plural: Fusionner + label_crm_duplicate_plural: Doublons possibles + label_crm_duplicate_for_plural: Doublons possibles pour + label_crm_add_tag: + ajouter étiquette + + label_crm_note_show_extras: Avancés (type, données, fichiers) + label_crm_note_hide_extras: Masquer avancés + label_crm_note_added: La note a été ajoutée + label_crm_note_read_more: (tout afficher) + label_crm_invoice_import: Importer les factures en CSV + + label_deal_plural: Affaires + label_crm_contractor_plural: Liste des contacts + label_deal: Affaire + label_crm_deal_new: Nouvelle affaire + label_crm_deal_edit_information: Éditer affaire + label_crm_deal_change_status: Changer de statut + label_crm_statistics: Statistiques + label_crm_deals_import: Importer des affaires en CSV + + label_crm_deal_status_new: Nouvelle + label_crm_deal_status_first_contact: Premier contact + label_crm_deal_status_negotiations: Négociations + label_crm_deal_status_pending: En cours + label_crm_deal_status_won: Gagné + label_crm_deal_status_lost: Perdu + + label_crm_created_on: Créée le + + field_note_date: Date de la note + field_background: Contexte + field_currency: Unité monétaire + field_contact: Contact + + field_deal_name: Nom + field_deal_background: Contexte + field_deal_contact: Contact + field_deal_price: Montant + field_price: Montant + + field_contact_avatar: Photo + field_contact_is_company: Société + field_contact_name: Nom + field_contact_last_name: Nom de famille + field_contact_first_name: Prénom + field_contact_middle_name: Particule + field_contact_job_title: Métier + field_contact_company: Société + field_contact_address: Adresse + field_contact_phone: Téléphone + field_contact_email: Email + field_contact_website: Site Web + field_contact_skype: Skype + field_contact_status: État + field_contact_background: Contexte + field_contact_tag_names: Étiquettes + field_first_name: Prénom + field_last_name: Nom de famille + field_company: Société + field_birthday: Anniversaire + field_contact_department: Département + + + field_company_field: Secteur d'activité + + field_color: Couleur + + button_add_note: Ajouter note + notice_successful_save: Enregistré correctement + notice_successful_add: Ajouté correctement + notice_unsuccessful_save: Enregistrement impossible + notice_successful_merged: Fusionné correctement + + notice_merged_warning: Toutes les notes, projets, étiquettes et tâches associés à cette personne seront déplacé vers celle choisie ci-dessous. Le contact sera ensuite supprimé. + + project_module_contacts: Contacts + + permission_view_contacts: Afficher les contacts + permission_edit_contacts: Éditer les contacts + permission_delete_contacts: Supprimer les contacts + permission_view_deals: Voir les affaires + permission_edit_deals: Éditer les affaires + permission_delete_deals: Supprimer les affaires + permission_add_notes: Ajouter des notes + permission_delete_notes: Supprimer des notes + permission_delete_own_notes: Supprimer ses notes + + # 2.0.0 + label_crm_deal_category: Catégorie d'affaire + label_crm_deal_category_plural: Catégories d'affaire + label_crm_deal_category_new: Nouvelle catégorie + text_deal_category_destroy_assignments: Supprimer les attributions de catégories + text_deal_category_destroy_question: "Des affaires (%{count}) sont attribuées à cette catégorie. Que souhaitez-vous faire ?" + text_deal_category_reassign_to: Réattribuer les affaires à cette catégorie + text_deals_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer ces affaires ?' + label_crm_deal_status_plural: État des affaires + label_crm_deal_status: État de l'affaire + field_deal_status_is_closed: Fermée + label_crm_deal_status_new: New + permission_manage_contacts: Enumérations + label_crm_sales_funnel: Clients potentiels + label_crm_period: Point + label_crm_count: Comptage + + #2.0.1 + label_crm_user_format: Format du nom + label_crm_my_contact_plural: Mes contacts + label_crm_my_deal_plural: Ouvrir Mes affaires + label_crm_contact_view_all: Voir tous les contacts + label_crm_deal_view_all: Voir toutes les affaires + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Éditer tous les contacts sélectionnés + label_crm_bulk_edit_selected_deals: Éditer toutes les affaires sélectionnées + label_crm_bulk_send_mail_selected_contacts: Envoyer un email aux contacts sélectionnés + field_add_tags: Ajouter des étiquettes + field_delete_tags: Supprimer les étiquettes + label_crm_send_mail: Envoyer un email + error_empty_email: L'email ne peut être vide + permission_send_contacts_mail: Envoyer l'email + field_mail_from: Expéditeur + text_email_macros: "Macros disponibles : %{macro}" + field_message: Message + + #2.0.3 + label_crm_add_contact: Ajouter un contact + label_contact: Contact + field_age: Âge + label_crm_vcf_import: Importer de la vCard + label_crm_mail_from: De + permission_import_contacts: Importer des contacts + + #2.1.0 + field_company_name: Nom de la société + label_crm_recently_added_contacts: Contacts récents + label_crm_created_by_me: Contact créés par moi + my_contacts: Mes contacts + my_deals: Mes affaires + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Appeler + label_crm_note_type_meeting: Réunion + field_deal_currency: Unité monétaire + label_crm_my_contacts_stats: Statistiques des contacts pour ce mois + label_crm_contacts_created: Contacts créés + label_crm_deals_created: Affaires créées + my_contacts_avatars: Mes photos de contacts + my_contacts_stats: Statistiques des contacts + label_crm_add_into: Ajouter à + label_crm_delete_from: Supprimer de + label_crm_show_deaks_tab: Afficher l'onglet Affaires + label_crm_show_on_projects_show: Afficher les contacts sur la vue d'ensemble des projets + + #2.2.1 + label_crm_contacts_show_in_list: Afficher dans la liste + + #2.3.0 + label_crm_module_plural: Modules + label_crm_list_partial_style: Style de liste + label_crm_list_excerpt: Liste d'exceptions + label_crm_list_cards: Cartes + label_crm_list_list: Table + field_contacts: Contact + field_companies: Société + label_crm_added_by: Ajouté par + label_crm_contact_note_authoring_time: Afficher l'heure de la note + label_crm_contact_issues_filters: Filtres de demandes + label_crm_csv_import: Importer d'un CSV + label_crm_upload_encoding: Encodage du fichier + label_crm_csv_file: Fichier CSV + label_crm_csv_separator: Séparateur + field_middle_name: Particule + field_job_title: Métier + field_company: Company + field_address: Adresse + field_phone: Téléphone + field_email: Email + field_tags: Étiquettes + field_last_note: Dernière note + field_is_company: Est une société + field_contact_full_name: Nom complet + button_contacts_edit_query: Modifier la requête + button_contacts_delete_query: Supprimer la requête + permission_manage_public_contacts_queries: Gérer les requêtes publiques + permission_add_deals: Ajouter des affaires + permission_add_contacts: Ajouter des contacts + permission_save_contacts_queries: Enregistrer les requêtes + + #2.3.3 + label_crm_contact_show_in_app_menu: Afficher les onglets dans le menu des applications + + #2.3.4 + label_crm_contact_show_closed_issues: Afficher les demandes fermées + + #3.0.0 + label_crm_import: Importer + label_contact_note_plural: Notes du contact + label_deal_note_plural: Notes des affaires + label_crm_contact_all_note_plural: Toutes les notes + error_unable_delete_deal_status: Impossible de supprimer l'état de l'affaire + label_crm_contacts_hidden: Paramètres masqués + + #3.1.0 + label_crm_contact_added: Contact ajouté + label_crm_note_added: Note added + label_crm_deal_added: Affaire ajoutée + label_crm_deal_updated: Affaires mise à jour + text_crm_contact_added: "Le contact %{name} a été ajouté par %{author}." + text_crm_deal_added: "L'affaire %{name} a été ajoutée par %{author}." + text_crm_deal_status_changed: "L'état de l'affaire a été changé de %{old} en %{new}" + text_crm_deal_updated: "L'affaire %{name} a été mise à jour par %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Cci + label_crm_contact_import: Importer d'un CSV + permission_import_deals: Importer des affaires + label_crm_single_quotes: "Apostrophe (')" + label_crm_double_quotes: "Guillemets (\")" + label_crm_quotes_type: Type de marques de citations + label_crm_contacts_visibility: Visibilité + label_crm_contacts_visibility_project: Permissions par projet + label_crm_contacts_visibility_public: Public + label_crm_contacts_visibility_private: Privé + permission_view_private_contacts: Afficher les contacts privés + text_crm_error_on_line: 'Erreur à la ligne %{line} : %{error}.' + + #3.2.0 + label_crm_probability: Probabilité + label_crm_deal_status_type: Type d'état + label_crm_select_companies: Sociétés avec affaires + label_crm_expected_revenue: Revenu attendu + label_crm_deal_due_date: Date de livraison + + #3.2.2 + label_crm_show_deals_in_top_menu: Afficher les affaires dans le menu principal + label_crm_show_details: Afficher les détails + label_crm_has_deals: A des affaires + label_crm_has_open_issues: Demandes ouvertes + label_crm_note: Note + + #3.2.5 + notice_failed_to_save_contacts: "Échec de l'enregistrement de %{count} contact(s) sur %{total} sélectionnés : %{ids}." + + #3.2.6 + project_module_deals: Affaires + permission_manage_deals: Gérer les affaires + label_crm_deals_from_subprojects: Afficher les affaires des sous-projets + label_crm_megre_tags: Fusionner les étiquettes + label_crm_monochrome_tags: Étiquettes monochromes + + #3.2.7 + label_crm_address: Adresse + label_crm_street1: Rue 1 + label_crm_street2: Rue 2 + label_crm_city: Ville + label_crm_region: 'État' + label_crm_postcode: 'Code postal' + label_crm_country: Pays + label_crm_countries: + AF: Afghanistan + AL: Albanie + DZ: Algérie + AS: Samoa américain + AD: Andorre + AO: Angola + AI: Anguilla + AQ: Antarctique + AG: Antigua -et-Barbuda + AR: Argentine + AM: Arménie + AW: Aruba + AU: Australie + AT: Autriche + AZ: Azerbaïdjan + BS: Bahamas + BH: Bahreïn + BD: Bangladesh + BB: Barbade + BY: Bélarus + BE: Belgique + BZ: Belize + BJ: Bénin + BM: Bermudes + BT: Bhoutan + BO: Bolivie + BA: Bosnie-Herzégovine + BW: Botswana + BV: Bouvet Island + BR: Brésil + BQ: Territoire britannique de l'Antarctique + IO: Territoire britannique de l' océan Indien + VG: ÃŽles Vierges britanniques + BN: Brunei + BG: Bulgarie + BF: Burkina Faso + BI: Burundi + KH: Cambodge + CM: Cameroun + CA: Canada + CT: Canton et Enderbury + CV: Cap-Vert + KY: ÃŽles Caïmans + CF: République centrafricaine + TD: Tchad + CL: Chili + CN: Chine + CX: Christmas Island + CC: Cocos [ Keeling ] ÃŽles + CO: Colombie + KM: Comores + CG: Congo-Brazzaville + CD: Congo-Kinshasa + CK: ÃŽles Cook + CR: Costa Rica + HR: Croatie + CU: Cuba + CY: Chypre + CZ: République tchèque + CI: Côte d' Ivoire + DK: Danemark + DJ: Djibouti + DM: Dominique + DO: République dominicaine + NQ: Dronning Maud Land + DD: Allemagne de l'Est + EC: Équateur + EG: Egypte + SV: El Salvador + GQ: Guinée équatoriale + ER: Erythrée + EE: Estonie + ET: Ethiopie + FK: îles Falkland + FO: ÃŽles Féroé + FJ: Fidji + FI: Finlande + FR: France + GF: Guyane française + PF: Polynésie française + TF: Terres australes françaises + FQ: Sud et antarctiques françaises + GA: Gabon + GM: Gambie + GE: Géorgie + DE: Allemagne + GH: Ghana + GI: Gibraltar + GR: Grèce + GL: Groenland + GD: Grenade + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernesey + GN: Guinée + GW: Guinée- Bissau + GY: Guyane + HT: Haïti + HM: Les îles Heard et McDonald + HN: Honduras + HK: Hong Kong Chine + HU: Hongrie + IS: Islande + IN: Inde + ID: Indonésie + IR: Iran + IQ: Irak + IE: Irlande + IM: Ile de Man + IL: Israël + IT: Italie + JM: Jamaïque + JP: Japon + JE: jersey + JT: Johnston Island + JO: Jordanie + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Koweit + KG: Kirghizistan + LA: Laos + LV: Lettonie + LB: Liban + LS: Lesotho + LR: Libéria + LY: Libye + LI: Liechtenstein + LT: Lituanie + LU: Luxembourg + MO: Macao Chine + MK: Macédoine + MG: Madagascar + MW: Malawi + MY: Malaisie + MV: Maldives + ML: Mali + MT: Malte + MH: ÃŽles Marshall + MQ: Martinique + MR: Mauritanie + MU: Maurice + YT: Mayotte + FX: France métropolitaine + MX: Mexique + FM: Micronésie + MI: Midway Islands + MD: Moldavie + MC: Monaco + MN: Mongolie + ME: Monténégro + MS: Montserrat + MA: Maroc + MZ: Mozambique + MM: Myanmar [ Birmanie ] + NA: Namibie + NR: Nauru + NP: Népal + NL: Pays-Bas + AN: Antilles néerlandaises + NT: Zone Neutre + NC: Nouvelle-Calédonie + NZ: nouvelle-Zélande + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: Corée du Nord + VD: Nord Vietnam + MP: ÃŽles Mariannes du Nord + "NO": Norvège + OM: Oman + PC: Territoire des îles du Pacifique d'affectation spéciale + PK: Pakistan + PW: Palau + PS: Territoires palestiniens + PA: Panama + PZ: Panama Canal Zone + PG: Papouasie-Nouvelle- Guinée + PY: Paraguay + YD: République démocratique populaire du Yémen + PE: Pérou + PH: Philippines + PN: Pitcairn + PL: Pologne + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Roumanie + RU: Russie + RW: Rwanda + RE: Réunion + BL: Saint-Barthélemy + SH: Sainte-Hélène + KN: Saint-Kitts- et-Nevis + LC: Sainte-Lucie + MF: Saint Martin + PM: Saint-Pierre-et-Miquelon + VC: Saint -Vincent-et -les-Grenadines + WS: Samoa + SM: San Marino + SA: Arabie Saoudite + SN: Sénégal + RS: Serbie + CS: Serbie -et-Monténégro + SC: Seychelles + SL: Sierra Leone + SG: Singapour + SK: Slovaquie + SI: Slovénie + SB: ÃŽles Salomon + SO: Somalie + ZA: Afrique du Sud + GS: Géorgie du Sud et les îles Sandwich du Sud + KR: Corée du Sud + ES: Espagne + LK: Sri Lanka + SD: Soudan + SR: Suriname + SJ: Svalbard et Jan Mayen + SZ: Swaziland + SE: Suède + CH: Suisse + SY: Syrie + ST: São Tomé et Príncipe + TW: Taiwan + TJ: Tadjikistan + TZ: Tanzanie + TH: Thaïlande + TL: Timor -Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinité-et- Tobago + TN: Tunisie + TR: Turquie + TM: Turkménistan + TC: ÃŽles Turques et Caïques + TV: Tuvalu + UM: ÃŽles mineures éloignées des États-Unis + PU: États-Unis ÃŽles du Pacifique Divers + VI: ÃŽles Vierges américaines + UG: Ouganda + UA: Ukraine + AE: Émirats arabes unis + GB: Royaume-Uni + US: États-Unis + ZZ: Région inconnue ou non valide + UY: Uruguay + UZ: Ouzbékistan + VU: Vanuatu + VA: Cité du Vatican + VE: Venezuela + VN: Viêt-Nam + WK: Wake Island + WF: Wallis et Futuna + EH: Sahara occidental + YE: Yémen + ZM: Zambie + ZW: Zimbabwe + AX: ÃŽles Ã…land + label_crm_cross_project_contacts: Autoriser les relations de contacts inter-projets + label_crm_list_board: Tableau + label_crm_default_list_style: Style de liste par défaut + label_crm_show_in_top_menu: Afficher dans le menu principal + label_crm_show_in_app_menu: Afficher dans le menu des applications + label_crm_money_settings: Monnaie + label_crm_disable_taxes: Désactiver les taxes + label_crm_default_tax: Valeur de taxe par défaut + label_crm_tax_type: Type de taxe + label_crm_tax_type_inclusive: Taxe incluse + label_crm_tax_type_exclusive: Hors taxe + label_crm_default_currency: Monnaie par défaut + label_crm_thousands_delimiter: Séparateur de milliers + label_crm_decimal_separator: Séparateur décimal + + #3.2.10 + label_crm_add_contact_plural: Ajouter des contacts + label_crm_search_for_contact: Rechercher un contact + label_crm_major_currencies: Principales monnaies + + #3.2.11 + label_crm_post_address_format: Format d'adresse postale + label_crm_post_address_format_macros: "Macros de formats d'adresses : %{macros}" + + #3.2.14 + label_crm_last_year: label_crm_last_year + + #3.2.15 + permission_export_contacts: Exporter les contacts et les affaires + + #3.4.0 + label_crm_deal_contact: Contact de l'affaire + + #3.4.1 + label_crm_default_country: Pays par défaut + label_attribute_of_contact: "Contacts de %{name}" + label_crm_contact_country: Pays du contact + label_crm_contact_city: Ville du contact + label_attribute_of_deals: "Affaires de %{name}" + + #3.4.2 + permission_manage_public_deals_queries: Gérer les requêtes publiques pour l'affaire + permission_save_deals_queries: Enregistrer les requêtes de l'affaire + permission_manage_contact_issue_relations: Gérer les relations de demandes + + #3.4.4 + label_crm_pipeline: Pipeline + text_crm_no_deal_statuses_in_project: Aucun état d'affaires dans le projet + + #3.4.6 + label_crm_contact_email: Email du contact + label_crm_light_free_version: CRM version gratuite + label_crm_link_to_pro: Mettre à jour en version PRO + label_crm_link_to_pro_demo: Démo live de la version PRO + label_crm_link_to_more_plugins: Voir plus de plugins RedmineCRM \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/hu.yml b/plugins/redmine_contacts/config/locales/hu.yml new file mode 100644 index 0000000..e3f985c --- /dev/null +++ b/plugins/redmine_contacts/config/locales/hu.yml @@ -0,0 +1,272 @@ +# encoding: utf-8 +hu: + contacts_title: Kapcsolatok + + label_crm_recently_viewed: Nemrég megtekintve + label_crm_gravatar_enabled: Gravatar használata + label_crm_thumbnails_enabled: Mutassa a képek elÅ‘nézetét a megjegyzésekben + label_crm_max_thumbnail_file_size: Max elÅ‘nézeti képméret + label_crm_view_all_contacts: Összes kapcsolat megtekintése + label_crm_background_info: Háttérinformáció + label_crm_company: Cég + label_contact_plural: Kapcsolatok + label_crm_contact_edit_information: Kapcsolat információ szerkesztése + label_crm_edit_tags: Címkék szerkesztése + label_crm_contact_view: Megtekintés + label_crm_contact_list: Lista + label_crm_contact_new: Új kapcsolat + label_crm_at_company: itt + label_crm_last_notes: Legutóbbi megjegyzések + label_crm_tags_plural: Címkék + label_crm_multi_tags_plural: Több címke választása + label_crm_single_tag_mode: Egy címke + label_crm_multiple_tags_mode: Több címke + label_crm_contact_tag: Címke + label_crm_time_ago: óta + label_crm_add_note_plural: Megjegyzés hozzádadása + label_crm_note_plural: Megjegyzések + + label_crm_add_tags_rule: vesszÅ‘vel elválasztva + label_crm_contact_search: Keresés név alapján + label_crm_note_for: "Megjegyzés a következÅ‘höz:" + label_crm_show_on_map: Megtekintés térképen + label_crm_add_another_phone: telefonszám hozzáadása + label_crm_remove: törlés + label_crm_related_contacts: Kapcsolódó kapcsolatok + label_crm_assigned_to: FelelÅ‘s + label_crm_issue_added: Feladat hozzáadva + label_crm_add_emails_rule: vesszÅ‘vel elválasztva + label_crm_add_phones_rule: vesszÅ‘vel elválasztva + label_crm_add_employee: Új alkalmazott + label_crm_merge_duplicate_plural: Összevonás + label_crm_duplicate_plural: Lehetséges másolat + label_crm_duplicate_for_plural: "Lehetséges másolata a következÅ‘nek:" + label_crm_add_tag: "+ címke hozzáadása" + + label_crm_note_show_extras: "Haladó opciók (típus, dátum, fájlok)" + label_crm_note_hide_extras: Haladó opciók elrejtése + label_crm_note_added: Megjegyzés hozzáadva + label_crm_note_read_more: (tovább) + label_crm_invoice_import: Számlák importálása CSV fájlból + + label_deal_plural: Alkuk + label_crm_contractor_plural: Kapcsolatok + label_deal: Alku + label_crm_deal_new: Új alku + label_crm_deal_edit_information: Alku részleteinek szerkesztése + label_crm_deal_change_status: Státusz módosítása + label_crm_statistics: Statisztika + label_crm_deals_import: Alkuk importálása CSV fájlból + + label_crm_deal_status_new: Új + label_crm_deal_status_first_contact: Kapcsolatfelvétel + label_crm_deal_status_negotiations: Tárgyalás alatt + label_crm_deal_status_pending: Folyamatban + label_crm_deal_status_won: Sikeres + label_crm_deal_status_lost: Sikertelen + + label_crm_created_on: Létrehozva + + field_note_date: Megjegyzés dátuma + field_background: Háttér + field_currency: Pénznem + field_contact: Kapcsolat + + field_deal_name: Név + field_deal_background: Háttér + field_deal_contact: Kapcsolat + field_deal_price: Összeg + field_price: Összeg + + field_contact_avatar: Avatar + field_contact_is_company: Cég + field_contact_name: Név + field_contact_last_name: Vezetéknév + field_contact_first_name: Keresztnév + field_contact_middle_name: "KözépsÅ‘ név" + field_contact_job_title: Beosztás + field_contact_company: Cég + field_contact_address: Cím + field_contact_phone: Telefonszám + field_contact_email: Email + field_contact_website: Weboldal + field_contact_skype: Skype + field_contact_status: Státusz + field_contact_background: Háttér + field_contact_tag_names: Címkék + field_first_name: Keresztnév + field_last_name: Vezetéknév + field_company: Cég + field_birthday: Születésnap + field_contact_department: Részleg + + + field_company_field: Iparág + + field_color: Szín + + button_add_note: Megjegyzés hozzáadása + notice_successful_save: Sikeresen elmentve + notice_successful_add: Sikeresen létrehozva + notice_unsuccessful_save: Problémák mentés közben + notice_successful_merged: Sikeresen összevonva + + notice_merged_warning: "Az összes megjegyzés, projekt, címke és feladat át lesz helyezve az alább kiválasztott kapcsolathoz, ez pedig törölve lesz." + + project_module_contacts: Kapcsolatok + + permission_view_contacts: Kapcsolatok megtekintése + permission_edit_contacts: Kapcsolatok szerkesztése + permission_delete_contacts: Kapcsolatok törlése + permission_view_deals: Alkuok megtekintése + permission_edit_deals: Alkuok szerkesztése + permission_delete_deals: Alkuok törlése + permission_add_notes: Megjegyzések hozzáadása + permission_delete_notes: Megjegyzések törlése + permission_delete_own_notes: Saját megjegyzések törlése + + # 2.0.0 + label_crm_deal_category: Alku kategória + label_crm_deal_category_plural: Alku kategóriák + label_crm_deal_category_new: Új kategória + text_deal_category_destroy_assignments: Kategória hozzárendelések törlése + text_deal_category_destroy_question: "Néhány alku (%{count}) ehhez a kategóriához van rendelve. Mit szeretne tenni?" + text_deal_category_reassign_to: Alkuok áthelyezése ebbe a kategóriába + text_deals_destroy_confirmation: 'Tényleg törölni akarja a kiválasztott alku(ka)t?' + label_crm_deal_status_plural: Alku státuszok + label_crm_deal_status: Alku státusza + field_deal_status_is_closed: Lezárva + label_crm_deal_status_new: Új + permission_manage_contacts: Felsorolások + label_sale_funel: Értékesítési csatorna + label_crm_period: Periódus + label_crm_count: Számítás + + #2.0.1 + label_crm_user_format: Kapcsolat nevének formátuma + label_crm_my_contact_plural: Hozzám rendelt kapcsolatok + label_crm_my_deal_plural: Folyamatban lévÅ‘ alkuaim + label_crm_contact_view_all: Összes kapcsolat megtekintése + label_crm_deal_view_all: Összes alku megtekintése + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Kiválasztott kapcsolatok szerkesztése + label_crm_bulk_edit_selected_deals: Kiválasztott alkuk szerkesztése + label_crm_bulk_send_mail_selected_contacts: Üzenet küldése a kiválasztott kapcsolatoknak + field_add_tags: Címkék hozzáadása + field_delete_tags: Címkék eltávolítása + label_crm_send_mail: Üzenet küldése + error_empty_email: Az email nem lehet üres + permission_send_contacts_mail: Üzenet küldése + field_mail_from: Feladó címe + text_email_macros: "ElérhetÅ‘ makrók %{macro}" + field_message: Üzenet + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Kapcsolat + field_age: Kor + label_crm_vcf_import: Importálás vCard fájlból + label_crm_mail_from: Feladó + permission_import_contacts: Kapcsolatok importálása + + #2.1.0 + field_company_name: Cég neve + label_crm_recently_added_contacts: Legutóbb hozzáadott kapcsolatok + label_crm_created_by_me: Ãltalam létrehozott kapcsolatok + my_contacts: Sajét kapcsolatok + my_deals: Saját alkuk + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Hívás + label_crm_note_type_meeting: Alku + field_deal_currency: Pénznem + label_crm_my_contacts_stats: Kapcsolatok havi statisztikája + label_crm_contacts_created: Kapcsolatok létrehozva + label_crm_deals_created: Alkuok létrehozva + my_contacts_avatars: Saját kapcsolatok fényképei + my_contacts_stats: Kapcsolatok statisztikája + label_crm_add_into: Hozzáadás ide + label_crm_delete_from: Törlés innen + label_crm_show_deaks_tab: Alkuok fül megjelenítése + label_crm_show_on_projects_show: Kapcsolatok megjelenítése a projekt áttekintési oldalán + + #2.2.1 + label_crm_contacts_show_in_list: Megjelenítés listában + + #2.3.0 + label_crm_module_plural: Modulok + label_crm_list_partial_style: Kapcsolatok megjelenítési módja + label_crm_list_excerpt: Kivonatolt lista + label_crm_list_cards: Névjegyek + label_crm_list_list: Táblázat + field_contacts: Kapcsolat + field_companies: Cég + label_crm_added_by: Hozzáadta + label_crm_contact_note_authoring_time: Megjegyzés idejének megtekintése + label_crm_contact_issues_filters: Feladat szűrÅ‘k + label_crm_csv_import: Importálás CSV fájlból + label_crm_upload_encoding: Fájl kódolás + label_crm_csv_file: CSV fájl + label_crm_csv_separator: Elválasztó + field_middle_name: KözépsÅ‘ név + field_job_title: Beosztás + field_company: Cég + field_address: Cím + field_phone: Telefonszám + field_email: Email + field_tags: Címkék + field_last_note: Utolsó megjegyzés + field_is_company: Cég + field_contact_full_name: Teljes név + button_contacts_edit_query: Lekérdezés szerkesztése + button_contacts_delete_query: Lekérdezés törlése + permission_manage_public_contacts_queries: Publikus lekérdezések szerkesztése + permission_add_deals: Alkuk létrhozása + permission_add_contacts: Kapcsolatok hozzáadása + permission_save_contacts_queries: Lekérdezések mentése + + #2.3.3 + label_crm_contact_show_in_app_menu: Megjelenítése az alkalmazás menüben + + #2.3.4 + label_crm_contact_show_closed_issues: Mutassa a lezárt feladatokat + + #3.0.0 + label_crm_import: Importálás + label_contact_note_plural: Kapcsolat megjegyzései + label_deal_note_plural: Alku megjegyzései + label_crm_contact_all_note_plural: Összes megjegyzés + error_unable_delete_deal_status: Nem lehet eltávolítani az alku státuszát + label_crm_contacts_hidden: Rejtett beállítások + + #3.1.0 + label_crm_contact_added: Kapcsolat hozzáadva + label_crm_note_added: Megjegyzés hozzáadva + label_crm_deal_added: Alku létrehozva + label_crm_deal_updated: Alku frissítve + text_crm_contact_added: "A %{name} kapcsolatot %{author} módosította." + text_crm_deal_added: "A %{name} alkut %{author} létrehozta." + text_crm_deal_status_changed: "Alku státusza megváltozott, %{old} helyett %{new} lett" + text_crm_deal_updated: "A %{name} alkut %{author} módosította." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Importálás CSV fájlból + permission_import_deals: Alkuok importálása + label_crm_single_quotes: "Szimpla idézÅ‘jel (')" + label_crm_double_quotes: "Dupla idézÅ‘jel (\")" + label_crm_quotes_type: IdézÅ‘jel típusa + label_crm_contacts_visibility: Láthatóság + label_crm_contacts_visibility_project: Projekt jogosultságok alapján + label_crm_contacts_visibility_public: Publikus + label_crm_contacts_visibility_private: Privát + permission_view_private_contacts: Privát kontaktok megjelenítése + text_crm_error_on_line: "Hiba a %{line}. sorban: %{error}." + + #3.2.0 + label_crm_probability: Valószínűség + label_crm_deal_status_type: Státusz típusa + label_crm_select_companies: Cégek mint alku kapcsolatai + label_crm_expected_revenue: Becsült bevétel + label_crm_deal_due_date: Esedékes dátum \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/it.yml b/plugins/redmine_contacts/config/locales/it.yml new file mode 100644 index 0000000..0322336 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/it.yml @@ -0,0 +1,159 @@ +it: + contacts_title: Contatti + + label_crm_recently_viewed: Visualizzati di recente + label_crm_gravatar_enabled: Usa Gravatar + label_crm_thumbnails_enabled: Mostra miniature belle note + label_crm_max_thumbnail_file_size: Dimensione massima delle note + label_crm_view_all_contacts: Vedi tutti i contatti + label_crm_background_info: Storico contatto + label_crm_company: Azienda + label_contact_plural: Lista dei contatti + label_crm_contact_edit_information: Modifica le informazione del contatto + label_crm_edit_tags: Modifica etichette + label_crm_contact_view: Visualizza + label_crm_contact_list: Lista + label_crm_contact_new: Nuovo + label_crm_at_company: at + label_crm_last_notes: Note recenti + label_crm_tags_plural: Etichette + label_crm_multi_tags_plural: Selezione etichette multiple + label_crm_single_tag_mode: Etichetta singola + label_crm_multiple_tags_mode: Etichette multiple + label_crm_contact_tag: Etichetta + label_crm_time_ago: fa' + label_crm_add_note_plural: Aggiungi note a + label_crm_note_plural: Note + + label_crm_add_tags_rule: separarti da virgola + label_crm_contact_search: Cerca per nome + label_crm_note_for: Nota per + label_crm_show_on_map: Mostra nella mappa + label_crm_add_another_phone: aggiungi telefono + label_contact_note_plural: Tutte le note + label_crm_remove: elimina + label_crm_related_contacts: Contatti collegati + label_crm_assigned_to: Responsabile + label_crm_issue_added: Segnalazione aggiunta + label_crm_add_emails_rule: separati da virgola + label_crm_add_phones_rule: separati da virgola + label_crm_add_employee: Nuovo impiegato + label_crm_merge_duplicate_plural: Merge + label_crm_duplicate_plural: Duplicati ammessi + label_crm_duplicate_for_plural: Possible duplicates for + + label_crm_note_show_extras: Dettagli (files, date) + label_crm_note_hide_extras: Nascondi dettagli + label_crm_note_added: La nota e' stata aggiunta + label_crm_note_read_more: (continua) + + label_deal_plural: Contratti + label_crm_contractor_plural: Contraenti + label_deal: Contratto + label_crm_deal_new: Nuovo contratto + label_crm_deal_edit_information: Modifica dettagli contratto + label_crm_deal_change_status: Cambia stato + label_crm_deal_pending: In trattativa + label_crm_deal_won: Attivo + label_crm_deal_lost: Perso + label_crm_deal_expired: Scaduto + + + field_note_date: Data della nota + + field_deal_name: Nome + field_deal_background: Storico + field_deal_contact: Contatti + field_deal_price: Importo + field_price: Importo + + field_contact_avatar: Avatar + field_contact_is_company: Azienda + field_contact_name: Nome + field_contact_last_name: Cognome + field_contact_first_name: Nome + field_contact_middle_name: Secondo nome + field_contact_job_title: Titolo + field_contact_company: Azienda + field_contact_address: Indirizzo + field_contact_phone: Telefono + field_contact_email: Email + field_contact_website: Sito web + field_contact_skype: Skype + field_contact_status: Stato + field_contact_background: Storico + field_contact_tag_names: Etichette + field_first_name: Nome + field_last_name: Cognome + field_company: Azienda + field_birthday: Compleanno + field_contact_department: Reparto + + field_company_field: Industry + + field_color: Colore + + button_add_note: Aggiungi nota + notice_successful_save: Salvato correttamente + notice_successful_add: Creato correttamente + notice_unsuccessful_save: Salvataggio fallito + notice_successful_merged: Uniti correttamente + + notice_merged_warning: Tutte le note, i progetti, le etichette ed i compiti collegati a questo contatto saranno uniti con il contatto scelto in basso. Quindi questo contatto sara' cancellato. + + project_module_contacts: Contatti + + permission_view_contacts: Visualizza contatto + permission_edit_contacts: Modifica contatto + permission_delete_contacts: Elimina contatto + permission_view_deals: Visualizza contratto + permission_edit_deals: Modifica contratto + permission_delete_deals: Elimina contratto + permission_add_notes: Aggiungi note + permission_delete_notes: Elimina note + permission_delete_own_notes: Elimina le proprie note + + # 2.0.0 + label_crm_deal_category: categoria affare + label_crm_deal_category_plural: categorie di offerte + label_crm_deal_category_new: Nuova categoria + text_deal_category_destroy_assignments: Rimuovere categoria incarichi + text_deal_category_destroy_question: "Alcune occasioni (% {count}) vengono assegnati a questa categoria Cosa vuoi fare.? " + text_deal_category_reassign_to: Riassegnare offerte per questa categoria + text_deals_destroy_confirmation: "Sei sicuro di voler cancellare l'affare selezionati (s)?" + label_crm_deal_status_plural: stati affare + label_crm_deal_status: affare di stato + field_deal_status_is_closed: chiuso + label_crm_deal_status_new: Nuovo + permission_manage_contacts: enumerazioni + label_crm_sales_funnel: imbuto di vendita + label_crm_period: Data dell'arrivo + label_crm_count: Conte + + # 2.0.1 + label_crm_user_format: Nome del contatto formato + label_crm_my_contact_plural: Contatti assegnato a me + label_crm_my_deal_plural: Offerte aperto assegnato a me + label_crm_contact_view_all: Vedi tutti i contatti + label_crm_deal_view_all: Mostra tutte le offerte + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edit all selected contacts + label_crm_bulk_edit_selected_deals: Edit all selected deals + label_crm_bulk_send_mail_selected_contacts: Send mail to selected contacts + field_add_tags: Add tags + field_delete_tags: Delete tags + label_crm_send_mail: Send mail + error_empty_email: Email can not be blank + permission_send_contacts_mail: Send mail + field_mail_from: From address + text_email_macros: Avaliable macros %{macro} + field_message: Message + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Contact + field_age: Age + label_crm_vcf_import: Import from vCard + label_crm_mail_from: From + permission_import_contacts: Import contacts \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/ja.yml b/plugins/redmine_contacts/config/locales/ja.yml new file mode 100644 index 0000000..354e241 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/ja.yml @@ -0,0 +1,588 @@ +# encoding: utf-8 +ja: + contacts_title: コンタクト + + label_crm_recently_viewed: 最近表示ã—ãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_gravatar_enabled: Gravatar ã®ä½¿ç”¨ + label_crm_thumbnails_enabled: ノートã«ã‚µãƒ ãƒã‚¤ãƒ«ã‚’表示ã™ã‚‹ + label_crm_max_thumbnail_file_size: サムãƒã‚¤ãƒ«ç”»åƒã®æœ€å¤§ã‚µã‚¤ã‚º + label_crm_view_all_contacts: ã™ã¹ã¦ã®ã‚³ãƒ³ã‚¿ã‚¯ãƒˆã‚’表示 + label_crm_background_info: 背景 + label_crm_company: 会社 + label_contact_plural: コンタクト + label_crm_contact_edit_information: コンタクト情報を編集 + label_crm_edit_tags: タグを編集 + label_crm_contact_view: 表示 + label_crm_contact_list: 一覧 + label_crm_contact_new: æ–°è¦ã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_at_company: at + label_crm_last_notes: 最近ã®ãƒŽãƒ¼ãƒˆ + label_crm_tags_plural: ã‚¿ã‚° + label_crm_multi_tags_plural: 複数タグã®é¸æŠž + label_crm_single_tag_mode: å˜ä¸€ã‚¿ã‚° + label_crm_multiple_tags_mode: 複数タグ + label_crm_contact_tag: ã‚¿ã‚° + label_crm_time_ago: å‰ + label_crm_add_note_plural: 'ノートを追加: ' + label_crm_note_plural: ノート + + label_crm_add_tags_rule: カンマ区切り + label_crm_contact_search: å剿¤œç´¢ + label_crm_note_for: 'ノート: ' + label_crm_show_on_map: 地図表示 + label_crm_add_another_phone: 電話番å·ã®è¿½åŠ  + label_crm_remove: 削除 + label_crm_related_contacts: 関連ã™ã‚‹ã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_assigned_to: 担当者 + label_crm_issue_added: ãƒã‚±ãƒƒãƒˆã®è¿½åŠ  + label_crm_add_emails_rule: カンマ区切り + label_crm_add_phones_rule: カンマ区切り + label_crm_add_employee: æ–°è¦ç¤¾å“¡ + label_crm_merge_duplicate_plural: マージ + label_crm_duplicate_plural: é‡è¤‡ç¢ºèª + label_crm_duplicate_for_plural: 'é‡è¤‡ç¢ºèª: ' + label_crm_add_tag: + タグを追加 + + label_crm_note_show_extras: 詳細 (type, date, files) + label_crm_note_hide_extras: 詳細をéžè¡¨ç¤º + label_crm_note_added: ノートã®è¿½åŠ ã«æˆåŠŸã—ã¾ã—㟠+ label_crm_note_read_more: (詳細表示) + + label_deal_plural: å–引 + label_crm_contractor_plural: コンタクト + label_deal: å–引 + label_crm_deal_new: æ–°è¦å–引 + label_crm_deal_edit_information: å–引情報ã®ç·¨é›† + label_crm_deal_change_status: ステータスを変更 + label_crm_statistics: 統計 + + label_crm_deal_status_new: æ–°è¦ + label_crm_deal_status_first_contact: åˆã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_deal_status_negotiations: 交渉中 + label_crm_deal_status_pending: ペンディング + label_crm_deal_status_won: å—æ³¨ + label_crm_deal_status_lost: 失注 + + label_crm_created_on: ä½œæˆæ—¥ + + field_note_date: ノート日 + + field_deal_name: åç§° + field_deal_background: 背景 + field_deal_contact: コンタクト + field_deal_price: åˆè¨ˆ + field_price: åˆè¨ˆ + + field_contact_avatar: ã‚¢ãƒã‚¿ãƒ¼ + field_contact_is_company: 会社 + field_contact_name: åå‰ + field_contact_last_name: è‹—å­— + field_contact_first_name: åå‰ + field_contact_middle_name: ミドルãƒãƒ¼ãƒ  + field_contact_job_title: å½¹è· + field_contact_company: 会社 + field_contact_address: 使‰€ + field_contact_phone: é›»è©±ç•ªå· + field_contact_email: Email + field_contact_website: Web サイト + field_contact_skype: Skype + field_contact_status: ステータス + field_contact_background: 背景 + field_contact_tag_names: ã‚¿ã‚° + field_first_name: åå‰ + field_last_name: è‹—å­— + field_company: 会社 + field_birthday: 誕生日 + field_contact_department: 部署 + + + field_company_field: 事業 + + field_color: 色 + + button_add_note: æ–°è¦ãƒŽãƒ¼ãƒˆ + notice_successful_save: ä¿å­˜ã«æˆåŠŸã—ã¾ã—㟠+ notice_successful_add: 作æˆã«æˆåŠŸã—ã¾ã—㟠+ notice_unsuccessful_save: ä¿å­˜ã«å¤±æ•—ã—ã¾ã—㟠+ notice_successful_merged: ãƒžãƒ¼ã‚¸ã«æˆåŠŸã—ã¾ã—㟠+ + notice_merged_warning: ã“ã®äººã«æŽ¥ç¶šã•れã¦ã„ã‚‹ã™ã¹ã¦ã®ãƒŽãƒ¼ãƒˆã€ãƒ—ロジェクトã€ã‚¿ã‚°ã€ã‚¿ã‚¹ã‚¯ã¯ä»¥ä¸‹ã«ç§»å‹•ã•れã¾ã™ã€‚コンタクトã¯ãã®å¾Œå‰Šé™¤ã•れã¾ã™ã€‚ + + project_module_contacts: コンタクト + + permission_view_contacts: コンタクトã®è¡¨ç¤º + permission_edit_contacts: コンタクトã®ç·¨é›† + permission_delete_contacts: コンタクトã®å‰Šé™¤ + permission_view_deals: å–引ã®è¡¨ç¤º + permission_edit_deals: å–引ã®ç·¨é›† + permission_delete_deals: å–引ã®å‰Šé™¤ + permission_add_notes: ノートã®è¿½åŠ  + permission_delete_notes: ノートã®å‰Šé™¤ + permission_delete_own_notes: 自分ã®ãƒŽãƒ¼ãƒˆã®å‰Šé™¤ + + # 2.0.0 + label_crm_deal_category: å–引カテゴリ + label_crm_deal_category_plural: å–引カテゴリ + label_crm_deal_category_new: æ–°è¦ã‚«ãƒ†ã‚´ãƒª + text_deal_category_destroy_assignments: カテゴリ割当を解除 + text_deal_category_destroy_question: "ã„ãã¤ã‹ã®å–引 (%{count} ä»¶) ãŒã“ã®ã‚«ãƒ†ã‚´ãƒªã«å‰²å½“ã¦ã‚‰ã‚Œã¦ã„ã¾ã™ã€‚ã©ã®ã‚ˆã†ã«å‡¦ç†ã—ã¾ã™ã‹?" + text_deal_category_reassign_to: ã“ã®ãŒæ‰‹ã”りã¸å†åº¦å‰²å½“ã¦ã‚‹ + text_deals_destroy_confirmation: 'é¸æŠžã•れãŸå–引を削除ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹?' + label_crm_deal_status_plural: å–引ステータス + label_crm_deal_status: å–引ステータス + field_deal_status_is_closed: 終了 + label_crm_deal_status_new: æ–°è¦ + permission_manage_contacts: コンタクトã®ç®¡ç† + label_crm_sales_funnel: 販売目標 + label_crm_period: æœŸé™ + label_crm_count: æ•°é‡ + + #2.0.1 + label_crm_user_format: コンタクトåã®ãƒ•ォーマット + label_crm_my_contact_plural: コンタクトを自分ã«å‰²å½“ã¦ã‚‹ + label_crm_my_deal_plural: å–引を自分ã«å‰²å½“ã¦ã‚‹ + label_crm_contact_view_all: ã™ã¹ã¦ã®ã‚³ãƒ³ã‚¿ã‚¯ãƒˆã‚’表示 + label_crm_deal_view_all: ã™ã¹ã¦ã®å–引を表示 + + #2.0.2 + label_crm_bulk_edit_selected_contacts: é¸æŠžã—ãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆã‚’編集 + label_crm_bulk_edit_selected_deals: é¸æŠžã—ãŸå–引を編集 + label_crm_bulk_send_mail_selected_contacts: é¸æŠžã—ãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆã¸ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ + field_add_tags: タグを追加 + field_delete_tags: タグを削除 + label_crm_send_mail: メールé€ä¿¡ + error_empty_email: 空メールã¯é€ä¿¡ã§ãã¾ã›ã‚“ + permission_send_contacts_mail: メールé€ä¿¡ + field_mail_from: From アドレス + text_email_macros: 利用å¯èƒ½ãªãƒžã‚¯ãƒ­ %{macro} + field_message: メッセージ + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: コンタクト + field_age: å¹´é½¢ + label_crm_vcf_import: vCard ã‹ã‚‰ã‚¤ãƒ³ãƒãƒ¼ãƒˆ + label_crm_mail_from: From アドレス + permission_import_contacts: コンタクトã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ + + #2.1.0 + field_company_name: 会社å + label_crm_recently_added_contacts: 最近追加ã•れãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_created_by_me: 自分ãŒè¿½åŠ ã—ãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + my_contacts: マイコンタクト + my_deals: マイå–引 + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: 電話 + label_crm_note_type_meeting: 会議 + field_deal_currency: é€šéŽ + label_crm_my_contacts_stats: 当月ã®ã‚³ãƒ³ã‚¿ã‚¯ãƒˆçµ±è¨ˆ + label_crm_contacts_created: 作æˆã•れãŸã‚³ãƒ³ã‚¿ã‚¯ãƒˆ + label_crm_deals_created: 作æˆã•れãŸå–引 + my_contacts_avatars: マイコンタクトã®å†™çœŸ + my_contacts_stats: コンタクト統計 + label_crm_add_into: 追加 + label_crm_delete_from: 削除 + label_crm_show_deaks_tab: å–引タブを表示 + label_crm_show_on_projects_show: コンタクトをプロジェクト概è¦ã«è¡¨ç¤º + + #2.2.1 + label_crm_contacts_show_in_list: リストã«è¡¨ç¤º + + #2.3.0 + label_crm_module_plural: モジュール + label_crm_list_partial_style: コンタクトリストã®ã‚¹ã‚¿ã‚¤ãƒ« + label_crm_list_excerpt: Excerpert リスト + label_crm_list_cards: カード + label_crm_list_list: テーブル + field_contacts: コンタクト + field_companies: 会社 + label_crm_added_by: ä½œæˆæ—¥ + label_crm_contact_note_authoring_time: ノート時間ã®è¡¨ç¤º + label_crm_contact_issues_filters: ãƒã‚±ãƒƒãƒˆãƒ•ィルター + label_crm_csv_import: CSV ã‹ã‚‰ã‚³ãƒ³ã‚¿ã‚¯ãƒˆã‚’インãƒãƒ¼ãƒˆ + label_crm_upload_encoding: File エンコード + label_crm_csv_file: CSV ファイル + label_crm_csv_separator: セパレータ + field_middle_name: ミドルãƒãƒ¼ãƒ  + field_job_title: å½¹è· + field_company: 会社 + field_address: 使‰€ + field_phone: 電話 + field_email: Email + field_tags: ã‚¿ã‚° + field_last_note: 最終ノート + field_is_company: 会社 + field_contact_full_name: åå‰ + button_contacts_edit_query: クエリーã®ç·¨é›† + button_contacts_delete_query: クエリーを削除 + permission_manage_public_contacts_queries: 公開クエリーã®ç®¡ç† + permission_add_deals: å–引ã®è¿½åŠ  + permission_add_contacts: コンタクトã®è¿½åŠ  + permission_save_contacts_queries: クエリーã®ä¿å­˜ + + #2.3.3 + label_crm_contact_show_in_app_menu: アプリケーションメニューã«ã‚¿ãƒ–を表示 + + #2.3.4 + label_crm_contact_show_closed_issues: 終了ã—ãŸãƒã‚±ãƒƒãƒˆã‚’表示 + + #3.0.0 + label_crm_import: インãƒãƒ¼ãƒˆ + label_contact_note_plural: コンタクトノート + label_deal_note_plural: å–引ノート + label_crm_contact_all_note_plural: ã™ã¹ã¦ã®ãƒŽãƒ¼ãƒˆ + error_unable_delete_deal_status: å–引ステータスを削除ã§ãã¾ã›ã‚“ + label_crm_contacts_hidden: 設定ã‹ã‚‰éš ã™ + + + #3.1.0 + label_crm_contact_added: Contact added + label_crm_note_added: Note added + label_crm_deal_added: Deal added + label_crm_deal_updated: Deal updated + text_crm_contact_added: "Contact %{name} has been added by %{author}." + text_crm_deal_added: "Deal %{name} has been added by %{author}." + text_crm_deal_status_changed: "Deal status changed from %{old} to %{new}" + text_crm_deal_updated: "Deal %{name} has been updated by %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Import from CSV + permission_import_deals: Import deals + label_crm_single_quotes: "Single quotes (')" + label_crm_double_quotes: "Double quotes (\")" + label_crm_quotes_type: Quotation marks type + label_crm_contacts_visibility: Visibility + label_crm_contacts_visibility_project: By projects permissions + label_crm_contacts_visibility_public: Public + label_crm_contacts_visibility_private: Private + permission_view_private_contacts: View private contacts + text_crm_error_on_line: 'Error on line %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Probability + label_crm_deal_status_type: Status type + label_crm_select_companies: Filter companies in deal + label_crm_expected_revenue: Expected revenue + label_crm_deal_due_date: Due date + + #3.2.2 + label_crm_show_deals_in_top_menu: Show deals in top menu + label_crm_show_details: Show details + label_crm_has_deals: Has deals + label_crm_has_open_issues: Open issues + label_crm_note: Note + + #3.2.5 + notice_failed_to_save_contacts: "Failed to save %{count} contact(s) on %{total} selected: %{ids}." + + #3.2.6 + project_module_deals: Deals + permission_manage_deals: Manage deals + label_crm_deals_from_subprojects: Show deals from subprojects + label_crm_megre_tags: Merge tags + label_crm_monochrome_tags: Monochrome tags + + #3.2.7 + label_crm_address: Address + label_crm_street1: Street 1 + label_crm_street2: Street 2 + label_crm_city: City + label_crm_region: 'State' + label_crm_postcode: 'ZIP' + label_crm_country: Country + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: Cuba + CY: Cyprus + CZ: Czech Republic + CI: Côte d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong SAR China + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: Mexico + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: Poland + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: Russia + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + KR: South Korea + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syria + ST: São Tomé and Príncipe + TW: Taiwan + TJ: Tajikistan + TZ: Tanzania + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + SU: Union of Soviet Socialist Republics + AE: United Arab Emirates + GB: United Kingdom + US: United States + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: Vietnam + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land Islands + label_crm_cross_project_contacts: Allow cross-project contacts relations + label_crm_list_board: Board + label_crm_default_list_style: Default list style + label_crm_show_in_top_menu: Show in top menu + label_crm_show_in_app_menu: Show in app menu + label_crm_money_settings: Money + label_crm_disable_taxes: Disable taxes + label_crm_default_tax: Default tax value + label_crm_tax_type: Tax type + label_crm_tax_type_inclusive: Tax inclusive + label_crm_tax_type_exclusive: Tax exclusive + label_crm_default_currency: Default currency + label_crm_thousands_delimiter: Thousands delimiter + label_crm_decimal_separator: Decimal separator + + #3.2.10 + label_crm_add_contact_plural: Add contacts + label_crm_search_for_contact: Search for contact + label_crm_major_currencies: Major currencies + + #3.2.11 + label_crm_post_address_format: Post address format + label_crm_post_address_format_macros: "Address format macros: %{macros}" + + #3.2.14 + label_crm_last_year: last year + + #3.2.15 + permission_export_contacts: Export contacts and deals \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/ko.yml b/plugins/redmine_contacts/config/locales/ko.yml new file mode 100644 index 0000000..ea61f4c --- /dev/null +++ b/plugins/redmine_contacts/config/locales/ko.yml @@ -0,0 +1,272 @@ +# encoding: utf-8 +ko: + contacts_title: ì—°ë½ì²˜ + + label_crm_recently_viewed: 최근 본 ì‚¬ìš©ìž + label_crm_gravatar_enabled: ì´ë¯¸ì§€ 사용 + label_crm_thumbnails_enabled: ë…¸íŠ¸ì— ì¸ë„¤ì¼ í¬í•¨ + label_crm_max_thumbnail_file_size: Max ì¸ë„¤ì¼ ì´ë¯¸ì§€ 사ì´ì¦ˆ + label_crm_view_all_contacts: ì „ì²´ ì—°ë½ì²˜ 보기 + label_crm_background_info: 설명 + label_crm_company: 회사 + label_contact_plural: ì—°ë½ì²˜ + label_crm_contact_edit_information: ì—°ë½ì²˜ ì •ë³´ 수정 + label_crm_edit_tags: 태그 수정 + label_crm_contact_view: View + label_crm_contact_list: List + label_crm_contact_new: 새 ì—°ë½ì²˜ + label_crm_at_company: ì†Œì† - + label_crm_last_notes: 최근 메모 + label_crm_tags_plural: 태그 + label_crm_multi_tags_plural: 복수 태그 ì„ íƒ + label_crm_single_tag_mode: 싱글 태그 + label_crm_multiple_tags_mode: 복수 태그 + label_crm_contact_tag: Tag + label_crm_time_ago: ì „ + label_crm_add_note_plural: 메모 작성 + label_crm_note_plural: 메모 + + label_crm_add_tags_rule: 콤마로 구분 + label_crm_contact_search: ì´ë¦„으로 찾기 + label_crm_note_for: Note for + label_crm_show_on_map: ì§€ë„ì—서 찾기 + label_crm_add_another_phone: 전화번호 추가 + label_crm_remove: ì‚­ì œ + label_crm_related_contacts: ì—°ê´€ëœ ì—°ë½ì²˜ + label_crm_assigned_to: ì±…ìž„ìž + label_crm_issue_added: ì´ìŠˆ 추가 + label_crm_add_emails_rule: 콤마로 구분 + label_crm_add_phones_rule: 콤마로 구분 + label_crm_add_employee: 새 ì§ì›ì¶”ê°€ + label_crm_merge_duplicate_plural: 합치기 + label_crm_duplicate_plural: 중복 ì‚¬ìš©ìž + label_crm_duplicate_for_plural: ë‹¤ìŒ ì‚¬ìš©ìžì™€ 중복ë˜ì—ˆìŒ + label_crm_add_tag: + 태그 추가 + + label_crm_note_show_extras: 고급옵션 (타입, ë‚ ì§œ, 파ì¼) + label_crm_note_hide_extras: 옵션 숨기기 + label_crm_note_added: Note added + label_crm_note_read_more: (read more) + label_crm_invoice_import: Import invoices from CSV + + label_deal_plural: ìž…ì°° + label_crm_contractor_plural: ì—°ë½ì²˜ + label_deal: ìž…ì°° + label_crm_deal_new: 새 ìž…ì°° + label_crm_deal_edit_information: ìž…ì°° ì •ë³´ 수정 + label_crm_deal_change_status: ìƒíƒœ 변경 + label_crm_statistics: 통계 + label_crm_deals_import: Import deals from CSV + + label_crm_deal_status_new: ì‹ ê·œ + label_crm_deal_status_first_contact: 첫번째 ì—°ë½ì²˜ + label_crm_deal_status_negotiations: í˜‘ìƒ + label_crm_deal_status_pending: 지연 + label_crm_deal_status_won: 낙찰 + label_crm_deal_status_lost: 유찰 + + label_crm_created_on: ìƒì„± 시기 + + field_note_date: Note date + field_background: 설명 + field_currency: 통화 + field_contact: ì—°ë½ì²˜ + + field_deal_name: ì´ë¦„ + field_deal_background: 설명 + field_deal_contact: ì—°ë½ì²˜ + field_deal_price: 합계 + field_price: 합계 + + field_contact_avatar: 사진 + field_contact_is_company: 회사 + field_contact_name: ì´ë¦„ + field_contact_last_name: 성 + field_contact_first_name: ì´ë¦„ + field_contact_middle_name: 미들 네임 + field_contact_job_title: ì§ì—… + field_contact_company: 회사 + field_contact_address: 주소 + field_contact_phone: 전화번호 + field_contact_email: Email + field_contact_website: 웹페ì´ì§€ + field_contact_skype: Skype + field_contact_status: ìƒíƒœ + field_contact_background: 설명 + field_contact_tag_names: 태그 + field_first_name: ì´ë¦„ + field_last_name: 성 + field_company: 회사 + field_birthday: ìƒì¼ + field_contact_department: 부서 + + + field_company_field: 업종 + + field_color: ìƒ‰ìƒ + + button_add_note: 메모 남기기 + notice_successful_save: 성공ì ìœ¼ë¡œ 저장ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_successful_add: 성공ì ìœ¼ë¡œ ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. + notice_unsuccessful_save: ì €ìž¥ì— ë¬¸ì œê°€ 있습니다. + notice_successful_merged: 성곡ì ìœ¼ë¡œ í•©ì³ì¡ŒìŠµë‹ˆë‹¤. + + notice_merged_warning: 현재 ì—°ë½ì²˜ëŠ” ì‚­ì œë˜ê³  ì´ ì‚¬ëžŒì—게 ë¶€ì—¬ëœ ëª¨ë“  메모, 프로ì íЏ, 태그 그리고 업무는 ì•„ëž˜ì— ì„ íƒëœ 사람으로 ì´ë™ë©ë‹ˆë‹¤. + + project_module_contacts: ì—°ë½ì²˜ + + permission_view_contacts: ì—°ë½ì²˜ 보기 + permission_edit_contacts: ì—°ë½ì²˜ 수정 + permission_delete_contacts: ì—°ë½ì²˜ ì‚­ì œ + permission_view_deals: ìž…ì°° 보기 + permission_edit_deals: ìž…ì°° 수정 + permission_delete_deals: ìž…ì°° ì‚­ì œ + permission_add_notes: 노트 추가 + permission_delete_notes: 노트 ì‚­ì œ + permission_delete_own_notes: ìžì‹ ì˜ 노트 ì‚­ì œ + + # 2.0.0 + label_crm_deal_category: ìž…ì°° 카테고리 + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: 새 카테고리 + text_deal_category_destroy_assignments: Remove category assignments + text_deal_category_destroy_question: "Some deals (%{count}) are assigned to this category. What do you want to do?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'ì„ íƒëœ ìž…ì°°ì„ ì‚­ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ??' + label_crm_deal_status_plural: ìž…ì°° ìƒíƒœ + label_crm_deal_status: ìž…ì°° ìƒíƒœ + field_deal_status_is_closed: Closed + label_crm_deal_status_new: New + permission_manage_contacts: ìž…ì°° 리스트 보기 + label_crm_sales_funnel: Sales funnel + label_crm_period: 기간 + label_crm_count: Count + + #2.0.1 + label_crm_user_format: ì´ë¦„ 표현 í˜•ì‹ + label_crm_my_contact_plural: Contacts assigned to me + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: 모든 ì—°ë½ì²˜ 보기 + label_crm_deal_view_all: View all deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edit all selected contacts + label_crm_bulk_edit_selected_deals: Edit all selected deals + label_crm_bulk_send_mail_selected_contacts: ì„ íƒëœ ì—°ë½ì²˜ë¡œ ë©”ì¼ ë³´ë‚´ê¸° + field_add_tags: 태그 추가 + field_delete_tags: 태그 지우기 + label_crm_send_mail: ë©”ì¼ ë³´ë‚´ê¸° + error_empty_email: ë©”ì¼ ë‚´ìš©ì´ ì—†ìŠµë‹ˆë‹¤. + permission_send_contacts_mail: ë©”ì¼ ë³´ë‚´ê¸° + field_mail_from: 보내는 사람 + text_email_macros: 가능한 ë©”í¬ë¡œ %{macro} + field_message: 메시지 + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: ì—°ë½ì²˜ + field_age: ë‚˜ì´ + label_crm_vcf_import: Import from vCard + label_crm_mail_from: 부터 + permission_import_contacts: Import contacts + + #2.1.0 + field_company_name: 회사 ì´ë¦„ + label_crm_recently_added_contacts: 최근 ì¶”ê°€ëœ ì—°ë½ì²˜ + label_crm_created_by_me: ë‚´ê°€ 추가한 ì—°ë½ì²˜ + my_contacts: ë‚´ ì—°ë½ì²˜ + my_deals: ë‚´ ìž…ì°° + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Call + label_crm_note_type_meeting: 미팅 + field_deal_currency: 통화 + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contacts created + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Add into + label_crm_delete_from: Delete from + label_crm_show_deaks_tab: Show deals tab + label_crm_show_on_projects_show: Show contacts on projects overview + + #2.2.1 + label_crm_contacts_show_in_list: Show in list + + #2.3.0 + label_crm_module_plural: 모듈 + label_crm_list_partial_style: ì—°ë½ì²˜ 표시 ë°©ì‹ + label_crm_list_excerpt: ì¸ìš© 리스트 + label_crm_list_cards: Cards + label_crm_list_list: Table + field_contacts: ì—°ë½ì²˜ + field_companies: 회사 + label_crm_added_by: ìž‘ì„±ìž - + label_crm_contact_note_authoring_time: 메모 시간보기 + label_crm_contact_issues_filters: Issues filters + label_crm_csv_import: Import from CSV + label_crm_upload_encoding: íŒŒì¼ ì¸ì½”딩 + label_crm_csv_file: CSV file + label_crm_csv_separator: Separator + field_middle_name: Middle Name + field_job_title: ì§ì—… + field_company: 회사 + field_address: 주소 + field_phone: 전화번호 + field_email: Email + field_tags: 태그 + field_last_note: 최근 노트 + field_is_company: 회사만 + field_contact_full_name: ì´ë¦„ + button_contacts_edit_query: 쿼리 수정 + button_contacts_delete_query: 쿼리 ì‚­ì œ + permission_manage_public_contacts_queries: 공개 쿼리 + permission_add_deals: ìž…ì°° 추가 + permission_add_contacts: ì—°ë½ì²˜ 추가 + permission_save_contacts_queries: 쿼리 저장 + + #2.3.3 + label_crm_contact_show_in_app_menu: 탭으로 보기 + + #2.3.4 + label_crm_contact_show_closed_issues: ì™„ë£Œëœ ì´ìŠˆë³´ê¸° + + #3.0.0 + label_crm_import: Import + label_contact_note_plural: Contacts notes + label_deal_note_plural: Deal notes + label_crm_contact_all_note_plural: All notes + error_unable_delete_deal_status: Unable delete deal status + label_crm_contacts_hidden: Hidden settings + + #3.1.0 + label_crm_contact_added: Contact added + label_crm_note_added: Note added + label_crm_deal_added: Deal added + label_crm_deal_updated: Deal updated + text_crm_contact_added: "%{name} ì—°ë½ì²˜ëŠ” %{author}ì— ì˜í•´ 추가ë˜ì—ˆìŠµë‹ˆë‹¤.." + text_crm_deal_added: "%{name} ìž…ì°°ì€ %{author}ì— ì˜í•´ 추가ë˜ì—ˆìŠµë‹ˆë‹¤.." + text_crm_deal_status_changed: "ìž…ì°° ìƒíƒœê°€ %{old} ì—서 %{new}로 변경ë˜ì—ˆìŠµë‹ˆë‹¤." + text_crm_deal_updated: "%{name}ìž…ì°°ì€ %{author}ì— ì˜í•´ ì—…ë°ì´íЏ ë˜ì—ˆìŠµë‹ˆë‹¤.." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: CSV 내보내기 + permission_import_deals: Import deals + label_crm_single_quotes: "Single quotes (')" + label_crm_double_quotes: "Double quotes (\")" + label_crm_quotes_type: ì¸ìš©ë¬¸êµ¬ 타입 + label_crm_contacts_visibility: 공개여부 + label_crm_contacts_visibility_project: 프로ì íŠ¸ì— í•œí•´ì„œë§Œ 공개 + label_crm_contacts_visibility_public: 전체공개 + label_crm_contacts_visibility_private: 비공개 + permission_view_private_contacts: ê°œì¸ ì—°ë½ì²˜ 보기 + text_crm_error_on_line: 'Error on line %{line}: %{error}.' + + #3.2.0 + label_crm_probability: 확률 + label_crm_deal_status_type: 타입 ìƒíƒœ + label_crm_select_companies: 회사를 입찰대ìƒìœ¼ë¡œ í¬í•¨ + label_crm_expected_revenue: ì˜ˆìƒ ìˆ˜ìµ + label_crm_deal_due_date: ë§Œê¸°ì¼ \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/nl.yml b/plugins/redmine_contacts/config/locales/nl.yml new file mode 100644 index 0000000..8de9957 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/nl.yml @@ -0,0 +1,142 @@ +# encoding: utf-8 +nl: + contacts_title: Contacten + + label_crm_recently_viewed: Recent bekeken + label_crm_gravatar_enabled: Gebruik Gravatar + label_crm_view_all_contacts: Bekijk alle contacten + label_crm_background_info: Achtergrondinformatie + label_crm_company: Bedrijf + label_contact_plural: Contactenlijst + label_crm_contact_edit_information: Contactinformatie bewerken + label_crm_edit_tags: Tags bewerken + label_crm_contact_view: Bekijken + label_crm_contact_list: Lijst + label_crm_contact_new: Nieuw + label_crm_at_company: van + label_crm_last_notes: Laatste notities + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Selecteer meerdere tags + label_crm_single_tag_mode: Enkele tag + label_crm_multiple_tags_mode: Meerdere tags + label_crm_contact_tag: Tag + label_crm_time_ago: geleden + label_crm_add_note_plural: Notitie toevoegen aan + label_crm_note_plural: Notities + + label_crm_add_tags_rule: scheiden op comma + label_crm_contact_search: Zoeken op naam + label_crm_note_for: Notitie voor + label_crm_show_on_map: Toon op kaart + label_crm_add_another_phone: telefoon toevoegen + label_contact_note_plural: Alle notities + label_crm_remove: verwijderen + label_crm_related_contacts: Gerelateerde contacten + label_crm_assigned_to: Verantwoordelijk + label_crm_issue_added: Issue toegevoegd + + label_crm_note_show_extras: Geavanceerd (bestanden, datum) + label_crm_note_hide_extras: Geavanceerd verbergen + label_crm_note_added: Notitie succesvol toegevoegd + + label_deal_plural: Deals + label_crm_contractor_plural: Contractors + label_deal: Deal + label_crm_deal_new: Nieuwe deal + label_crm_deal_edit_information: Deal bewerken + label_crm_deal_change_status: Status veranderen + label_crm_statistics: Statistieken + + field_note_date: Notitiedatum + + field_deal_name: Naam + field_deal_background: Achtergrond + field_deal_contact: Contact + field_deal_price: Som + field_price: Som + + field_contact_avatar: Avatar + field_contact_is_company: Bedrijf + field_contact_name: Naam + field_contact_last_name: Achternaam + field_contact_first_name: Voornaam + field_contact_middle_name: Tussenvoegsel + field_contact_job_title: Functie + field_contact_company: Bedrijf + field_contact_address: Adres + field_contact_phone: Telefoon + field_contact_email: E-Mail + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Achtergrond + field_contact_tag_names: Tags + field_first_name: Name + field_last_name: Achternaam + field_company: Bedrijf + field_birthday: Geboortedatum + field_contact_department: Afdeling + + field_company_field: Branche + + button_add_note: Notitie toevoegen + notice_successful_save: Succesvol opgeslagen + notice_successful_add: Succesvol aangemaakt + notice_unsuccessful_save: Succesvol opgeslagen + + project_module_contacts: Contacten + + permission_view_contacts: Contacten bekijken + permission_edit_contacts: Contacten bewerken + permission_delete_contacts: Contacten verwijderen + permission_view_deals: Deals bekijken + permission_edit_deals: Deals bewerken + permission_delete_deals: Deals verwijderen + permission_add_notes: Notities toevoegen + permission_delete_notes: Verwijder notities + permission_delete_own_notes: Verwijder eigen notities + + # 2.0.0 + label_crm_deal_category: Deal categorie + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: Nieuwe categorie + text_deal_category_destroy_assignments: Verwijder categorie toewijzingen + text_deal_category_destroy_question: "Sommige deals (%{count}) vallen onder deze categorie. Wat wil je doen?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'Weet je zeker dat je de geselecteerde deal(s) wilt verwijderen?' + label_crm_deal_status_plural: Deal statuses + label_crm_deal_status: Deal status + field_deal_status_is_closed: Gesloten + label_crm_deal_status_new: Nieuw + permission_manage_contacts: Enumerations + label_crm_sales_funnel: Sales funnel + label_crm_period: Periode + label_crm_count: Aantal + + #2.0.1 + label_crm_user_format: Contact naam formaat + label_crm_my_contact_plural: Contacten aan mij toegewezen + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: Bekijk alle contacts + label_crm_deal_view_all: Bekijk alle deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Bewerk alle geselecteerde contacten + label_crm_bulk_edit_selected_deals: Bewerk alle geselecteerde deals + label_crm_bulk_send_mail_selected_contacts: Stuur mail naar geselecteerde contacten + field_add_tags: Voeg tags toe + field_delete_tags: Verwijder tags + label_crm_send_mail: Stuur mail + error_empty_email: Email mag niet leeg zijn + permission_send_contacts_mail: Stuur mail + field_mail_from: Adres afzender + text_email_macros: Beschikbare macro's %{macro} + field_message: Bericht + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Contact + field_age: Leeftijd + label_crm_vcf_import: Importeer vCard + label_crm_mail_from: Afzender + permission_import_contacts: Importeer contacten \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/no.yml b/plugins/redmine_contacts/config/locales/no.yml new file mode 100755 index 0000000..d468109 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/no.yml @@ -0,0 +1,189 @@ +# encoding: utf-8 +"no": + contacts_title: Contacts + + label_crm_recently_viewed: Recently viewed + label_crm_gravatar_enabled: Use Gravatar + label_crm_thumbnails_enabled: Show image thumbnails in notes + label_crm_max_thumbnail_file_size: Max thumbnailed image size + label_crm_view_all_contacts: View all contacts + label_crm_background_info: Background info + label_crm_company: Company + label_contact_plural: Contacts + label_crm_contact_edit_information: Editing Contact Information + label_crm_edit_tags: Edit tags + label_crm_contact_view: View + label_crm_contact_list: List + label_crm_contact_new: New + label_crm_at_company: at + label_crm_last_notes: Latest notes + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Select multilpe tags + label_crm_single_tag_mode: Single tag + label_crm_multiple_tags_mode: Multiple tags + label_crm_contact_tag: Tag + label_crm_time_ago: ago + label_crm_add_note_plural: Add note to + label_crm_note_plural: Notes + + label_crm_add_tags_rule: devide by commas + label_crm_contact_search: Search by name + label_crm_note_for: Note for + label_crm_show_on_map: Show on map + label_crm_add_another_phone: add phone + label_contact_note_plural: All notes + label_crm_remove: delete + label_crm_related_contacts: Related contacts + label_crm_assigned_to: Responsible + label_crm_issue_added: Issue added + label_crm_add_emails_rule: divide by commas + label_crm_add_phones_rule: divide by commas + label_crm_add_employee: New employee + label_crm_merge_duplicate_plural: Merge + label_crm_duplicate_plural: Possible duplicates + label_crm_duplicate_for_plural: Possible duplicates for + label_crm_add_tag: Add new... + + label_crm_note_show_extras: Advanced (type, date, files) + label_crm_note_hide_extras: Hide advanced + label_crm_note_added: The note is successfully added + label_crm_note_read_more: (read more) + + label_deal_plural: Deals + label_crm_contractor_plural: Contactors + label_deal: Deal + label_crm_deal_new: New deal + label_crm_deal_edit_information: Edit deal information + label_crm_deal_change_status: Change status + label_crm_statistics: Statistics + + label_crm_deal_status_new: New + label_crm_deal_status_first_contact: First contact + label_crm_deal_status_negotiations: Negotiations + label_crm_deal_status_pending: Pending + label_crm_deal_status_won: Won + label_crm_deal_status_lost: Lost + + label_crm_created_on: Created on + + field_note_date: Note date + + field_deal_name: Name + field_deal_background: Background + field_deal_contact: Contact + field_deal_price: Sum + field_price: Sum + + field_contact_avatar: Avatar + field_contact_is_company: Company + field_contact_name: Name + field_contact_last_name: Last Name + field_contact_first_name: First Name + field_contact_middle_name: Middle Name + field_contact_job_title: Job title + field_contact_company: Company + field_contact_address: Address + field_contact_phone: Phone + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Background + field_contact_tag_names: Tags + field_first_name: Name + field_last_name: Last name + field_company: Company + field_birthday: Birthday + field_contact_department: Department + + + field_company_field: Industry + + field_color: Color + + button_add_note: Add note + notice_successful_save: Saved successfully + notice_successful_add: Successfully created + notice_unsuccessful_save: Save problems + notice_successful_merged: Successfully merged + + notice_merged_warning: All the notes, projects, tags and tasks attached to this person will be moved to choosed below. The contact will then be deleted. + + project_module_contacts: Contacts + + permission_view_contacts: View contacts + permission_edit_contacts: Edit contacts + permission_delete_contacts: Delete contacts + permission_view_deals: View deals + permission_edit_deals: Edit deals + permission_delete_deals: Delete deals + permission_add_notes: Add notes + permission_delete_notes: Delete notes + permission_delete_own_notes: Delete own notes + + # 2.0.0 + label_crm_deal_category: Deal category + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: New category + text_deal_category_destroy_assignments: Remove category assignments + text_deal_category_destroy_question: "Some deals (%{count}) are assigned to this category. What do you want to do?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'Are you sure you want to delete the selected deal(s)?' + label_crm_deal_status_plural: Deal statuses + label_crm_deal_status: Deal status + field_deal_status_is_closed: Closed + label_crm_deal_status_new: New + permission_manage_contacts: Enumerations + label_crm_sales_funnel: Sales funnel + label_crm_period: Period + label_crm_count: Count + + #2.0.1 + label_crm_user_format: Contact name format + label_crm_my_contact_plural: Contacts assigned to me + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: View all contacts + label_crm_deal_view_all: View all deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edit all selected contacts + label_crm_bulk_edit_selected_deals: Edit all selected deals + label_crm_bulk_send_mail_selected_contacts: Send mail to selected contacts + field_add_tags: Add tags + field_delete_tags: Delete tags + label_crm_send_mail: Send mail + error_empty_email: Email can not be blank + permission_send_contacts_mail: Send mail + field_mail_from: From address + text_email_macros: Avaliable macros %{macro} + field_message: Message + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Contact + field_age: Age + label_crm_vcf_import: Import from vCard + label_crm_mail_from: From + permission_import_contacts: Import contacts + + #2.1.0 + field_company_name: Company name + label_crm_recently_added_contacts: Recently added contacts + label_crm_created_by_me: Contacts created by me + my_contacts: My contacts + my_deals: My deals + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Call + label_crm_note_type_meeting: Meeting + field_deal_currency: Currency + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contacts created + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Add into + label_crm_delete_from: Delete from + label_crm_show_deaks_tab: Show deals tab + label_crm_show_on_projects_show: Show contacts on projects overview \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/pl.yml b/plugins/redmine_contacts/config/locales/pl.yml new file mode 100644 index 0000000..79d6c4b --- /dev/null +++ b/plugins/redmine_contacts/config/locales/pl.yml @@ -0,0 +1,585 @@ +# encoding: utf-8 +pl: + contacts_title: Kontakty + + label_crm_recently_viewed: Ostatnio oglÄ…dane + label_crm_gravatar_enabled: Używaj Gravatar + label_crm_thumbnails_enabled: Pokazuj miniatury w notkach + label_crm_max_thumbnail_file_size: Maksymalny rozmiar miniatury + label_crm_view_all_contacts: WyÅ›wietl wszystkie kontakty + label_crm_background_info: Informacje dodatkowe + label_contact: Osoba + label_crm_company: Firma + label_contact_plural: Kontakty + label_crm_contact_edit_information: Edycja informacji o kontaktach + label_crm_edit_tags: Edycja tagów + label_crm_contact_view: Pokaż + label_crm_contact_list: Wypisz + label_crm_contact_new: Nowy + label_crm_at_company: z + label_crm_last_notes: Najnowsze notatki + label_crm_tags_plural: Tagi + label_crm_multi_tags_plural: Wybierz wiele tagów + label_crm_single_tag_mode: Pojedynczy tag + label_crm_multiple_tags_mode: Wiele tagów + label_crm_contact_tag: Tag + label_crm_time_ago: temu + label_crm_add_note_plural: Dodaj notatkÄ™ + label_crm_note_plural: Notatki + + label_crm_add_tags_rule: oddziel przecinkami + label_crm_contact_search: Wyszukaj po nazwie + label_crm_note_for: Notka dla + label_crm_show_on_map: Pokaż na mapie + label_crm_add_another_phone: dodaj telefon + label_contact_note_plural: Wszystkie notki + label_crm_remove: usuÅ„ + label_crm_related_contacts: PowiÄ…zane kontakty + label_crm_assigned_to: Odpowiedzialny + label_crm_issue_added: KwestiÄ™ dodano + label_crm_add_emails_rule: oddziel przecinkami + label_crm_add_phones_rule: oddziel przecinkami + label_crm_add_employee: Nowy zatrudniony + label_crm_merge_duplicate_plural: Scal + label_crm_duplicate_plural: Możliwe duplikaty + label_crm_duplicate_for_plural: Możliwe duplikaty dla + label_crm_add_tag: Dodaj nowy... + + label_crm_note_show_extras: Zaawansowane (pliki, data) + label_crm_note_hide_extras: Ukryj zaawansowane + label_crm_note_added: PomyÅ›lnie dodano notkÄ™ + label_crm_note_read_more: (czytaj dalej) + + label_deal_plural: Oferty + label_crm_contractor_plural: Kontrahenci + label_deal: Oferta + label_crm_deal_new: Nowa oferta + label_crm_deal_edit_information: Edytuj informacjÄ™ oferty + label_crm_deal_change_status: ZmieÅ„ status + label_crm_statistics: Statystyki + + label_crm_deal_status_new: Nowy + label_crm_deal_status_first_contact: Pierwszy kontakt + label_crm_deal_status_negotiations: Negocjacje + label_crm_deal_status_pending: OczekujÄ…ca + label_crm_deal_status_won: Wygrana + label_crm_deal_status_lost: Przegrana + + label_crm_created_on: Utworzono + + field_note_date: Data notatki + + field_deal_name: Nazwa + field_deal_background: Dodatkowe + field_deal_contact: Kontakt + field_deal_price: Suma + field_price: Suma + + field_contact_avatar: Logo + field_contact_is_company: Firma + field_contact_name: Nazwa + field_contact_last_name: Nazwisko + field_contact_first_name: ImiÄ™ + field_contact_middle_name: DrugiÄ™ imiÄ™ + field_contact_job_title: Stanowisko + field_contact_company: Firma + field_contact_address: Adres + field_contact_phone: Telefon + field_contact_email: Email + field_contact_website: Strona WWW + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Dodatkowe + field_contact_tag_names: Tagi + field_first_name: ImiÄ™ + field_last_name: Nazwisko + field_company: Firma + field_birthday: Urodziny + field_contact_department: DziaÅ‚ + + + field_company_field: Branża + + field_color: Kolor + + button_add_note: Dodaj notkÄ™ + notice_successful_save: PomyÅ›lnie zapisano + notice_successful_add: PomyÅ›lnie dodano + notice_unsuccessful_save: WystÄ…piÅ‚y problemy z zapisem + notice_successful_merged: PomyÅ›lnie scalono + + notice_merged_warning: 'Wszystkie notki, projekty, tagi i zadania przydzielone do tej osoby zostanÄ… przeniesione do wybranej poniżej. Kontakt zostanie usuniÄ™ty.' + + project_module_contacts: kontakty + + permission_view_contacts: WyÅ›wietlanie kontaktów + permission_edit_contacts: Edycja kontaktów + permission_delete_contacts: Usuwanie kontaktów + permission_view_deals: WyÅ›wietlanie ofert + permission_edit_deals: Edycja ofert + permission_delete_deals: Usuwanie ofert + permission_add_notes: Dodawanie notek + permission_delete_notes: Usuwanie notek + permission_delete_own_notes: Usuwanie wÅ‚asnych notek + + # 2.0.0 + label_crm_deal_category: Kategoria ofert + label_crm_deal_category_plural: Kategorie ofert + label_crm_deal_category_new: Nowa kategoria + text_deal_category_destroy_assignments: UsuÅ„ przypisania kategorii + text_deal_category_destroy_question: "Pewne oferty (%{count}) SÄ… przydzielone do tej kategorii. Co chcesz zrobić?" + text_deal_category_reassign_to: ZmieÅ„ przypisanie na nastÄ™pujÄ…cÄ… kategoriÄ™ + text_deals_destroy_confirmation: 'Czy jesteÅ› pewien, że chcesz usunąć wybranÄ…/e ofertÄ™/y?' + label_crm_deal_status_plural: Statusy ofert + label_crm_deal_status: Status oferty + field_deal_status_is_closed: ZamkniÄ™ta + label_crm_deal_status_new: Nowy + permission_manage_contacts: ZarzÄ…dzanie kontaktami + label_crm_sales_funnel: Lejek sprzedaży + label_crm_period: Okres + label_crm_count: Ilość + + #2.0.1 + label_crm_user_format: Format nazwy kontaktu + label_crm_my_contact_plural: Kontakty przydzielone do mnie + label_crm_my_deal_plural: Otwarte oferty przydzielone do mnie + label_crm_contact_view_all: WyÅ›wietl wszystkie kontakty + label_crm_deal_view_all: WyÅ›wietl wszystkie oferty + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edytuj wszystkie zaznaczone kontakty + label_crm_bulk_send_mail_selected_contacts: WyÅ›lij maila do wszystkich zaznaczonych kontaktów + field_add_tags: Dodaj tagi + field_delete_tags: UsuÅ„ tagi + label_crm_send_mail: WyÅ›lij maila + error_empty_email: Email nie może być pusty + permission_send_contacts_mail: WysyÅ‚anie maili + field_mail_from: Od + text_email_macros: DostÄ™pne makra %{macro} + +#2.0.3 + label_crm_add_contact: Dodaj kontakt + label_contact: Kontakt + field_age: Age + label_crm_vcf_import: Import from vCard + label_crm_mail_from: From + permission_import_contacts: Importuj kontakty + + #2.1.0 + field_company_name: Nazwa firmy + label_crm_recently_added_contacts: Recently added contacts + label_crm_created_by_me: Contacts created by me + my_contacts: My contacts + my_deals: My deals + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Telefon + label_crm_note_type_meeting: Spotkanie + field_deal_currency: Waluta + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contacts created + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Add into + label_crm_delete_from: Delete from + label_crm_show_deaks_tab: Show deals tab + label_crm_show_on_projects_show: Show contacts on projects overview + + #2.2.1 + label_crm_contacts_show_in_list: Show in list + +#2.3.0 + label_crm_module_plural: ModuÅ‚y + label_crm_list_partial_style: List style + label_crm_list_excerpt: Excerpt list + label_crm_list_cards: Cards + label_crm_list_list: Table + field_contacts: Contact + field_companies: Company + label_crm_added_by: Added by + label_crm_contact_note_authoring_time: Show note time + label_crm_contact_issues_filters: Issues filters + label_crm_csv_import: Import from CSV + label_crm_upload_encoding: File encoding + label_crm_csv_file: CSV file + label_crm_csv_separator: Separator + field_middle_name: Middle Name + field_job_title: Job title + field_company: Company + field_address: Adres + field_phone: Telefon + field_email: Email + field_tags: Tagi + field_last_note: Last note + field_is_company: Is company + field_contact_full_name: Full name + button_contacts_edit_query: Edit query + button_contacts_delete_query: Delete query + permission_manage_public_contacts_queries: Manage public queries + permission_add_deals: Add deals + permission_add_contacts: Add contacts + permission_save_contacts_queries: Save queries + + #2.3.3 + label_crm_contact_show_in_app_menu: Show tabs in app menu + + label_crm_contact_show_closed_issues: Show closed issues + + #3.0.0 + label_crm_import: Import + label_contact_note_plural: Contacts notes + label_deal_note_plural: Deal notes + label_crm_contact_all_note_plural: Wszystkie notatki + error_unable_delete_deal_status: Unable to delete deal status + label_crm_contacts_hidden: Hidden settings + + #3.1.0 + label_crm_contact_added: Dodano kontakt + label_crm_note_added: Dodano notatkÄ™ + label_crm_deal_added: Deal added + label_crm_deal_updated: Deal updated + text_crm_contact_added: "Contact %{name} has been added by %{author}." + text_crm_deal_added: "Deal %{name} has been added by %{author}." + text_crm_deal_status_changed: "Deal status changed from %{old} to %{new}" + text_crm_deal_updated: "Deal %{name} has been updated by %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Import from CSV + permission_import_deals: Import deals + label_crm_single_quotes: "Single quotes (')" + label_crm_double_quotes: "Double quotes (\")" + label_crm_quotes_type: Quotation marks type + label_crm_contacts_visibility: Widzialność + label_crm_contacts_visibility_project: Wg uprawnieÅ„ projektowych + label_crm_contacts_visibility_public: Publiczna + label_crm_contacts_visibility_private: Prywatna + permission_view_private_contacts: View private contacts + text_crm_error_on_line: 'Error on line %{line}: %{error}.' + +#3.2.0 + label_crm_probability: Probability + label_crm_deal_status_type: Status type + label_crm_select_companies: Filter companies in deal + label_crm_expected_revenue: Expected revenue + label_crm_deal_due_date: Due date + + #3.2.2 + label_crm_show_deals_in_top_menu: Show deals in top menu + label_crm_show_details: Pokaż szczegóły + label_crm_has_deals: Has deals + label_crm_has_open_issues: Open issues + label_crm_note: Notatka + + #3.2.5 + notice_failed_to_save_contacts: "Failed to save %{count} contact(s) on %{total} selected: %{ids}." + + #3.2.6 + project_module_deals: Deals + permission_manage_deals: Manage deals + label_crm_deals_from_subprojects: Show deals from subprojects + label_crm_megre_tags: Merge tags + label_crm_monochrome_tags: Monochrome tags + +#3.2.7 + label_crm_address: Adres + label_crm_street1: Ulica 1 + label_crm_street2: Ulica 2 + label_crm_city: Miasto + label_crm_region: 'województwo' + label_crm_postcode: 'KOD' + label_crm_country: Kraj + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: Cuba + CY: Cyprus + CZ: Czech Republic + CI: Côte d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong SAR China + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: Mexico + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: Polska + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: Russia + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + KR: South Korea + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syria + ST: São Tomé and Príncipe + TW: Taiwan + TJ: Tajikistan + TZ: Tanzania + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + SU: Union of Soviet Socialist Republics + AE: United Arab Emirates + GB: United Kingdom + US: United States + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: Vietnam + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land Islands + label_crm_cross_project_contacts: Pozwól na relacje kontaktów miÄ™dzy projektami + label_crm_list_board: Board + label_crm_default_list_style: Default list style + label_crm_show_in_top_menu: Pokaż w górnym menu + label_crm_show_in_app_menu: Pokaż w menu aplikacji + label_crm_money_settings: PieniÄ…dze + label_crm_disable_taxes: Wyłącz podatki + label_crm_default_tax: DomyÅ›lna stawka podatku + label_crm_tax_type: Typ podatku + label_crm_tax_type_inclusive: Tax inclusive + label_crm_tax_type_exclusive: Tax exclusive + label_crm_default_currency: DomyÅ›lna waluta + label_crm_thousands_delimiter: Separator tysiÄ™cy + label_crm_decimal_separator: Separator dziesiÄ™tnych + + #3.2.10 + label_crm_add_contact_plural: Add contacts + label_crm_search_for_contact: Search for contact + label_crm_major_currencies: Główne waluty + + + #3.2.11 + label_crm_post_address_format: Format adresu pocztowego + label_crm_post_address_format_macros: "Marka adresowe: %{macros}" + + #3.2.14 + label_crm_last_year: rok poprzedni \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/pt-BR.yml b/plugins/redmine_contacts/config/locales/pt-BR.yml new file mode 100644 index 0000000..31703bc --- /dev/null +++ b/plugins/redmine_contacts/config/locales/pt-BR.yml @@ -0,0 +1,607 @@ +# encoding: utf-8 +pt-BR: + contacts_title: Contatos + + label_crm_recently_viewed: Recentemente visto + label_crm_gravatar_enabled: Usar Gravatar + label_crm_thumbnails_enabled: Exibir miniaturas nas notas + label_crm_max_thumbnail_file_size: Tamanho máximo da miniatura + label_crm_view_all_contacts: Ver todos os contatos + label_crm_background_info: Informação de background + label_crm_company: Empresa + label_contact_plural: Contatos + label_crm_contact_edit_information: A editar informação do Contato + label_crm_edit_tags: Editar tags + label_crm_contact_view: Ver + label_crm_contact_list: Lista + label_crm_contact_new: Novo + label_crm_at_company: em + label_crm_last_notes: Últimas notas + label_crm_tags_plural: Tags + label_crm_multi_tags_plural: Seleccionar múltiplas tags + label_crm_single_tag_mode: Tag individual + label_crm_multiple_tags_mode: Tags múltiplas + label_crm_contact_tag: Tag + label_crm_time_ago: atrás + label_crm_add_note_plural: Adicionar notas + label_crm_note_plural: Notas + + label_crm_add_tags_rule: separar por vírgulas + label_crm_contact_search: Buscar por nome + label_crm_note_for: Nota para + label_crm_show_on_map: Mostrar no mapa + label_crm_add_another_phone: adicionar telefone + label_crm_remove: apagar + label_crm_related_contacts: Contatos relacionados + label_crm_assigned_to: Responsável + label_crm_issue_added: Tarefa adicionada + label_crm_add_emails_rule: separar por vírgulas + label_crm_add_phones_rule: separar por vírgulas + label_crm_add_employee: Novo funcionário + label_crm_merge_duplicate_plural: Mesclar + label_crm_duplicate_plural: Possíveis duplicações + label_crm_duplicate_for_plural: Possíveis duplicações para + label_crm_add_tag: Adicionar novo... + + label_crm_note_show_extras: Avançado (ficheiros, data) + label_crm_note_hide_extras: Esconder avançado + label_crm_note_added: A nota foi acrescentada com sucesso + label_crm_note_read_more: (leia mais) + label_crm_invoice_import: Import invoices from CSV + + label_deal_plural: Acordos + label_crm_contractor_plural: Contratores + label_deal: Acordo + label_crm_deal_new: Novo acordo + label_crm_deal_edit_information: Editar informação do acordo + label_crm_deal_change_status: Alterar estado + label_crm_statistics: Estatísticas + label_crm_deals_import: Import deals from CSV + + label_crm_deal_status_new: Novo + label_crm_deal_status_first_contact: Primeiro contato + label_crm_deal_status_negotiations: Negociações + label_crm_deal_status_pending: Pendente + label_crm_deal_status_won: Venceu + label_crm_deal_status_lost: Perdido + + label_crm_created_on: Criado em + + field_note_date: Data de nota + field_background: Background + field_currency: Moeda + field_contact: Contato + + field_deal_name: Nome + field_deal_background: Expreriência + field_deal_contact: Contato + field_deal_price: Soma + field_price: Soma + + field_contact_avatar: Avatar + field_contact_is_company: Empresa + field_contact_name: Nome + field_contact_last_name: Último Nome + field_contact_first_name: Primeiro Nome + field_contact_middle_name: Nome do Meio + field_contact_job_title: Título de emprego + field_contact_company: Empresa + field_contact_address: Morada + field_contact_phone: Telefone + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Estado + field_contact_background: Expreriência + field_contact_tag_names: Tags + field_first_name: Nome + field_last_name: Último nome + field_company: Empresa + field_birthday: Data de nascimento + field_contact_department: Departamento + + + field_company_field: Indústria + + field_color: Cor + + button_add_note: Adicionar nota + notice_successful_save: Gravado com sucesso + notice_successful_add: Criado com sucesso + notice_unsuccessful_save: Problemas ao gravar + notice_successful_merged: Mesclado com sucesso + + notice_merged_warning: Todas as notas, projetos, tags e tarefas associadas a esta pessoa será movido para choosed abaixo. O contato será então excluído. + + project_module_contacts: Contatos + + permission_view_contacts: Ver contatos + permission_edit_contacts: Editar contatos + permission_delete_contacts: Apagar contatos + permission_view_deals: Ver acordos + permission_edit_deals: Editar acordos + permission_delete_deals: Apagar acordos + permission_add_notes: Adicionar notas + permission_delete_notes: Apagar notas + permission_delete_own_notes: Apagar notas pessoais + + # 2.0.0 + label_crm_deal_category: Categoria do negócio + label_crm_deal_category_plural: Categorias dos negócios + label_crm_deal_category_new: Nova categoria + text_deal_category_destroy_assignments: Remove atribuições da categoria + text_deal_category_destroy_question: "Alguns acordos (% {count}) estão atribuídos a esta categoria. O que você quer fazer?" + text_deal_category_reassign_to: Reatribuir acordos a esta categoria + text_deals_destroy_confirmation: 'Tem certeza de que deseja excluir o(s) acordo(s) selecionado(s)?' + label_crm_deal_status_plural: Situação do acordos + label_crm_deal_status: Situação do acordo + field_deal_status_is_closed: Fechado + label_crm_deal_status_new: Novo + permission_manage_contacts: Enumeradores + label_crm_sales_funnel: Fúnil de vendas + label_crm_period: Período + label_crm_count: Contagem + + #2.0.1 + label_crm_user_format: Formato do nome do contato + label_crm_my_contact_plural: Contatos atribuídos a mim + label_crm_my_deal_plural: Acordos aberto atribuídos a mim + label_crm_contact_view_all: Ver todos os contatos + label_crm_deal_view_all: Ver todos os acordos + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Editar todos os contatos selecionados + label_crm_bulk_edit_selected_deals: Editar todos os acordos selecionados + label_crm_bulk_send_mail_selected_contacts: Enviar mensagem para os contatos selecionados + field_add_tags: Adicionar tags + field_delete_tags: Apagar tags + label_crm_send_mail: Enviar mensagem + error_empty_email: Email não pode estar em branco + permission_send_contacts_mail: Enviar mensagem + field_mail_from: De + text_email_macros: Macros disponíveis %{macro} + field_message: Mensagem + + #2.0.3 + label_crm_add_contact: Adicionar contato + label_contact: Pessoa + field_age: Idade + label_crm_vcf_import: Import from vCard + label_crm_mail_from: De + permission_import_contacts: Import contacts + + #2.1.0 + field_company_name: Nome da empresa + label_crm_recently_added_contacts: Contatos adicionados recentemente + label_crm_created_by_me: Contatos criado por min + my_contacts: Meus Contatos + my_deals: Minhas negociação + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Ligação + label_crm_note_type_meeting: Reunião + field_deal_currency: Moeda + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contatos criado + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Adicionar em + label_crm_delete_from: Excluir da + label_crm_show_deaks_tab: Mostrar guia de negocio + label_crm_show_on_projects_show: Show contacts on projects overview + + #2.2.1 + label_crm_contacts_show_in_list: Ver em lista + + #2.3.0 + label_crm_module_plural: Módulos + label_crm_list_partial_style: Estilo de lista + label_crm_list_excerpt: Excerpt list + label_crm_list_cards: Cards + label_crm_list_list: Table + field_contacts: Contato + field_companies: Company + label_crm_added_by: Adicionado por + label_crm_contact_note_authoring_time: Mostrar a hora na nota + label_crm_contact_issues_filters: Issues filters + label_crm_csv_import: Import from CSV + label_crm_upload_encoding: File encoding + label_crm_csv_file: CSV file + label_crm_csv_separator: Separator + field_middle_name: Nome do meio + field_job_title: Cargo + field_company: Company + field_address: Endereço + field_phone: Telefone + field_email: Email + field_tags: Tags + field_last_note: Última nota + field_is_company: é empresa + field_contact_full_name: Nome Completo + button_contacts_edit_query: Editar + button_contacts_delete_query: Excluir + permission_manage_public_contacts_queries: Manage public queries + permission_add_deals: Adicionar Acordos + permission_add_contacts: Add contacts + permission_save_contacts_queries: Save queries + + #2.3.3 + label_crm_contact_show_in_app_menu: Show tabs in app menu + + #2.3.4 + label_crm_contact_show_closed_issues: Show closed issues + + #3.0.0 + label_crm_import: Import + label_contact_note_plural: Todas as notas + label_deal_note_plural: Deal notes + label_crm_contact_all_note_plural: All notes + error_unable_delete_deal_status: Unable delete deal status + label_crm_contacts_hidden: Hidden settings + + #3.1.0 + label_crm_contact_added: Contact added + label_crm_note_added: Note added + label_crm_deal_added: Deal added + label_crm_deal_updated: Deal updated + text_crm_contact_added: "Contact %{name} has been added by %{author}." + text_crm_deal_added: "Deal %{name} has been added by %{author}." + text_crm_deal_status_changed: "Deal status changed from %{old} to %{new}" + text_crm_deal_updated: "Deal %{name} has been updated by %{author}." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Import from CSV + permission_import_deals: Import deals + label_crm_single_quotes: "Single quotes (')" + label_crm_double_quotes: "Double quotes (\")" + label_crm_quotes_type: Quotation marks type + label_crm_contacts_visibility: Visibility + label_crm_contacts_visibility_project: By projects permissions + label_crm_contacts_visibility_public: Public + label_crm_contacts_visibility_private: Private + permission_view_private_contacts: View private contacts + text_crm_error_on_line: 'Error on line %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Probabilidade + label_crm_deal_status_type: Tipo de status + label_crm_select_companies: Filter companies in deal + label_crm_expected_revenue: Receita esperada + label_crm_deal_due_date: Data de vencimento + + #3.2.2 + label_crm_show_deals_in_top_menu: Mostrar acordos no menu superior + label_crm_show_details: Mostrar detalhes + label_crm_has_deals: tem acordos + label_crm_has_open_issues: Tarefa abertas + label_crm_note: Nota + + #3.2.5 + notice_failed_to_save_contacts: "Failed to save %{count} contact(s) on %{total} selected: %{ids}." + + #3.2.6 + project_module_deals: Acordos + permission_manage_deals: Gerenciar acordos + label_crm_deals_from_subprojects: Ver acordos de subprojetos + label_crm_megre_tags: Merge tags + label_crm_monochrome_tags: Monochrome tags + + #3.2.7 + label_crm_address: Endereco + label_crm_street1: Rua 1 + label_crm_street2: Rua 2 + label_crm_city: Cidade + label_crm_region: 'Estado' + label_crm_postcode: 'CEP' + label_crm_country: País + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: Cuba + CY: Cyprus + CZ: Czech Republic + CI: Côte d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong SAR China + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: Mexico + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: Poland + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: Russia + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + KR: South Korea + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syria + ST: São Tomé and Príncipe + TW: Taiwan + TJ: Tajikistan + TZ: Tanzania + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + SU: Union of Soviet Socialist Republics + AE: United Arab Emirates + GB: United Kingdom + US: United States + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: Vietnam + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land Islands + label_crm_cross_project_contacts: Permitir relações de contatos entre projetos + label_crm_list_board: Board + label_crm_default_list_style: Estilo de lista padrão + label_crm_show_in_top_menu: Show in top menu + label_crm_show_in_app_menu: Show in app menu + label_crm_money_settings: Moeda + label_crm_disable_taxes: Desabilitar impostos + label_crm_default_tax: Valor padrão de impostos + label_crm_tax_type: Tipo de imposto + label_crm_tax_type_inclusive: Imposto incluído + label_crm_tax_type_exclusive: Imposto exclusivo + label_crm_default_currency: Moeda padrão + label_crm_thousands_delimiter: Delimitador de milhares + label_crm_decimal_separator: Separador decimal + + + #3.2.10 + label_crm_add_contact_plural: Adicionar contatos + label_crm_search_for_contact: Buscar pelo contato + label_crm_major_currencies: Principais moedas + + #3.2.11 + label_crm_post_address_format: Formato do endereço + label_crm_post_address_format_macros: "Macros do formato do endereço: %{macros}" + + #3.2.14 + label_crm_last_year: Último ano + + #3.2.15 + permission_export_contacts: Exportar contatos e acordos + + #3.4.0 + label_crm_deal_contact: Acordos do contato + + #3.4.1 + label_crm_default_country: País padrão + label_attribute_of_contact: "Contato %{name}" + label_crm_contact_country: País do contato + label_crm_contact_city: Cidade do contato + label_attribute_of_deals: "Acordo %{name}" + + #3.4.2 + permission_manage_public_deals_queries: Administrar buscas de acordos públicos + permission_save_deals_queries: Salvar buscas de acordos \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/ru.yml b/plugins/redmine_contacts/config/locales/ru.yml new file mode 100644 index 0000000..a6de1b4 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/ru.yml @@ -0,0 +1,598 @@ +# encoding: utf-8 +ru: + contacts_title: Контакты + + label_crm_recently_viewed: ПоÑледние проÑмотренные + label_crm_gravatar_enabled: ИÑпользовать Gravatar Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚Ð¾Ð² + label_crm_thumbnails_enabled: Отображать миниатюры изображений в заметках + label_crm_max_thumbnail_file_size: МакÑимальный размер отображаемой миниатюры + label_crm_view_all_contacts: Ð’Ñе контакты + label_crm_background_info: Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + label_crm_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + label_contact_plural: Контакты + label_crm_contact_edit_information: Редактировать информацию по контакту + label_crm_edit_tags: Редактировать Ñ‚Ñги + label_crm_contact_view: ПроÑмотр + label_crm_contact_list: СпиÑок + label_crm_contact_new: Ðовый контакт + label_crm_at_company: в + label_crm_last_notes: ПоÑледние заметки + label_crm_tags_plural: ТÑги + label_crm_multi_tags_plural: Выбор неÑкольких Ñ‚Ñгов + label_crm_single_tag_mode: Один Ñ‚Ñг + label_crm_multiple_tags_mode: ÐеÑколько Ñ‚Ñгов + label_crm_contact_tag: ТÑг + label_crm_time_ago: назад + label_crm_add_note_plural: Добавить заметку + label_crm_note_plural: Заметки + + label_crm_add_tags_rule: неÑколько Ñ‚Ñгов через зÑпÑтую + label_crm_contact_search: ПоиÑк по имени + label_crm_note_for: Заметка Ð´Ð»Ñ + label_crm_show_on_map: Показать на карте + label_crm_add_another_phone: добавить телефон + label_crm_remove: удалить + label_crm_related_contacts: СвÑзанные контакты + label_crm_assigned_to: ОтветÑтвенный + label_crm_issue_added: Добавлена задача + label_crm_add_emails_rule: неÑколько email через зÑпÑтую + label_crm_add_phones_rule: неÑколько телефонов через зÑпÑтую + label_crm_add_employee: Ðовый Ñотрудник + label_crm_merge_duplicate_plural: Объединить + label_crm_duplicate_plural: ВероÑтные дубликаты + label_crm_duplicate_for_plural: ВероÑтные дубликаты Ð´Ð»Ñ + label_crm_add_tag: + добавить Ñ‚Ñг + + label_crm_note_show_extras: Дополнительно (тип, дата, файлы) + label_crm_note_hide_extras: Скрыть параметры + label_crm_note_added: Заметка уÑпешно добавлена + label_crm_note_read_more: (вÑÑ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ°) + label_crm_invoice_import: Загрузить Ñчета из CSV + + label_deal_plural: Сделки + label_crm_contractor_plural: УчаÑтники Ñделки + label_deal: Сделка + label_crm_deal_new: ÐÐ¾Ð²Ð°Ñ Ñделка + label_crm_deal_edit_information: Редактировать информацию по Ñделке + label_crm_deal_change_status: Сменить ÑÑ‚Ð°Ñ‚ÑƒÑ + label_crm_statistics: CтатиÑтика + + label_crm_deal_status_new: ÐÐ¾Ð²Ð°Ñ + label_crm_deal_status_first_contact: Первый контакт + label_crm_deal_status_negotiations: Переговоры + label_crm_deal_status_pending: ПринÑтие Ñ€ÐµÑˆÐµÐ½Ð¸Ñ + label_crm_deal_status_won: Выиграна + label_crm_deal_status_lost: Отвергнута + label_crm_deals_import: Загрузить Ñделки из CSV + + label_crm_created_on: Дата ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ + + field_note_date: Дата заметки + + field_deal_name: Ðазвание + field_deal_background: ОпиÑание + field_deal_contact: УчаÑтник Ñделки + field_deal_price: Сумма + field_price: Cумма + + field_contact_avatar: Фото + field_contact_is_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + field_contact_name: Ð˜Ð¼Ñ + field_contact_last_name: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_contact_first_name: Ð˜Ð¼Ñ + field_contact_middle_name: ОтчеÑтво + field_contact_job_title: ДолжноÑть + field_contact_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + field_contact_address: ÐÐ´Ñ€ÐµÑ + field_contact_phone: Телефон + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_background: Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ + field_contact_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ + field_contact_tag_names: ТÑги + field_first_name: Ð˜Ð¼Ñ + field_last_name: Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ + field_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + field_birthday: День Ñ€Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ + field_contact_department: Отдел + + field_company_field: Род деÑтельноÑти + + field_color: Цвет + + button_add_note: Добавить заметку + notice_successful_save: Сохранение уÑпешно завершено + notice_successful_add: Создание уÑпешно завершено + notice_unsuccessful_save: Ðевозможно Ñохранить + notice_successful_merged: Объединение уÑпешно завершено + + notice_merged_warning: Ð’Ñе привÑзанные проекты, заметки, Ñ‚Ñги и задачи Ñтого контакта будут перенеÑены в выбранный в ÑпиÑке внизу, а Ñтот контакт будет удален. + + project_module_contacts: Контакты + + permission_view_contacts: ПроÑмотр контактов + permission_edit_contacts: Редактирование контактов + permission_delete_contacts: Удаление контактов + permission_view_deals: ПроÑмотр Ñделок + permission_edit_deals: Редактирование Ñделок + permission_delete_deals: Удаление Ñделок + permission_add_notes: Добавление заметок + permission_delete_notes: Удаление заметок + permission_delete_own_notes: Удаление Ñвоих заметок + + # 2.0.0 + label_crm_deal_category: ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ñделки + label_crm_deal_category_plural: Категории Ñделок + label_crm_deal_category_new: ÐÐ¾Ð²Ð°Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ + text_deal_category_destroy_assignments: Удалить Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ ÐºÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸ + text_deal_category_destroy_question: "ÐеÑколько Ñделок (%{count}) назначено в данную категорию. Что Ð’Ñ‹ хотите предпринÑть?" + text_deal_category_reassign_to: Переназначить Ñделки Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ категории + text_deals_destroy_confirmation: 'Ð’Ñ‹ уверены, что хотите удалить выбранные Ñделки?' + label_crm_deal_status_plural: СтатуÑÑ‹ Ñделок + label_crm_deal_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ cделки + field_deal_status_is_closed: Закрытa + label_crm_deal_status_new: Ðовый + permission_manage_contacts: ÐаÑтройка Ñправочников + label_crm_sales_funnel: Воронка продаж + label_crm_period: Период + label_crm_count: Кол-во + + #2.0.1 + label_crm_user_format: Формат имени + label_crm_my_contact_plural: Контакты назначенные мне + label_crm_my_deal_plural: Открытые Ñделки назначенные мне + label_crm_contact_view_all: ПоÑмотреть вÑе контакты + label_crm_deal_view_all: ПоÑмотреть вÑе Ñделки + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Редактировать вÑе выбранные контакты + label_crm_bulk_edit_selected_deals: Редактировать вÑе выбранные Ñделки + label_crm_bulk_send_mail_selected_contacts: Отправить пиÑьмо выбранным контактам + field_add_tags: Добавить Ñ‚Ñги + field_delete_tags: СнÑть Ñ‚Ñги + label_crm_send_mail: Отправить пиÑьмо + error_empty_email: Email не может быть пуÑтым + permission_send_contacts_mail: ОтправлÑть пиÑьма + field_mail_from: Отправитель + text_email_macros: МакроÑÑ‹ %{macro} + field_message: Сообщение + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Контакт + field_age: ВозраÑÑ‚ + label_crm_vcf_import: Импорт из vCard + label_crm_mail_from: От + permission_import_contacts: Импорт контактов + + #2.1.0 + field_company_name: Ðаименование компании + label_crm_recently_added_contacts: Ðедавно Ñозданные контакты + label_crm_created_by_me: Контакты Ñозданные мной + my_contacts: Мои контакты + my_deals: Мои Ñделки + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Звонок + label_crm_note_type_meeting: Ð’Ñтреча + field_deal_currency: Валюта + label_crm_my_contacts_stats: СтатиÑтика по контактам за Ñтот меÑÑц + label_crm_contacts_created: Добавлено контактов + label_crm_deals_created: Добавлено Ñделок + my_contacts_avatars: Фотографии моих контактов + my_contacts_stats: СтатиÑтика по контактам + label_crm_add_into: Добавить в + label_crm_delete_from: Удалить из + label_crm_show_deaks_tab: Позывать закладку Сделки + label_crm_show_on_projects_show: Показывать контакты на обзоре проекта + + #2.2.1 + label_crm_contacts_show_in_list: Показывать в ÑпиÑке + + #2.3.0 + label_crm_module_plural: Модули + label_crm_list_partial_style: Отображать как + label_crm_list_excerpt: СпиÑком + label_crm_list_cards: Карточками + label_crm_list_list: Таблицей + field_contacts: Контакт + field_companies: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + label_crm_added_by: Добавил(а) + label_crm_contact_note_authoring_time: Показывать Ð²Ñ€ÐµÐ¼Ñ Ð·Ð°Ð¼ÐµÑ‚Ð¾Ðº + label_crm_contact_issues_filters: Фильтры по задачам + label_crm_csv_import: Импортировать из CSV + label_crm_upload_encoding: Кодировка файла + label_crm_csv_file: CSV файл + label_crm_csv_separator: Разделитель + field_middle_name: ОтчеÑтво + field_job_title: ДолжноÑть + field_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ + field_address: ÐÐ´Ñ€ÐµÑ + field_phone: Телефон + field_email: Email + field_tags: ТÑги + field_is_company: ЯвлÑетÑÑ ÐºÐ¾Ð¼Ð¿Ð°Ð½Ð¸ÐµÐ¹ + field_contact_full_name: Полное Ð¸Ð¼Ñ + field_last_note: ПоÑледнÑÑ Ð·Ð°Ð¼ÐµÑ‚ÐºÐ° + button_contacts_edit_query: Редактировать Ð·Ð°Ð¿Ñ€Ð¾Ñ + button_contacts_delete_query: Удалить Ð·Ð°Ð¿Ñ€Ð¾Ñ + permission_manage_public_contacts_queries: Управление общими запроÑами + permission_add_deals: Добавление Ñделок + permission_add_contacts: Добавление контактов + permission_save_contacts_queries: Сохранение запроÑов + + #2.3.3 + label_crm_contact_show_in_app_menu: Показывать вкладки в меню Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + + #2.3.4 + label_crm_contact_show_closed_issues: Показывать закрытые задачи + + #3.0.0 + label_crm_import: Импорт + label_contact_note_plural: Заметки контактов + label_deal_note_plural: Заметки Ñделок + label_crm_contact_all_note_plural: Ð’Ñе заметки + error_unable_delete_deal_status: Ðевозможно удалить ÑÑ‚Ð°Ñ‚ÑƒÑ Ñделки + label_crm_contacts_hidden: Дополнительные параметры + + #3.1.0 + label_crm_contact_added: Добавлен контакт + label_crm_note_added: Добавлена заметка + label_crm_deal_added: Добавлена Ñделка + label_crm_deal_updated: Сделка изменена + text_crm_contact_added: "Создан новый контакт %{name} (%{author})." + text_crm_deal_added: "Создана Ð½Ð¾Ð²Ð°Ñ Ñделка %{name} (%{author})." + text_crm_deal_status_changed: "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ñделки изменен Ñ %{old} на %{new}" + text_crm_deal_updated: "Сделка %{name} была обновлена (%{author})." + label_crm_contacts_cc: Cc + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Импорт из CSV + permission_import_deals: Импорт Ñделок + label_crm_single_quotes: "Одиночные кавычки (')" + label_crm_double_quotes: "Двойные кавычки (\")" + label_crm_quotes_type: Тип кавычек + label_crm_contacts_visibility: ВидимоÑть + label_crm_contacts_visibility_project: По правам в проектах + label_crm_contacts_visibility_public: Публичный + label_crm_contacts_visibility_private: ЧаÑтный + permission_view_private_contacts: ПроÑмотр чаÑтных контактов + text_crm_error_on_line: "Ошибка в Ñтроке %{line}: %{error}" + + #3.2.0 + label_crm_probability: ВероÑтноÑть + label_crm_deal_status_type: Тип + label_crm_select_companies: Контрагенты только компании + label_crm_expected_revenue: Ожидаемый доход + + #3.2.2 + label_crm_show_deals_in_top_menu: Показывать Ñделки в главном меню + label_crm_show_details: Показать детали + label_crm_has_deals: ЕÑть Ñделки + label_crm_has_open_issues: ЕÑть открытые задачи + label_crm_note: Заметка + + #3.2.5 + notice_failed_to_save_contacts: "Ðевозможно Ñохранить %{count} контакт(ов) из %{total} выбранных: %{ids}." + + #3.2.6 + project_module_deals: Сделки + permission_manage_deals: ÐаÑтройка Ñделок + label_crm_deals_from_subprojects: Отображать Ñделки из подпроектов + label_crm_megre_tags: Объединить метки + label_crm_monochrome_tags: Монохромные метки + + #3.2.7 + label_crm_address: ÐÐ´Ñ€ÐµÑ + label_crm_street1: Улица 1 + label_crm_street2: Улица 2 + label_crm_city: Город + label_crm_region: 'Регион' + label_crm_postcode: 'Почтовый индекÑ' + label_crm_country: Страна + label_crm_countries: + AU: ÐвÑÑ‚Ñ€Ð°Ð»Ð¸Ñ + AT: ÐвÑÑ‚Ñ€Ð¸Ñ + AZ: Ðзербайджан + AX: ÐландÑкие оÑтрова + AL: ÐÐ»Ð±Ð°Ð½Ð¸Ñ + DZ: Ðлжир + VI: ÐмериканÑкие ВиргинÑкие оÑтрова + AS: ÐмериканÑкое Самоа + AO: Ðнгола + AI: Ðнгуилла + AD: Ðндорра + AQ: Ðнтарктика + AG: Ðнтигуа и Барбуда + AR: Ðргентина + AM: ÐÑ€Ð¼ÐµÐ½Ð¸Ñ + AW: Ðруба + AF: ÐфганиÑтан + BS: БагамÑкие оÑтрова + BD: Бангладеш + BB: Ð‘Ð°Ñ€Ð±Ð°Ð´Ð¾Ñ + BH: Бахрейн + BY: БеларуÑÑŒ + BZ: Белиз + BE: Ð‘ÐµÐ»ÑŒÐ³Ð¸Ñ + BJ: Бенин + BM: БермудÑкие ОÑтрова + BG: Ð‘Ð¾Ð»Ð³Ð°Ñ€Ð¸Ñ + BO: Ð‘Ð¾Ð»Ð¸Ð²Ð¸Ñ + BA: БоÑÐ½Ð¸Ñ Ð¸ Герцеговина + BW: БотÑвана + BR: Ð‘Ñ€Ð°Ð·Ð¸Ð»Ð¸Ñ + IO: БританÑÐºÐ°Ñ Ñ‚ÐµÑ€Ñ€Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð² ИндийÑком океане + VG: БританÑкие ВиргинÑкие ОÑтрова + BN: Бруней ДаруÑÑалам + BF: Буркина ФаÑо + BI: Бурунди + BT: Бутан + VU: Вануату + VA: Ватикан + GB: Ð’ÐµÐ»Ð¸ÐºÐ¾Ð±Ñ€Ð¸Ñ‚Ð°Ð½Ð¸Ñ + HU: Ð’ÐµÐ½Ð³Ñ€Ð¸Ñ + VE: ВенеÑуÑла + UM: Внешние малые оÑтрова (СШÐ) + TL: ВоÑточный Тимор + VN: Вьетнам + GA: Габон + HT: Гаити + GY: Гайана + GM: Ð“Ð°Ð¼Ð±Ð¸Ñ + GH: Гана + GP: Гваделупа + GT: Гватемала + GN: Ð“Ð²Ð¸Ð½ÐµÑ + GW: ГвинеÑ-БиÑÑау + DE: Ð“ÐµÑ€Ð¼Ð°Ð½Ð¸Ñ + GG: ГернÑи + GI: Гибралтар + HN: Ð“Ð¾Ð½Ð´ÑƒÑ€Ð°Ñ + HK: Гонконг, ОÑобый ÐдминиÑтративный Район ÐšÐ¸Ñ‚Ð°Ñ + GD: Гренада + GL: Ð“Ñ€ÐµÐ½Ð»Ð°Ð½Ð´Ð¸Ñ + GR: Ð“Ñ€ÐµÑ†Ð¸Ñ + GE: Ð“Ñ€ÑƒÐ·Ð¸Ñ + GU: Гуам + DK: Ð”Ð°Ð½Ð¸Ñ + CD: ДемократичеÑÐºÐ°Ñ Ð ÐµÑпублика Конго + JE: ДжерÑи + DJ: Джибути + DO: ДоминиканÑÐºÐ°Ñ Ð ÐµÑпублика + EG: Египет + ZM: Ð—Ð°Ð¼Ð±Ð¸Ñ + EH: Ð—Ð°Ð¿Ð°Ð´Ð½Ð°Ñ Ð¡Ð°Ñ…Ð°Ñ€Ð° + ZW: Зимбабве + IL: Израиль + IN: Ð˜Ð½Ð´Ð¸Ñ + ID: Ð˜Ð½Ð´Ð¾Ð½ÐµÐ·Ð¸Ñ + JO: Ð˜Ð¾Ñ€Ð´Ð°Ð½Ð¸Ñ + IQ: Ирак + IR: Иран + IE: Ð˜Ñ€Ð»Ð°Ð½Ð´Ð¸Ñ + IS: ИÑÐ»Ð°Ð½Ð´Ð¸Ñ + ES: ИÑÐ¿Ð°Ð½Ð¸Ñ + IT: Ð˜Ñ‚Ð°Ð»Ð¸Ñ + YE: Йемен + KZ: КазахÑтан + KY: Каймановы оÑтрова + KH: Камбоджа + CM: Камерун + CA: Канада + QA: Катар + KE: ÐšÐµÐ½Ð¸Ñ + CY: Кипр + KI: Кирибати + CN: Китай + CC: КокоÑовые оÑтрова + CO: ÐšÐ¾Ð»ÑƒÐ¼Ð±Ð¸Ñ + KM: КоморÑкие ОÑтрова + CG: Конго + KP: КорейÑÐºÐ°Ñ Ðародно-ДемократичеÑÐºÐ°Ñ Ð ÐµÑпублика + CR: КоÑта-Рика + CI: Кот д’Ивуар + CU: Куба + KW: Кувейт + KG: КыргызÑтан + LA: Ð›Ð°Ð¾Ñ + LV: Ð›Ð°Ñ‚Ð²Ð¸Ñ + LS: ЛеÑото + LR: Ð›Ð¸Ð±ÐµÑ€Ð¸Ñ + LB: Ливан + LY: Ð›Ð¸Ð²Ð¸Ñ + LT: Литва + LI: Лихтенштейн + LU: ЛюкÑембург + MU: Маврикий + MR: ÐœÐ°Ð²Ñ€Ð¸Ñ‚Ð°Ð½Ð¸Ñ + MG: МадагаÑкар + YT: Майотта + MO: Макао (оÑобый админиÑтративный район КÐР) + MK: ÐœÐ°ÐºÐµÐ´Ð¾Ð½Ð¸Ñ + MW: Малави + MY: ÐœÐ°Ð»Ð°Ð¹Ð·Ð¸Ñ + ML: Мали + MV: Мальдивы + MT: Мальта + MA: Марокко + MQ: Мартиник + MH: Маршалловы ОÑтрова + MX: МекÑика + MZ: Мозамбик + MD: Молдова + MC: Монако + MN: ÐœÐ¾Ð½Ð³Ð¾Ð»Ð¸Ñ + MS: МонÑеррат + MM: МьÑнма + NA: ÐÐ°Ð¼Ð¸Ð±Ð¸Ñ + NR: Ðауру + ZZ: ÐеизвеÑтный или недейÑтвительный регион + NP: Ðепал + NE: Ðигер + NG: ÐÐ¸Ð³ÐµÑ€Ð¸Ñ + AN: ÐидерландÑкие ÐнтильÑкие оÑтрова + NL: Ðидерланды + NI: Ðикарагуа + NU: Ðиуе + NZ: ÐÐ¾Ð²Ð°Ñ Ð—ÐµÐ»Ð°Ð½Ð´Ð¸Ñ + NC: ÐÐ¾Ð²Ð°Ñ ÐšÐ°Ð»ÐµÐ´Ð¾Ð½Ð¸Ñ + "NO": ÐÐ¾Ñ€Ð²ÐµÐ³Ð¸Ñ + AE: Объединенные ÐрабÑкие Эмираты + OM: Оман + BV: ОÑтров Буве + DM: ОÑтров Доминика + IM: ОÑтров МÑн + NF: ОÑтров Ðорфолк + CX: ОÑтров РождеÑтва + BL: ОÑтров СвÑтого Ð‘Ð°Ñ€Ñ‚Ð¾Ð»Ð¾Ð¼ÐµÑ + MF: ОÑтров СвÑтого Мартина + SH: ОÑтров СвÑтой Елены + CV: ОÑтрова Зеленого МыÑа + CK: ОÑтрова Кука + TC: ОÑтрова Ð¢Ñ‘Ñ€ÐºÑ Ð¸ ÐšÐ°Ð¹ÐºÐ¾Ñ + HM: ОÑтрова Херд и Макдональд + PK: ПакиÑтан + PW: Палау + PS: ПалеÑтинÑÐºÐ°Ñ Ð°Ð²Ñ‚Ð¾Ð½Ð¾Ð¼Ð¸Ñ + PA: Панама + PG: Папуа-ÐÐ¾Ð²Ð°Ñ Ð“Ð²Ð¸Ð½ÐµÑ + PY: Парагвай + PE: Перу + PN: Питкерн + PL: Польша + PT: ÐŸÐ¾Ñ€Ñ‚ÑƒÐ³Ð°Ð»Ð¸Ñ + PR: ПуÑрто-Рико + KR: РеÑпублика ÐšÐ¾Ñ€ÐµÑ + RE: Реюньон + RU: РоÑÑÐ¸Ñ + RW: Руанда + RO: Ð ÑƒÐ¼Ñ‹Ð½Ð¸Ñ + US: СШР+ SV: Сальвадор + WS: Самоа + SM: Сан-Марино + ST: Сан-Томе и ПринÑипи + SA: СаудовÑÐºÐ°Ñ ÐÑ€Ð°Ð²Ð¸Ñ + SZ: Свазиленд + SJ: Свальбард и Ян-Майен + MP: Северные МарианÑкие ОÑтрова + SC: СейшельÑкие ОÑтрова + PM: Сен-Пьер и Микелон + SN: Сенегал + VC: Сент-ВинÑент и Гренадины + KN: Сент-ÐšÐ¸Ñ‚Ñ‚Ñ Ð¸ ÐÐµÐ²Ð¸Ñ + LC: Сент-ЛюÑÐ¸Ñ + RS: Ð¡ÐµÑ€Ð±Ð¸Ñ + CS: Ð¡ÐµÑ€Ð±Ð¸Ñ Ð¸ Ð§ÐµÑ€Ð½Ð¾Ð³Ð¾Ñ€Ð¸Ñ + SG: Сингапур + SY: СирийÑÐºÐ°Ñ ÐрабÑÐºÐ°Ñ Ð ÐµÑпублика + SK: Ð¡Ð»Ð¾Ð²Ð°ÐºÐ¸Ñ + SI: Ð¡Ð»Ð¾Ð²ÐµÐ½Ð¸Ñ + SB: Соломоновы ОÑтрова + SO: Сомали + SD: Судан + SR: Суринам + SL: Сьерра-Леоне + TJ: ТаджикиÑтан + TH: Таиланд + TW: Тайвань + TZ: Ð¢Ð°Ð½Ð·Ð°Ð½Ð¸Ñ + TG: Того + TK: Токелау + TO: Тонга + TT: Тринидад и Тобаго + TV: Тувалу + TN: Ð¢ÑƒÐ½Ð¸Ñ + TM: ТуркмениÑтан + TR: Ð¢ÑƒÑ€Ñ†Ð¸Ñ + UG: Уганда + UZ: УзбекиÑтан + UA: Украина + WF: Ð£Ð¾Ð»Ð»Ð¸Ñ Ð¸ Футуна + UY: Уругвай + FO: ФарерÑкие оÑтрова + FM: Федеративные Штаты Микронезии + FJ: Фиджи + PH: Филиппины + FI: ФинлÑÐ½Ð´Ð¸Ñ + FK: ФолклендÑкие оÑтрова + FR: Ð¤Ñ€Ð°Ð½Ñ†Ð¸Ñ + GF: ФранцузÑÐºÐ°Ñ Ð“Ð²Ð¸Ð°Ð½Ð° + PF: ФранцузÑÐºÐ°Ñ ÐŸÐ¾Ð»Ð¸Ð½ÐµÐ·Ð¸Ñ + TF: ФранцузÑкие Южные Территории + HR: Ð¥Ð¾Ñ€Ð²Ð°Ñ‚Ð¸Ñ + CF: Центрально-ÐфриканÑÐºÐ°Ñ Ð ÐµÑпублика + TD: Чад + ME: Ð§ÐµÑ€Ð½Ð¾Ð³Ð¾Ñ€Ð¸Ñ + CZ: ЧешÑÐºÐ°Ñ Ñ€ÐµÑпублика + CL: Чили + CH: Ð¨Ð²ÐµÐ¹Ñ†Ð°Ñ€Ð¸Ñ + SE: Ð¨Ð²ÐµÑ†Ð¸Ñ + LK: Шри-Ланка + EC: Эквадор + GQ: Ð­ÐºÐ²Ð°Ñ‚Ð¾Ñ€Ð¸Ð°Ð»ÑŒÐ½Ð°Ñ Ð“Ð²Ð¸Ð½ÐµÑ + ER: Ð­Ñ€Ð¸Ñ‚Ñ€ÐµÑ + EE: ЭÑÑ‚Ð¾Ð½Ð¸Ñ + ET: Ð­Ñ„Ð¸Ð¾Ð¿Ð¸Ñ + ZA: Ð®Ð¶Ð½Ð°Ñ Ðфрика + GS: Ð®Ð¶Ð½Ð°Ñ Ð”Ð¶Ð¾Ñ€Ð´Ð¶Ð¸Ñ Ð¸ Южные Сандвичевы ОÑтрова + JM: Ямайка + JP: Ð¯Ð¿Ð¾Ð½Ð¸Ñ + label_crm_cross_project_contacts: Разрешить ÑÑылатьÑÑ Ð½Ð° контакты во вÑех оÑтальных проектах + label_crm_list_board: ДоÑка + label_crm_default_list_style: Вид ÑпиÑка по умолчанию + label_crm_show_in_top_menu: Показывать в верхем меню + label_crm_show_in_app_menu: Показывать в меню Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ + label_crm_money_settings: Деньги + label_crm_disable_taxes: Отключить налоги + label_crm_default_tax: Ðалог по умолчанию + label_crm_tax_type: Тип налога + label_crm_tax_type_inclusive: Ðалог включен Ñ Ð¾Ð±Ñ‰ÑƒÑŽ Ñумму + label_crm_tax_type_exclusive: Ðалог добавлен к общей Ñумме + label_crm_default_currency: Валюта по умолчанию + label_crm_thousands_delimiter: Разделитель разрÑдов + label_crm_decimal_separator: ДеÑÑтичный разделитель + + #3.2.10 + label_crm_add_contact_plural: СвÑзать контакты + label_crm_search_for_contact: Ðайти контакт + label_crm_major_currencies: ОÑновные валюты + + #3.2.11 + label_crm_post_address_format: Формат почтового адреÑа + label_crm_post_address_format_macros: "МакроÑÑ‹ почтового формата: %{macros}" + + #3.2.14 + label_crm_last_year: поÑледний год + + #3.2.15 + permission_export_contacts: ЭкÑпортировать контакты и Ñделки + + #3.4.0 + label_crm_deal_contact: Контакт Ñделки + + #3.4.1 + label_crm_default_country: Страна по умолчанию + label_attribute_of_contact: "%{name} контакта" + label_crm_contact_country: Страна контакта + label_crm_contact_city: Город контакта + label_attribute_of_deals: "%{name} Ñделки" + + #3.4.2 + permission_manage_public_deals_queries: Управление общими запроÑами Ð´Ð»Ñ Ñделок + permission_save_deals_queries: Сохранение запроÑов Ð´Ð»Ñ Ñделок + permission_manage_contact_issue_relations: Управление ÑвÑзыванием Ñ Ð·Ð°Ð´Ð°Ñ‡Ð°Ð¼Ð¸ + + #3.4.4 + label_crm_pipeline: Воронка продаж + text_crm_no_deal_statuses_in_project: Ð’ проекте нет активных ÑтатуÑов Ð´Ð»Ñ Ñделок + label_crm_light_free_version: CRM беÑÐ¿Ð»Ð°Ñ‚Ð½Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ + label_crm_link_to_pro: Обновление до PRO + label_crm_link_to_pro_demo: Онлайн демо PRO + label_crm_link_to_more_plugins: Другие плагины RedmineCRM + + text_crm_string_incorrect_format: имеет неправильный формат + + label_deal_items: Объекты Ñделки diff --git a/plugins/redmine_contacts/config/locales/sk.yml b/plugins/redmine_contacts/config/locales/sk.yml new file mode 100644 index 0000000..5992f78 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/sk.yml @@ -0,0 +1,144 @@ +# encoding: utf-8-et +sk: + contacts_title: Kontakty + + label_crm_recently_viewed: Nedávno zobrazené + label_crm_gravatar_enabled: Použi Gravatar + label_crm_view_all_contacts: Zobraz vÅ¡echny kontakty + label_crm_background_info: Poznámky ku kontaktu + label_crm_company: SpoloÄnosÅ¥ + label_contact_plural: Kontakty + label_crm_contact_edit_information: Úprava kontaktných informácií + label_crm_edit_tags: UpraviÅ¥ Å¡títky + label_crm_contact_view: ZobraziÅ¥ + label_crm_contact_list: Zoznam + label_crm_contact_new: Nový kontakt + label_crm_at_company: v + label_crm_last_notes: Posledné poznámky + label_crm_tags_plural: Å títky + label_crm_multi_tags_plural: VybraÅ¥ viac Å¡titkov + label_crm_single_tag_mode: Jeden Å¡títok + label_crm_multiple_tags_mode: Viac Å¡títkov + label_crm_contact_tag: Å títok + label_crm_time_ago: pred + label_crm_add_note_plural: PridaÅ¥ poznámky k + label_crm_note_plural: Poznámky + + label_crm_add_tags_rule: oddeliÅ¥ Äiarkami + label_crm_contact_search: VyhľadaÅ¥ podľa mena + label_crm_note_for: Poznámka pre + label_crm_show_on_map: ZobraziÅ¥ na mape + label_crm_add_another_phone: pridaÅ¥ telefón + label_contact_note_plural: VÅ¡etky poznámky + label_crm_remove: zmazaÅ¥ + label_crm_related_contacts: Súvisiace kontakty + label_crm_assigned_to: Priradené + label_crm_issue_added: Issue pridaná + label_crm_add_emails_rule: oddeliÅ¥ Äiarkami + label_crm_add_phones_rule: oddeliÅ¥ Äiarkami + + label_crm_note_show_extras: PokroÄilé (súbory, dátum) + label_crm_note_hide_extras: Skrýť pokroÄilé + label_crm_note_added: Poznámka úspeÅ¡ne pridaná + + label_deal_plural: Ponuky + label_crm_contractor_plural: Sprostredkovatelia + label_deal: Ponuka + label_crm_deal_new: Nová ponuka + label_crm_deal_edit_information: UpraviÅ¥ informácie o ponuke + label_crm_deal_change_status: ZmeniÅ¥ stav + label_crm_statistics: Å tatistiky + + field_note_date: Dátum poznámky + + field_deal_name: Meno + field_deal_background: Poznámky k ponuke + field_deal_contact: Kontakt + field_deal_price: Suma + field_price: Suma + + field_contact_avatar: Avatar + field_contact_is_company: Kontakt na spoloÄnosÅ¥ + field_contact_name: Meno + field_contact_last_name: Priezvisko + field_contact_first_name: Meno + field_contact_middle_name: Stredné meno + field_contact_job_title: Pracovná pozícia + field_contact_company: SpoloÄnosÅ¥ + field_contact_address: Adresa + field_contact_phone: Telefón + field_contact_email: Email + field_contact_website: Webová stránka + field_contact_skype: Skype + field_contact_status: Stav + field_contact_background: Poznámky ku kontaktu + field_contact_tag_names: Å títky + field_first_name: Meno + field_last_name: Priezvisko + field_company: SpoloÄnosÅ¥ + field_birthday: Narodeniny + field_contact_department: Oddelenie + + field_company_field: Odvetie + + button_add_note: PridaÅ¥ poznámku + notice_successful_save: Uložené + notice_successful_add: Vytvorené + notice_unsuccessful_save: Chyba pri uložení + + project_module_contacts: Kontakty + + permission_view_contacts: ZobraziÅ¥ kontakty + permission_edit_contacts: UpraviÅ¥ kontakty + permission_delete_contacts: ZmazaÅ¥ kontakty + permission_view_deals: ZobraziÅ¥ nabídky + permission_edit_deals: UpraviÅ¥ nabídky + permission_delete_deals: ZmazaÅ¥ nabídky + permission_add_notes: PridaÅ¥ poznámky + permission_delete_notes: VymazaÅ¥ poznámky + permission_delete_own_notes: VymazaÅ¥ vlastné poznámky + + # 2.0.0 + label_crm_deal_category: Kategória poznámok + label_crm_deal_category_plural: Kategórie poznámok + label_crm_deal_category_new: Nová kategória + text_deal_category_destroy_assignments: ZmazaÅ¥ priradenie kateǵorie + text_deal_category_destroy_question: "Niektoré ponuky (%{count}) sú priradené do tejto kategórie. ÄŒo s nimi urobíme?" + text_deal_category_reassign_to: ZmeniÅ¥ kategóriu ponuky + text_deals_destroy_confirmation: 'Ste si istý, že chcete vymazaÅ¥ vybrané ponuky?' + label_crm_deal_status_plural: Stavy ponúk + label_crm_deal_status: Stav ponuky + field_deal_status_is_closed: Uzavretý + label_crm_deal_status_new: Nový + permission_manage_contacts: Oprávnenia + label_crm_sales_funnel: Predajný lievik + label_crm_period: Perióda + label_crm_count: PoÄet + + #2.0.1 + label_crm_user_format: Formát mena kontaktu + label_crm_my_contact_plural: Kontakty priradené mne + label_crm_my_deal_plural: Otvorené ponuky priradené mne + label_crm_contact_view_all: ZobraziÅ¥ vÅ¡etky kontakty + label_crm_deal_view_all: ZobraziÅ¥ vÅ¡etky ponuky + + #2.0.2 + label_crm_bulk_edit_selected_contacts: UpraviÅ¥ vÅ¡etky vybrané kontakty + label_crm_bulk_edit_selected_deals: UpraviÅ¥ vybrané ponuky + label_crm_bulk_send_mail_selected_contacts: PoslaÅ¥ email vybraným kontaktom + field_add_tags: PridaÅ¥ Å¡títky + field_delete_tags: VymazaÅ¥ Å¡títky + label_crm_send_mail: PoslaÅ¥ mail + error_empty_email: Email nesmie byÅ¥ prázdny + permission_send_contacts_mail: PoslaÅ¥ mail + field_mail_from: Z adresy + text_email_macros: Dostupné makra %{macro} + field_message: Správa + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Kontakt + field_age: Vek + label_crm_vcf_import: Import z vCard + label_crm_mail_from: Od + permission_import_contacts: Importuj kontakty \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/sr-YU.yml b/plugins/redmine_contacts/config/locales/sr-YU.yml new file mode 100644 index 0000000..b9b287d --- /dev/null +++ b/plugins/redmine_contacts/config/locales/sr-YU.yml @@ -0,0 +1,595 @@ +# Serbian translations for Redmine CRM +# by Aleksandar Pavic +sr-YU: + contacts_title: Kontakti + + label_crm_recently_viewed: Nedavno gledano + label_crm_gravatar_enabled: Koristiti Gravater + label_crm_thumbnails_enabled: Prikazati rad-sliku u beleÅ¡kama + label_crm_max_thumbnail_file_size: Maksimalna veliÄina slike + label_crm_view_all_contacts: Pregled svih kontakta + label_crm_background_info: Informacije o pozadini(ekrana) + label_crm_company: Kompanija + label_contact_plural: Kontakti + label_crm_contact_edit_information: Urediti informacije o kontaktima + label_crm_edit_tags: Urediti rad(deklaraciju) + label_crm_contact_view: Pregled + label_crm_contact_list: Lista + label_crm_contact_new: Novi kontakti + label_crm_at_company: u-na + label_crm_last_notes: Nedavne beleÅ¡ke + label_crm_tags_plural: Informacije-objaÅ¡njenje + label_crm_multi_tags_plural: Izabrati nekoliko objaÅ¡njenja + label_crm_single_tag_mode: Jedno objaÅ¡njenje + label_crm_multiple_tags_mode: ViÅ¡e objaÅ¡njenja + label_crm_contact_tag: Informacije-objaÅ¡njenje + label_crm_time_ago: pre + label_crm_add_note_plural: Dodati biljeÅ¡ku za + label_crm_note_plural: BeleÅ¡ke + + label_crm_add_tags_rule: odvojeno zapetama + label_crm_contact_search: pretraga po imenima + label_crm_note_for: BeleÅ¡ka za + label_crm_show_on_map: Prikaz na mapi-papiru + label_crm_add_another_phone: dodati broj telefona + label_crm_remove: obrisati + label_crm_related_contacts: povezani kontaktn + label_crm_assigned_to: Odgovoran + label_crm_issue_added: Dodato zadatak + label_crm_add_emails_rule: odvojiti zapetama + label_crm_add_phones_rule: odvojiti zapetama + label_crm_add_employee: Novi radnik + label_crm_merge_duplicate_plural: Integrisati-sjediniti se + label_crm_duplicate_plural: Mogući duplikati + label_crm_duplicate_for_plural: Mogući duplikati za + label_crm_add_tag: + dodati objaÅ¡njenje + + label_crm_note_show_extras: Napredan nivo + label_crm_note_hide_extras: Sakriven nivo + label_crm_note_added: Dodata zabeleÅ¡ka + label_crm_note_read_more: ViÅ¡e informacija + label_crm_invoice_import: Uneti raÄune iz CSV + + label_deal_plural: Govori o + label_crm_contractor_plural: Kontakti + label_deal: Poziv + label_crm_deal_new: Novi poziv + label_crm_deal_edit_information: Urediti informacije o pozivima + label_crm_deal_change_status: Promeniti status + label_crm_statistics: Statistika + label_crm_deals_import: Uneti poziv iz CSV + + label_crm_deal_status_new: Novi + label_crm_deal_status_first_contact: Prvi kontakt + label_crm_deal_status_negotiations: Pregovaranja + label_crm_deal_status_pending: za, za vreme + label_crm_deal_status_won: Osvojiti + label_crm_deal_status_lost: Izgubljeno + + label_crm_created_on: Kreirati na + + field_note_date: Datum zabeleÅ¡ke + field_background: Pozadina-podloga + field_currency: Rok trajanja-valuta + field_contact: Kontakt + + field_deal_name: Ime + field_deal_background: Pozadina + field_deal_contact: Kontakt + field_deal_price: Suma + field_price: Suma + + field_contact_avatar: ikona za kontakt + field_contact_is_company: Kompanija + field_contact_name: Ime + field_contact_last_name: Prezime + field_contact_first_name: Ime + field_contact_middle_name: Drugo ime + field_contact_job_title: Naziv posla + field_contact_company: Kompanija + field_contact_address: Adresa + field_contact_phone: Telfon + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skajp + field_contact_status: Status + field_contact_background: Pozadina + field_contact_tag_names: ObjaÅ¡njenja-komentari + field_first_name: Ime + field_last_name: Prezime + field_company: Kompanija + field_birthday: Datum roÄ‘enja + field_contact_department: Odsek + + + field_company_field: Delatnost + + field_color: Boja + + button_add_note: Dodati zabeleÅ¡ku + notice_successful_save: UspeÅ¡no saÄuvano + notice_successful_add: UspeÅ¡no kreirano + notice_unsuccessful_save: SaÄuvati greÅ¡ke-moguće probleme + notice_successful_merged: UspeÅ¡no sjedinjeno + + notice_merged_warning: Sve zabeleÅ¡ke, projekti, objaÅ¡njenja i zadaci dodeljeni ovoj osobi će biti dodati izabranoj osobi u nastavku. Kontakt će biti izbrisan. + + project_module_contacts: Kontakti + + permission_view_contacts: Pregled kontakta + permission_edit_contacts: Urediti kontakte + permission_delete_contacts: Izbrisati kontakte + permission_view_deals: Pregled razgovora + permission_edit_deals: Urediti razgovore + permission_delete_deals: Izbrisati razgovore + permission_add_notes: Dodati beleÅ¡ke + permission_delete_notes: Izabrati beleÅ¡ke + permission_delete_own_notes: Izabrati vlastite beleÅ¡ke + + # 2.0.0 + label_crm_deal_category: Kategorija poziva + label_crm_deal_category_plural: Kategorija poziva + label_crm_deal_category_new: Nova kategorija + text_deal_category_destroy_assignments: Premestiti-ukloniti zadatke prema kategorijama + text_deal_category_destroy_question: "Neki pozivi (% procenti) su dodeljeni ovoj kategoriji. Å ta želite uraditi?" + text_deal_category_reassign_to: Preraspodeliti pozive ovoj kategoriji + text_deals_destroy_confirmation: 'Da li ste sigurni da žeÄite izbrisati ovaj poziv-e?' + label_crm_deal_status_plural: Statusni pozivi + label_crm_deal_status: Statusni poziv + field_deal_status_is_closed: ZavrÅ¡eno-zatvoreno + label_crm_deal_status_new: Novi + permission_manage_contacts: Upravljanje kontaktima + label_crm_sales_funnel: Kanal prodaje + label_crm_period: Period + label_crm_count: Broj + + #2.0.1 + label_crm_user_format: Format imena kontakta + label_crm_my_contact_plural: Kontakti koji su mi dodjeljeni + label_crm_my_deal_plural: Pozivi dodjeljeni meni + label_crm_contact_view_all: Pregled svih kontakta + label_crm_deal_view_all: Pregled svih poziva + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Izbrisati sve odabrane pozive + label_crm_bulk_edit_selected_deals: Urediti sve odabrane pozive + label_crm_bulk_send_mail_selected_contacts: Poslati mejl odabranim kontaktima + field_add_tags: Dodati objaÅ¡njenja + field_delete_tags: Izbrisati objaÅ¡njenja + label_crm_send_mail: Poslati mejl + error_empty_email: Email ne može biti prazan + permission_send_contacts_mail: Poslati mejl + field_mail_from: Adresa + text_email_macros: Dostupni makroi %{macro} + field_message: Poruka + + #2.0.3 + label_crm_add_contact: Dodati kontakt + label_contact: Kontakt + field_age: Godine + label_crm_vcf_import: Prebaciti iz V-kard + label_crm_mail_from: iz-od + permission_import_contacts: Uneti kontakte + + #2.1.0 + field_company_name: Ime kompanije + label_crm_recently_added_contacts: Nedavno dodati kontakti + label_crm_created_by_me: Kontakti koje sam kreirao + my_contacts: Moji kontakti + my_deals: Moji pozivi + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Poziv + label_crm_note_type_meeting: Sastanak + field_deal_currency: Vrijeme trajanja + label_crm_my_contacts_stats: IzveÅ¡taj o kontaktima za ovaj mesec + label_crm_contacts_created: Kreirani kontakti + label_crm_deals_created: Kreirani pozivi + my_contacts_avatars: Slike mojih kontakta + my_contacts_stats: IzveÅ¡taj o kontaktima + label_crm_add_into: Dodati u + label_crm_delete_from: Izbrisati iz + label_crm_show_deaks_tab: Prikaz poziva + label_crm_show_on_projects_show: Prikaz kontakta na projektorskom pregledu + + #2.2.1 + label_crm_contacts_show_in_list: Prikazati na listi + + #2.3.0 + label_crm_module_plural: Moduli + label_crm_list_partial_style: Stil liste + label_crm_list_excerpt: Prikaz liste + label_crm_list_cards: Kartice + label_crm_list_list: Naslov + field_contacts: Kontakt + field_companies: Kompanija + label_crm_added_by: Dodano + label_crm_contact_note_authoring_time: Prikazati vrijeme + label_crm_contact_issues_filters: Finteri naloga + label_crm_csv_import: Unijeti iz CSV + label_crm_upload_encoding: Dekodiranje fajla + label_crm_csv_file: CSV fajl + label_crm_csv_separator: Zarez + field_middle_name: Drugo ime + field_job_title: Naziv posla + field_company: Kompanija + field_address: Adresa + field_phone: Telfon + field_email: Email + field_tags: ObjaÅ¡njenja-komentari + field_last_note: Poslednje obaveÅ¡tenje + field_is_company: je kompanija?? + field_contact_full_name: Puno ime + button_contacts_edit_query: Urediti pitanja + button_contacts_delete_query: Izbrisati pitanja + permission_manage_public_contacts_queries: Upravljati pitanjima + permission_add_deals: Dodati pozive + permission_add_contacts: Dodati kontakte + permission_save_contacts_queries: SaÄuvati pitanja + + #2.3.3 + label_crm_contact_show_in_app_menu: Prikazati prozore u meniju aplikacije + + #2.3.4 + label_crm_contact_show_closed_issues: Prikaz reÅ¡enih problema + + #3.0.0 + label_crm_import: Uneti-uvesti + label_contact_note_plural: BeleÅ¡ke o kontaktima + label_deal_note_plural: BeleÅ¡ke o pozivima + label_crm_contact_all_note_plural: Sve beleÅ¡ke + error_unable_delete_deal_status: Nije moguće izbrisati pozive + label_crm_contacts_hidden: Sakrivena podeÅ¡avanja + + #3.1.0 + label_crm_contact_added: Dodati kontakti + label_crm_note_added: Dodato obaveÅ¡tenje + label_crm_deal_added: Dodan poziv + label_crm_deal_updated: Poziv obnovljen + text_crm_contact_added: "Autor %{name} je dodao ime kontakta %{author}." + text_crm_deal_added: "Autor %{name} je dodao poziv %{author}." + text_crm_deal_status_changed: "Status poziva je promenjen iz %{old} u %{new}" + text_crm_deal_updated: "Autor je obnovio pozive %{author}." + label_crm_contacts_cc: CC + label_crm_contacts_bcc: Bcc + label_crm_contact_import: Uneti iz CSV + permission_import_deals: Uvezi dpgpvpre + label_crm_single_quotes: "Apostorf (')" + label_crm_double_quotes: Dvostruki navodnici (\")" + label_crm_quotes_type: Tip citiranja + label_crm_contacts_visibility: Vidljivost + label_crm_contacts_visibility_project: Vidljivost u poslovima + label_crm_contacts_visibility_public: Javno + label_crm_contacts_visibility_private: Privatno + permission_view_private_contacts: Pregled privatnih kontakta + text_crm_error_on_line: 'GreÅ¡ka na liniji %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Verovatnoća + label_crm_deal_status_type: Vrsta statusa + label_crm_select_companies: FilteriÅ¡i kompanije u dogovoru + label_crm_expected_revenue: OÄekivani prihod + label_crm_deal_due_date: Datum isteka + + #3.2.2 + label_crm_show_deals_in_top_menu: Prikaz poziva u glavnom meniju + label_crm_show_details: Detaljnije + label_crm_has_deals: Poziv na Äekanju + label_crm_has_open_issues: PoteÅ¡koće + label_crm_note: obaveÅ¡tenja + + #3.2.5 + notice_failed_to_save_contacts: "GreÅ¡ka pri snimanju %{count} kontakata na %{total} selektovanih: %{ids}." + + #3.2.6 + project_module_deals: Pozivi + permission_manage_deals: Upravljanje pozivima + label_crm_deals_from_subprojects: Prikaz poziva u subprojektima + label_crm_megre_tags: Ujediniti + label_crm_monochrome_tags: Crnobeli prikaz + + #3.2.7 + label_crm_address: Adresa + label_crm_street1: Ulica 1 + label_crm_street2: Ulica 2 + label_crm_city: Grad + label_crm_region: 'Država' + label_crm_postcode: 'ZIP' + label_crm_country: Država + label_crm_countries: + AF: Avganistan + AL: Albanija + DZ: Algerija + AS: AmeriÄka Samoa + AD: Andora + AO: Angola + AI: Anguilla + AQ: Antarktika + AG: Antigua and Barbuda + AR: Argentina + AM: Jermenija + AW: Aruba + AU: Australija + AT: Austrija + AZ: Azerbejcan + BS: Bahami + BH: Bahrein + BD: BangladeÅ¡ + BB: Barbados + BY: Bjelorusija + BE: Belgija + BZ: Belice + BJ: Benin + BM: Bermuda + BT: Butan + BO: Bolivija + BA: Bosna i Hercegovina + BW: Bocvana + BV: Bouvet Island + BR: Brazil + BQ: Britanska Antartik teritorija + IO: Britanska teritiorija na Indijskom okeanu + VG: Britanske DjeviÄanska Ostrva + BN: Bruneja + BG: Bugarska + BF: Burkina Faso + BI: Burundi + KH: Kamboca + CM: Kamerun + CA: Kanada + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Kajmanska ostrva + CF: Centralna AfriÄka Republika + TD: ÄŒad + CL: ÄŒile + CN: Kina + CX: Christmas Island + CC: 'Cocos [Keeling] Islands' + CO: Kolumbija + KM: KomoraÄ + CG: Kongo + CD: Kongo - Kinsaza + CK: Cook Islands + CR: Kostarika + HR: Hrvatska + CU: Kuba + CY: Kipar + CZ: ÄŒeÅ¡ka republika + CI: Obala SlonovaÄe + DK: Danska + DJ: Džibuti + DM: Dominika + DO: Dominikanska republika + NQ: Dronning Maud Land + DD: IstoÄna NjjemaÄka + EC: Ekvador + EG: Egipt + SV: El Salvador + GQ: Ekvatorijalna Gvineja + ER: Eritreja + EE: Estonija + ET: Etiopija + FK: Foklandska ostrva + FO: Farska ostrva + FJ: Fidži + FI: Finska + FR: Francuska + GF: Francuska Gvajana + PF: Francuska Polinezija + TF: Francuske Južne teritorije + FQ: Južna Francuska i AntartiÄke teritorije + GA: Gabon + GM: Gambija + GE: Gruzija + DE: NjjemaÄka + GH: Gana + GI: Gibraltar + GR: GrÄka + GL: Grenland + GD: Granada + GP: Gvadelupe + GU: Gam + GT: Guatemala + GG: Guernsey + GN: Gvineja + GW: Gvineja-Bisej + GY: Gvajana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: Hong Kong + HU: MaÄ‘arska + IS: Island + IN: Indija + ID: Indonezija + IR: Iran + IQ: Irak + IE: Irska + IM: Isle of Man + IL: Izrael + IT: Italija + JM: Jamajka + JP: Japan + JE: Džersi + JT: Johnston Island + JO: Džordan + KZ: Kazastan + KE: Kenija + KI: Kiribati + KW: Kuvajt + KG: Kirgistan + LA: Laos + LV: Latvija + LB: Leban + LS: Lesoto + LR: Liberija + LY: Libija + LI: LihtenÅ¡tajn + LT: Litvanija + LU: Luksemburg + MO: Makao Kina + MK: Makedonija + MG: Madagaskar + MW: Malavi + MY: Malezija + MV: Maldivi + ML: Mali + MT: Malta + MH: MarÅ¡alska Ostrva + MQ: Martinique + MR: Mauritanija + MU: Mauricijus + YT: Majoti + FX: Metropolitanska Francuska + MX: Meksiko + FM: Mikronezija + MI: Midway Islands + MD: Moldavija + MC: Monako + MN: Mongolija + ME: Crna Gora + MS: Monserat + MA: Maroko + MZ: Mozambija + MM: Burma + NA: Nambija + NR: Nauru + NP: Nepal + NL: Holandija + AN: Netherlands Antilles + NT: Neutralane zone + NC: Nova Kaledonija + NZ: Novi Zeland + NI: Nikagara + NE: Niger + NG: Nigerija + NU: Niue + NF: Norfolk Island + KP: Sjeverna Koreja + VD: Sjeverni Vijetnam + MP: Sjeverna Marijanska Ostrva + "NO": NorveÅ¡ka + OM: Oman + PC: PacifiÄka ostrva + PK: Pakistan + PW: Palu + PS: Palestinske teritorije + PA: Panama + PZ: Zona Panamskog kanala + PG: Nova Gvineja + PY: Paragvaj + YD: Jeman + PE: Peru + PH: Filipini + PN: Pitcairn Islands + PL: Poljska + PT: Portugal + PR: Portoriko + QA: Katar + RO: Rumunija + RU: Rusija + RW: Ruanda + RE: Réunion + BL: Sveti Bartolomej + SH: Sveta Helena + KN: Saint Kitts and Nevis + LC: Sveta Lucija + MF: Sveti Martin + PM: Sveti Pijer i Migelon + VC: Sveti Vinsent + WS: Samoa + SM: San Marino + SA: Saudijaska Arabija + SN: Senegal + RS: Srbija + CS: Srbija + SC: Seychelles + SL: Sijera Leone + SG: Singapur + SK: SlovaÄka + SI: Slovenija + SB: Solomonska ostrva + SO: Somalija + ZA: Južna Afrika + GS: Južna Gruzija + KR: Južna Koreja + ES: Å panija + LK: Å ri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Å vicarska + SE: Å vedska + CH: Å vajcarska + SY: Sirija + ST: São Tomé and Príncipe + TW: Tajvan + TJ: Tadžkistan + TZ: Tanzanija + TH: Tajland + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad i Tobago + TN: Tunizija + TR: Turska + TM: Turmekistan + TC: Turks and Caicos Islands + TV: Tuvala + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: DjeviÄanska ostrva-Sjedinjene države + UG: Uganda + UA: Ukrajina + AE: Ujedinjeni Emirati + GB: Ujedinjeno Kraljevstvo + US: Sjedinjene države + ZZ: Nepoznat ili ne postojeći region + UY: Urugvaj + UZ: Uzbekistan + VU: Vanutu + VA: Vatikan siti + VE: Venecuela + VN: Vijetnam + WK: Wake Island + WF: Wallis and Futuna + EH: Zapadna Sahara + YE: Jemen + ZM: Zambija + ZW: Zimbabve + AX: Ã…land Islands + label_crm_cross_project_contacts: Omogućiti projekte meÄ‘u povezanim kontaktima + label_crm_list_board: Radna povrÅ¡ina + label_crm_default_list_style: Onemogućen stil liste + label_crm_show_in_top_menu: Prikazati u glavnom meniju + label_crm_show_in_app_menu: Prikazati u meniju aplikacija + label_crm_money_settings: Novac + label_crm_disable_taxes: Onemogućiti porez + label_crm_default_tax: Onemogućena vrednost porez + label_crm_tax_type: Vrsta poreza + label_crm_tax_type_inclusive: ukljuÄiti porez + label_crm_tax_type_exclusive: bez poreza + label_crm_default_currency: IstiÄe + label_crm_thousands_delimiter: OgraniÄenje do hiljadu + label_crm_decimal_separator: Decimalni zarez + + #3.2.10 + label_crm_add_contact_plural: Dodati kontakte + label_crm_search_for_contact: Pretraga kontakta + label_crm_major_currencies: Glavne valute + + #3.2.11 + label_crm_post_address_format: Å ablon za adresu + label_crm_post_address_format_macros: "Address format macros: %{macros}" + + #3.2.14 + label_crm_last_year: ProÅ¡le godine + + #3.2.15 + permission_export_contacts: Uneti kontakte i pozive + + #3.4.0 + label_crm_deal_contact: Birani kontakti \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/sr.yml b/plugins/redmine_contacts/config/locales/sr.yml new file mode 100644 index 0000000..4ea83f8 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/sr.yml @@ -0,0 +1,593 @@ +sr: + contacts_title: Контакти + + label_crm_recently_viewed: Ðедавно гледано + label_crm_gravatar_enabled: КориÑтити Граватер + label_crm_thumbnails_enabled: Приказати рад-Ñлику у биљешкама + label_crm_max_thumbnail_file_size: МакÑимална величина Ñлике + label_crm_view_all_contacts: Преглед Ñвих контакта + label_crm_background_info: Информације о позадини(екрана) + label_crm_company: Компанија + label_contact_plural: Контакти + label_crm_contact_edit_information: Уредити информације о контактима + label_crm_edit_tags: Уредити рад(декларацију) + label_crm_contact_view: Преглед + label_crm_contact_list: ЛиÑта + label_crm_contact_new: Ðови контакти + label_crm_at_company: у-на + label_crm_last_notes: Ðедавне биљешке + label_crm_tags_plural: Информације-објашњење + label_crm_multi_tags_plural: Изабрати неколико објашњења + label_crm_single_tag_mode: Једно објашњење + label_crm_multiple_tags_mode: Више објашњења + label_crm_contact_tag: Информације-објашњење + label_crm_time_ago: прије + label_crm_add_note_plural: Додати биљешку за + label_crm_note_plural: Биљешке + + label_crm_add_tags_rule: одвојено зарезима + label_crm_contact_search: претрага по именима + label_crm_note_for: Биљешка за + label_crm_show_on_map: Приказ на мапи-папиру + label_crm_add_another_phone: додати број телефона + label_crm_remove: обриÑати + label_crm_related_contacts: повезани контактн + label_crm_assigned_to: Одговоран + label_crm_issue_added: Додато задатак + label_crm_add_emails_rule: одвојити зарезима + label_crm_add_phones_rule: одвојити зарезима + label_crm_add_employee: Ðови запоÑленик + label_crm_merge_duplicate_plural: Интегрирати-Ñјединити Ñе + label_crm_duplicate_plural: Могући дупликати + label_crm_duplicate_for_plural: Могући дупликати за + label_crm_add_tag: + додати објашњење + + label_crm_note_show_extras: Ðапредан ниво + label_crm_note_hide_extras: Скривен ниво + label_crm_note_added: Доадата забиљешка + label_crm_note_read_more: Више информација + label_crm_invoice_import: Унијети рачуне из ЦСВ + + label_deal_plural: Говори о + label_crm_contractor_plural: Контакти + label_deal: Позив + label_crm_deal_new: Ðови позив + label_crm_deal_edit_information: Уредити информације о позивима + label_crm_deal_change_status: Промјенити ÑÑ‚Ð°Ñ‚ÑƒÑ + label_crm_statistics: СтатиÑтика + label_crm_deals_import: Унијети позив из ЦСВ + + label_crm_deal_status_new: Ðови + label_crm_deal_status_first_contact: Први контакт + label_crm_deal_status_negotiations: Преговарања + label_crm_deal_status_pending: за, за вријеме + label_crm_deal_status_won: ОÑвојити + label_crm_deal_status_lost: Изгубљено + + label_crm_created_on: Креирати на + + field_note_date: Датум забиљешке + field_background: Позадина-подлога + field_currency: Рок трајања-валута + field_contact: Контакт + + field_deal_name: Име + field_deal_background: Позадина + field_deal_contact: Контакт + field_deal_price: Сума + field_price: Сума + + field_contact_avatar: Инкарнација божанÑтва + field_contact_is_company: Компанија + field_contact_name: Име + field_contact_last_name: Преѕиме + field_contact_first_name: Име + field_contact_middle_name: Друго име + field_contact_job_title: Ðазив поÑла + field_contact_company: Компанија + field_contact_address: AдреÑа + field_contact_phone: Телфон + field_contact_email: Имејл + field_contact_website: ВебÑајт + field_contact_skype: Скајп + field_contact_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ + field_contact_background: Позадина + field_contact_tag_names: Објашњења-коментари + field_first_name: Име + field_last_name: Презиме + field_company: Компанија + field_birthday: Датум рођења + field_contact_department: ОдÑјек + + + field_company_field: ДјелатноÑÑ‚ + + field_color: Боја + + button_add_note: Додати забиљешку + notice_successful_save: УÑпјешно Ñачувано + notice_successful_add: УÑпјешно креирано + notice_unsuccessful_save: Сачувати грешке-могуће проблеме + notice_successful_merged: УÑпјешно Ñјединити + + notice_merged_warning: Све забиљешке, пројекти, објашњења и задатци додјељени овој оÑоби ће бити додати изабраној оÑоби у наÑтавку. Контакт ће бити избриÑан. + + project_module_contacts: Контакти + + permission_view_contacts: Преглед контакта + permission_edit_contacts: Уредити контакте + permission_delete_contacts: ИзбриÑати контакте + permission_view_deals: Преглед разговора + permission_edit_deals: Уредити разговоре + permission_delete_deals: ИзбриÑати разговоре + permission_add_notes: Додати биљешке + permission_delete_notes: Изабрати биљешке + permission_delete_own_notes: Изабрати влаÑтите биљешке + + # 2.0.0 + label_crm_deal_category: Категорија позива + label_crm_deal_category_plural: Категорија позива + label_crm_deal_category_new: Ðова категорија + text_deal_category_destroy_assignments: ПремјеÑтити-уклонити задатке према категоријама + text_deal_category_destroy_question: "Ðеки позиви (% проценти) Ñу додјељени овој категорији. Шта желите урадити?" + text_deal_category_reassign_to: ПрераÑподјелти позиве овој категорији + text_deals_destroy_confirmation: 'Да ли Ñте Ñигурни да жечтие избриÑати овај позив-е?' + label_crm_deal_status_plural: СтатуÑни позиви + label_crm_deal_status: СтатуÑни позив + field_deal_status_is_closed: Завршено-затворено + label_crm_deal_status_new: Ðови + permission_manage_contacts: Управљање контактима + label_crm_sales_funnel: Sales funnel + label_crm_period: Период + label_crm_count: Број + + #2.0.1 + label_crm_user_format: Формат имена контакта + label_crm_my_contact_plural: Контакти који Ñу ми додјељени + label_crm_my_deal_plural: Позиви додјељени мени + label_crm_contact_view_all: Преглед Ñвих контакта + label_crm_deal_view_all: Преглед Ñвих позива + + #2.0.2 + label_crm_bulk_edit_selected_contacts: ИзбриÑати Ñве одабране позиве + label_crm_bulk_edit_selected_deals: Уредити Ñве одабране позиве + label_crm_bulk_send_mail_selected_contacts: ПоÑлати мејл одабраним контактима + field_add_tags: Додати ообјашњења + field_delete_tags: ИзбриÑати објашњења + label_crm_send_mail: ПоÑлати мејл + error_empty_email: Имејл не може бити празан + permission_send_contacts_mail: ПоÑлати мејл + field_mail_from: ÐдреÑа + text_email_macros: Avaliable macros %{macro} + field_message: Порука + + #2.0.3 + label_crm_add_contact: Додати контакт + label_contact: Контакт + field_age: Године + label_crm_vcf_import: Пребацити из Ð’-кард + label_crm_mail_from: из-од + permission_import_contacts: Унијети контакте + + #2.1.0 + field_company_name: Име компаније + label_crm_recently_added_contacts: Ðедавно додати контакти + label_crm_created_by_me: Контакти које Ñам креирао + my_contacts: Моји контакти + my_deals: Моји позиви + + #2.2.0 + label_crm_note_type_email: Имејл + label_crm_note_type_call: Позив + label_crm_note_type_meeting: СаÑтанак + field_deal_currency: Вријеме трајања + label_crm_my_contacts_stats: Извјештај о контактима за овај мјеÑец + label_crm_contacts_created: Креирани контакти + label_crm_deals_created: Креирани позиви + my_contacts_avatars: Слике мојих контакта + my_contacts_stats: Извјештај о контактима + label_crm_add_into: Додати у + label_crm_delete_from: ИзбриÑати из + label_crm_show_deaks_tab: Приказ позива + label_crm_show_on_projects_show: Приказ контакта на пројекторÑком прегледу + + #2.2.1 + label_crm_contacts_show_in_list: Приказати на лиÑти + + #2.3.0 + label_crm_module_plural: Mодули + label_crm_list_partial_style: Стил лиÑте + label_crm_list_excerpt: Приказ лиÑте + label_crm_list_cards: Картице + label_crm_list_list: ÐаÑлов + field_contacts: Контакт + field_companies: Компанија + label_crm_added_by: Додано + label_crm_contact_note_authoring_time: Приказати вријеме + label_crm_contact_issues_filters: Issues filters?????????? + label_crm_csv_import: Унијети из ЦСВ + label_crm_upload_encoding: Декодиранје фајла + label_crm_csv_file: ЦСВ фајл + label_crm_csv_separator: Зарез + field_middle_name: Друго име + field_job_title: Ðазив поÑла + field_company: Компанија + field_address: AдреÑа + field_phone: Телфон + field_email: Имејл + field_tags: Објашњења-коментари + field_last_note: ПоÑљедње обавјештење + field_is_company: је компанија?? + field_contact_full_name: Пуно име + button_contacts_edit_query: Уредити питања + button_contacts_delete_query: ИзбриÑати питања + permission_manage_public_contacts_queries: Управљати питањима + permission_add_deals: Доадати позиве + permission_add_contacts: Доадати контакте + permission_save_contacts_queries: Сачувати питања + + #2.3.3 + label_crm_contact_show_in_app_menu: Приказати прозоре у меију апликација + + #2.3.4 + label_crm_contact_show_closed_issues: Приказ ријешених проблема + + #3.0.0 + label_crm_import: Унијети-увеÑти + label_contact_note_plural: Биљешке о контактима + label_deal_note_plural: Биљешке о позивима + label_crm_contact_all_note_plural: Све биљешке + error_unable_delete_deal_status: Ðије могуће избриÑати позиве + label_crm_contacts_hidden: Скривена подешавања + + #3.1.0 + label_crm_contact_added: Доадани контакти + label_crm_note_added: Додано обавјештење + label_crm_deal_added: Додан позив + label_crm_deal_updated: Позив обновљен + text_crm_contact_added: "Ðутор %{name} је додао име контакта %{author}." + text_crm_deal_added: "Ðутор %{name} је додао позив %{author}." + text_crm_deal_status_changed: "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ð¾Ð·Ð¸Ð²Ð° је промјејењен из %{old} у %{new}" + text_crm_deal_updated: "Ðутор је обновио позиве %{author}." + label_crm_contacts_cc: ЦЦ + label_crm_contacts_bcc: Бцц + label_crm_contact_import: Унијети из ЦСВ + permission_import_deals: Import deals + label_crm_single_quotes: "ÐпоÑторф (')" + label_crm_double_quotes: ДвоÑтруки наводници (\")" + label_crm_quotes_type: Тип цитирања + label_crm_contacts_visibility: ВидљивоÑÑ‚ + label_crm_contacts_visibility_project: + label_crm_contacts_visibility_public: Јавно + label_crm_contacts_visibility_private: Приватно + permission_view_private_contacts: Преглед приватних контакта + text_crm_error_on_line: 'Грешка на линији %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Вјероватноћа + label_crm_deal_status_type: Ð’Ñ€Ñта ÑтатуÑа + label_crm_select_companies: Filter companies in deal???????? + label_crm_expected_revenue: Очекивани приход + label_crm_deal_due_date: Датум иÑтека + + #3.2.2 + label_crm_show_deals_in_top_menu: Приказ позива у главном менију + label_crm_show_details: Детаљније + label_crm_has_deals: Позив на чекању + label_crm_has_open_issues: Потешкоће + label_crm_note: Обавјештења + + #3.2.5 + notice_failed_to_save_contacts: "Грешка при Ñнимању %{count} контаката на %{total} Ñелектованих: %{ids}." + + #3.2.6 + project_module_deals: Позиви + permission_manage_deals: Управљање позивима + label_crm_deals_from_subprojects: Приказ позива у Ñубпројектима + label_crm_megre_tags: Ујединити + label_crm_monochrome_tags: Црнобијели приказ + + #3.2.7 + label_crm_address: AдреÑа + label_crm_street1: Улица 1 + label_crm_street2: Улица 2 + label_crm_city: Град + label_crm_region: 'Држава' + label_crm_postcode: 'ЗИП' + label_crm_country: Држава + label_crm_countries: + AF: AвганиÑтан + AL: Alбанија + DZ: Aлгерија + AS: Aмеричка Самоа + AD: Aндора + AO: Ðнгола + AI: Anguilla + AQ: Ðнтарктика + AG: Antigua and Barbuda + AR: Ðргентина + AM: Јерменија + AW: Ðруба + AU: ÐуÑтралија + AT: ÐуÑтрија + AZ: Ðзербеjцан + BS: Бахами + BH: Бахреин + BD: Бангладеш + BB: Ð‘Ð°Ñ€Ð±Ð°Ð´Ð¾Ñ + BY: БјелоруÑија + BE: Белгија + BZ: Белице + BJ: Бенин + BM: Бермуда + BT: Бутан + BO: Боливија + BA: БоÑна и Херцеговина + BW: Боцвана + BV: Bouvet Island + BR: Бразил + BQ: БританÑка Ðнтартик територија + IO: БританÑка теритиорија на ИндијÑком океану + VG: БританÑке ДјевичанÑка ОÑтрва + BN: Брунеја + BG: БугарÑка + BF: Буркина ФаÑо + BI: Бурунди + KH: Камбоца + CM: Камерун + CA: Канада + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: КајманÑка оÑтрва + CF: Централна Ðфричка Република + TD: Чад + CL: Чиле + CN: Кина + CX: Christmas Island + CC: 'Cocos [Keeling] Islands' + CO: Колумбија + KM: Коморач + CG: Конго + CD: Конго - КинÑаза + CK: Cook Islands + CR: КоÑтарика + HR: ХрватÑка + CU: Куба + CY: Кипар + CZ: Чешка република + CI: Обала Слоноваче + DK: ДанÑка + DJ: Ðибути + DM: Доминика + DO: ДоминиканÑка република + NQ: Dronning Maud Land + DD: ИÑточна Њјемачка + EC: Еквадор + EG: Египт + SV: Ел Салвадор + GQ: Екваторијална Гвинеја + ER: Еритреја + EE: ЕÑтонија + ET: Етиопија + FK: ФокландÑка оÑтрва + FO: ФарÑка оÑтрва + FJ: Фиџи + FI: ФинÑка + FR: ФранцуÑка + GF: ФранцуÑка Гвајана + PF: ФранцуÑка Полинезија + TF: ФранцуÑке Јужне територије + FQ: Јужна ФранцуÑка и Ðнтартичке територије + GA: Габон + GM: Гамбија + GE: Грузија + DE: Њјемачка + GH: Гана + GI: Гибралтар + GR: Грчка + GL: Гренланд + GD: Гранада + GP: Гваделупе + GU: Гам + GT: Гуатемала + GG: Guernsey + GN: Гвинеја + GW: Гвинеја-БиÑеј + GY: Гвајана + HT: Хаити + HM: Heard Island and McDonald Islands + HN: Ð¥Ð¾Ð½Ð´ÑƒÑ€Ð°Ñ + HK: Хонг Конг + HU: МађарÑка + IS: ИÑланд + IN: Индија + ID: Индонезија + IR: Иран + IQ: Ирак + IE: ИрÑка + IM: Isle of Man + IL: Израел + IT: Италија + JM: Јамајка + JP: Јапан + JE: ÐерÑи + JT: Johnston Island + JO: Ðордан + KZ: КазаÑтан + KE: Кенија + KI: Кирибати + KW: Кувајт + KG: КиргиÑтан + LA: Ð›Ð°Ð¾Ñ + LV: Латвија + LB: Лебан + LS: ЛеÑото + LR: Либерија + LY: Либија + LI: Лихтенштајн + LT: Литванија + LU: ЛукÑембург + MO: Макао Кина + MK: Македонија + MG: МадагаÑкар + MW: Малави + MY: Малезија + MV: Малдиви + ML: Мали + MT: Малта + MH: МаршалÑка ОÑтрва + MQ: Martinique + MR: Мауританија + MU: ÐœÐ°ÑƒÑ€Ð¸Ñ†Ð¸Ñ˜ÑƒÑ + YT: Мајоти + FX: МетрополитанÑка ФранцуÑка + MX: МекÑико + FM: Микронезија + MI: Midway Islands + MD: Молдавија + MC: Монако + MN: Монголија + ME: Црна Гора + MS: МонÑерат + MA: Мароко + MZ: Мозамбија + MM: Бурма + NA: Ðамбија + NR: Ðауру + NP: Ðепал + NL: Холандија + AN: Netherlands Antilles + NT: Ðеутралане зоне + NC: Ðова Каледонија + NZ: Ðови Зеланд + NI: Ðикагара + NE: Ðигер + NG: Ðигерија + NU: Ðиуе + NF: Norfolk Island + KP: Сјеверна Кореја + VD: Сјеверни Вијетнам + MP: Сјеверна МаријанÑка ОÑтрва + "NO": Ðорвешка + OM: Оман + PC: Пацифичка оÑтрва + PK: ПакиÑтан + PW: Палу + PS: ПалеÑтинÑке територије + PA: Панама + PZ: Зона ПанамÑког канала + PG: Ðова Гвинеја + PY: Парагвај + YD: Јеман + PE: Перу + PH: Филипини + PN: Pitcairn Islands + PL: ПољÑка + PT: Португал + PR: Порторико + QA: Катар + RO: Румунија + RU: РуÑија + RW: Руанда + RE: Réunion + BL: Свети Бартоломеј + SH: Света Хелена + KN: Saint Kitts and Nevis + LC: Света Луција + MF: Свети Мартин + PM: Свети Пијер и Мигелон + VC: Свети ВинÑент + WS: Самоа + SM: Сан Марино + SA: СаудијаÑка Ðрабија + SN: Сенегал + RS: Србија + CS: Србија + SC: Seychelles + SL: Сијера Леоне + SG: Сингапур + SK: Словачка + SI: Словенија + SB: СоломонÑка оÑтрва + SO: Сомалија + ZA: Јужна Ðфрика + GS: Јужна Грузија + KR: Јужна Кореја + ES: Шпанија + LK: Шри Ланка + SD: Судан + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: ШвицарÑка + SE: ШведÑка + CH: ШвајцарÑка + SY: Сирија + ST: São Tomé and Príncipe + TW: Тајван + TJ: ТаџкиÑтан + TZ: Танзанија + TH: Тајланд + TL: Timor-Leste + TG: Того + TK: Tokelau + TO: Тонга + TT: Тринидад и Тобаго + TN: Тунизија + TR: ТурÑка + TM: ТурмекиÑтан + TC: Turks and Caicos Islands + TV: Тувала + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: ДјевичанÑка оÑтрва-Сједињене државе + UG: Уганда + UA: Украјина + AE: Уједињени Емирати + GB: Уједињено КраљевÑтво + US: Сједињене државе + ZZ: Ðепознат или не поÑтојећи регион + UY: Уругвај + UZ: УзбекиÑтан + VU: Вануту + VA: Ватикан Ñити + VE: Венецуела + VN: Вијетнам + WK: Wake Island + WF: Wallis and Futuna + EH: Западна Сахара + YE: Јемен + ZM: Замбија + ZW: Зимбабве + AX: Ã…land Islands + label_crm_cross_project_contacts: Омогућити пројекте међз повезаним контактима + label_crm_list_board: Радна површина + label_crm_default_list_style: Онемогућен Ñтил лиÑте + label_crm_show_in_top_menu: Приказати у главном менију + label_crm_show_in_app_menu: Приказати у менију апликација + label_crm_money_settings: Ðовац + label_crm_disable_taxes: Онемогућити такÑу + label_crm_default_tax: Онемогућена вриједноÑÑ‚ такÑе + label_crm_tax_type: Ð’Ñ€Ñта такÑе + label_crm_tax_type_inclusive: укључити такÑу + label_crm_tax_type_exclusive: без такÑе + label_crm_default_currency: ИÑтиче + label_crm_thousands_delimiter: Ограничење до хиљаду + label_crm_decimal_separator: Децимални зарез + + #3.2.10 + label_crm_add_contact_plural: Додати контакте + label_crm_search_for_contact: Претрага контакта + label_crm_major_currencies: Главне поÑтавке + + #3.2.11 + label_crm_post_address_format: Шаблон за адреÑу + label_crm_post_address_format_macros: "Address format macros: %{macros}" + + #3.2.14 + label_crm_last_year: Прошле године + + #3.2.15 + permission_export_contacts: Унијети контакте и позиве + + #3.4.0 + label_crm_deal_contact: Бирани контакти \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/sv.yml b/plugins/redmine_contacts/config/locales/sv.yml new file mode 100644 index 0000000..c8ab25c --- /dev/null +++ b/plugins/redmine_contacts/config/locales/sv.yml @@ -0,0 +1,610 @@ +# encoding: utf-8 +# Swedish translation of RedmineCRM Contacts plugin version 3.4.4 +# Updated 2014 Dec 14 by Khedron Wilk (khedron.wilk@gmail.com) +# Based on earlier translation by X up to version 2.3.3 +sv: + contacts_title: Kontakter + + label_crm_recently_viewed: Senast visade + label_crm_gravatar_enabled: Använd Gravatar + label_crm_thumbnails_enabled: Visa miniatyrbild i anteckningen + label_crm_max_thumbnail_file_size: Max bildstorlek + label_crm_view_all_contacts: Se alla kontakter + label_crm_background_info: Bakgrundsinformation + label_crm_company: Företag + label_contact_plural: Kontakter + label_crm_contact_edit_information: Redigera kontaktinformation + label_crm_edit_tags: Ändra etiketter + label_crm_contact_view: Visa + label_crm_contact_list: Lista + label_crm_contact_new: Ny kontakt + label_crm_at_company: vid + label_crm_last_notes: Senaste anteckning + label_crm_tags_plural: Etiketter + label_crm_multi_tags_plural: Välj flera etiketter + label_crm_single_tag_mode: En etikett + label_crm_multiple_tags_mode: Flera etiketter + label_crm_contact_tag: Etikett + label_crm_time_ago: sedan + label_crm_add_note_plural: Lägg till anteckning i + label_crm_note_plural: Anteckningar + + label_crm_add_tags_rule: avgränsa med komma + label_crm_contact_search: Sök pÃ¥ namn + label_crm_note_for: Notering för + label_crm_show_on_map: Visa pÃ¥ karta + label_crm_add_another_phone: lägg till telefonnummer + label_contact_note_plural: Alla anteckningar + label_crm_remove: ta bort + label_crm_related_contacts: Relaterade kontakter + label_crm_assigned_to: Ansvarig + label_crm_issue_added: Ärende tillagt + label_crm_add_emails_rule: avgränsa med komma + label_crm_add_phones_rule: avgränsa med komma + label_crm_add_employee: Ny anställd + label_crm_merge_duplicate_plural: sammanfoga + label_crm_duplicate_plural: Möjliga dubbletter + label_crm_duplicate_for_plural: Möjliga dubbletter för + label_crm_add_tag: Lägg till ny... + + label_crm_note_show_extras: Avancerat (typ, datum, filer) + label_crm_note_hide_extras: Göm Avancerat + label_crm_note_added: Noteringen lades till + label_crm_note_read_more: (läs mer) + label_crm_invoice_import: Importera fakturor frÃ¥n CSV + + label_deal_plural: Affärsmöjligheter + label_crm_contractor_plural: Kontakter + label_deal: Affärsmöjlighet + label_crm_deal_new: Ny affärsmöjlighet + label_crm_deal_edit_information: Ändra affärsinformation + label_crm_deal_change_status: Byt status + label_crm_statistics: Statistik + + label_crm_deal_status_new: Ny + label_crm_deal_status_first_contact: Första kontakt + label_crm_deal_status_negotiations: Förhandling + label_crm_deal_status_pending: Väntar svar + label_crm_deal_status_won: Vunnen + label_crm_deal_status_lost: Förlorad + + label_crm_created_on: Skapad + + field_note_date: Anteckningsdatum + + field_deal_name: Namn + field_deal_background: Bakgrund + field_deal_contact: Kontakt + field_deal_price: Summa + field_price: Summa + + field_contact_avatar: Avatar + field_contact_is_company: Företag + field_contact_name: Namn + field_contact_last_name: Efternamn + field_contact_first_name: Förnamn + field_contact_middle_name: Mellannamn + field_contact_job_title: Jobbtitel + field_contact_company: Företag + field_contact_address: Adress + field_contact_phone: Telefonnr + field_contact_email: E-post + field_contact_website: Webbsida + field_contact_skype: Skype + field_contact_status: Status + field_contact_background: Bakgrund + field_contact_tag_names: Tags + field_first_name: Förnamn + field_last_name: Efternamn + field_company: Företag + field_birthday: Födelsedag + field_contact_department: Avdelning/Enhet + + + field_company_field: Bransch + + field_color: Färg + + button_add_note: Lägg till anteckning + notice_successful_save: Sparad + notice_successful_add: Skapad + notice_unsuccessful_save: Spara problem + notice_successful_merged: Sammanfogad + + notice_merged_warning: All anteckingar, projekt, taggar och uppgifter bifogade med denna kontakt kommer att bli flyttade till valet nedan. Kontakten blir sedan borttagen. + + project_module_contacts: Kontakter + + permission_view_contacts: Se kontakter + permission_edit_contacts: Redigera kontakter + permission_delete_contacts: Ta bort kontakter + permission_view_deals: Se affärsmöjlighet + permission_edit_deals: Redigera affärsmöjlighet + permission_delete_deals: Ta bort affärsmöjlighet + permission_add_notes: Lägg till antecking + permission_delete_notes: Ta bort anteckning + permission_delete_own_notes: Ta bort egna anteckingar + + # 2.0.0 + label_crm_deal_category: Kategori av affärsmöjlighet + label_crm_deal_category_plural: Kategorier av Affärsmöjligheter + label_crm_deal_category_new: Ny kategori + text_deal_category_destroy_assignments: Ta bort tilldelningar av kategorier + text_deal_category_destroy_question: "NÃ¥gra affärsmöjligheter (%{count}) är tilldelade till denna kategori. Vad vill du göra?" + text_deal_category_reassign_to: Omfördela affärsmöjligheter till denna kategori + text_deals_destroy_confirmation: 'Är du säker pÃ¥ att du vill ta bort valda affärsmöjligheter?' + label_crm_deal_status_plural: Status affärsmöjligheter + label_crm_deal_status: Status affärsmöjlighet + field_deal_status_is_closed: Stängd + label_crm_deal_status_new: Ny + permission_manage_contacts: Uppräkning + label_crm_sales_funnel: Säljkanalisering + label_crm_period: Period + label_crm_count: Sammanräkning + + #2.0.1 + label_crm_user_format: Format för kontaktnamn + label_crm_my_contact_plural: Kontakter som är tilldelade till mig + label_crm_my_deal_plural: Öppna affärsmöjligheter som är tilldelade till mig + label_crm_contact_view_all: Se alla kontakter + label_crm_deal_view_all: Se alla affärsmöjligheter + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Redigera alla valda kontakter + label_crm_bulk_edit_selected_deals: Redigera alla valda affärsmöjligheter + label_crm_bulk_send_mail_selected_contacts: Skicka e-post till alla valda kontakter + field_add_tags: Lägg till etiketter + field_delete_tags: Ta bort etiketter + label_crm_send_mail: Skicka e-post + error_empty_email: E-post kan inte vara tomt + permission_send_contacts_mail: Skicka e-post + field_mail_from: FrÃ¥nadress + text_email_macros: Tillgängliga macro %{macro} + field_message: Meddelande + + #2.0.3 + label_crm_add_contact: Lägg till kontakt + label_contact: Kontakt + field_age: Ã…lder + label_crm_vcf_import: Importera frÃ¥n vCard + label_crm_mail_from: FrÃ¥n + permission_import_contacts: Importera kontakter + + #2.1.0 + field_company_name: Företagsnamn + label_crm_recently_added_contacts: Senast tillagda kontaker + label_crm_created_by_me: Kontakter skapade av mig + my_contacts: Mina kontakter + my_deals: Mina affärsmöjligheter + + #2.2.0 + label_crm_note_type_email: E-post + label_crm_note_type_call: Samtal + label_crm_note_type_meeting: Möte + field_deal_currency: Valuta + label_crm_my_contacts_stats: Kontaktstatistik för denna mÃ¥nad + label_crm_contacts_created: Kontakter skapade + label_crm_deals_created: Affärsmöjligheter skapade + my_contacts_avatars: Mina kontakters foton + my_contacts_stats: Kontaktstatistik + label_crm_add_into: Lägg till i + label_crm_delete_from: Ta bort frÃ¥n + label_crm_show_deaks_tab: Visa affärsmöjlighetsfliken + label_crm_show_on_projects_show: Visa kontakter pÃ¥ projektets översikt + + #2.2.1 + label_crm_contacts_show_in_list: Visa i lista + + #2.3.0 + label_crm_module_plural: Moduler + label_crm_list_partial_style: Liststil för kontakter + label_crm_list_excerpt: Undantagslista + label_crm_list_cards: Kort + label_crm_list_list: Tabell + field_contacts: Kontakt + field_companies: Företag + label_crm_added_by: Tillagd av + label_crm_contact_note_authoring_time: Visa anteckningstid + label_crm_contact_issues_filters: Ärendefilter + label_crm_csv_import: Importera kontakter frÃ¥n CSV + label_crm_upload_encoding: Filkodning + label_crm_csv_file: CSV-fil + label_crm_csv_separator: Avdelare + field_middle_name: Mellannamn + field_job_title: Jobbtitel + field_company: Företag + field_address: Adress + field_phone: Telefon + field_email: E-postadress + field_tags: Etiketter + field_last_note: Senaste anteckning + field_is_company: Är företag + field_contact_full_name: Fullständigt namn + button_contacts_edit_query: Redigera frÃ¥ga + button_contacts_delete_query: Ta bort frÃ¥ga + permission_manage_public_contacts_queries: Hantera publika frÃ¥gor + permission_add_deals: Lägg till affärsmöjlighet + permission_add_contacts: Lägg till kontakt + permission_save_contacts_queries: Spara frÃ¥ga + + #2.3.3 + label_crm_contact_show_in_app_menu: Visa flik i applikationsmeny + + #2.3.4 + label_crm_contact_show_closed_issues: Visa stängda ärenden + + #3.0.0 + label_crm_import: Importera + label_contact_note_plural: Kontaktanteckningar + label_deal_note_plural: Affärsmöjllighetsanteckningar + label_crm_contact_all_note_plural: Alla anteckningar + error_unable_delete_deal_status: Kan inte radera status för affärmsöjlighet + label_crm_contacts_hidden: Dolda inställningar + + #3.1.0 + label_crm_contact_added: Kontakt tillagd + label_crm_note_added: Anteckning tillagd + label_crm_deal_added: Affärsmöjlighet tillagd + label_crm_deal_updated: Affärsmsöjlighet uppdaterad + text_crm_contact_added: "Kontakt %{name} har lagts till av %{author}." + text_crm_deal_added: "Affärsmöjlighet %{name} har lagts till av%{author}." + text_crm_deal_status_changed: "Status för affärsmöjllighet ändrad frÃ¥n %{old} till %{new}" + text_crm_deal_updated: "Affärsmöjlighet %{name} har uppdaterats av %{author}." + label_crm_contacts_cc: Kopia + label_crm_contacts_bcc: Dold kopia + label_crm_contact_import: Importera frÃ¥n CSV + permission_import_deals: Importera affärsmöjligheter + label_crm_single_quotes: "Apostrof (')" + label_crm_double_quotes: "Citationstecken (\")" + label_crm_quotes_type: Typ av citationstecken + label_crm_contacts_visibility: Synbarhet + label_crm_contacts_visibility_project: FrÃ¥n projektbehörigheter + label_crm_contacts_visibility_public: Publik + label_crm_contacts_visibility_private: Privat + permission_view_private_contacts: Visa privata kontakter + text_crm_error_on_line: 'Fel pÃ¥ rad %{line}: %{error}.' + + #3.2.0 + label_crm_probability: Sannolikhet + label_crm_deal_status_type: Statustyp + label_crm_select_companies: Filtera företag i affärsmöjlighet + label_crm_expected_revenue: Förväntad intäkt + label_crm_deal_due_date: Förfallodag + + #3.2.2 + label_crm_show_deals_in_top_menu: Visa affärsmöjligheter i toppmenyn + label_crm_show_details: Visa detaljer + label_crm_has_deals: Har affärsmöjligheter + label_crm_has_open_issues: Öppna ärenden + label_crm_note: Anteckning + + #3.2.5 + notice_failed_to_save_contacts: "Kunde inte spara %{count} kontakt(er) av %{total} valda: %{ids}." + + #3.2.6 + project_module_deals: Affärsmöjligheter + permission_manage_deals: Hantera affärsmöjligheter + label_crm_deals_from_subprojects: Visa affärsmöjligheter frÃ¥n underprojekt + label_crm_megre_tags: SlÃ¥ samman etiketter + label_crm_monochrome_tags: Svartvita etiketter + + #3.2.7 + label_crm_address: Adress + label_crm_street1: Gatuadress 1 + label_crm_street2: Gatuadress 2 + label_crm_city: Stad + label_crm_region: 'Stat' + label_crm_postcode: 'Postkod' + label_crm_country: Land + label_crm_countries: + AF: Afghanistan + AL: Albanien + DZ: Algeriet + AS: Amerikanska Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarktis + AG: Antigua och Barbuda + AR: Argentina + AM: Armenien + AW: Aruba + AU: Australien + AT: Österrike + AZ: Azerbajdzjan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + AV: Vitryssland + BE: Belgien + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnien och Hercegovina + BW: Botswana + BV: Bouvetön + BR: Brasilien + BQ: Brittiska antarktiska territoriet + IO: Brittiska Jungfruöarna + VG: Brittiska Jungfruöarna + BN: Brunei + BG: Bulgarien + BF: Burkina Faso + BI: Burundi + KH: Kambodja + CM: Kamerun + CA: Kanada + CT: Canton och Enderbury Islands + CV: Kap Verde + KY: Caymanöarna + CF: Centralafrikanska republiken + TD: TChad + CL: Chile + CN: Kina + CX: Julön + CC: Kokosöarna + CO: Colombia + KM: Komorerna + CG: Kongo-Brazzaville + CD: Kongo-Kinshasa + CK: Cooköarna + CR: Costa Rica + HR: Kroatien + CU: Kuba + CY: Cypern + CZ: Tjeckien + CI: Elfenbenskusten + DK: Danmark + DJ: Djibouti + DM: Dominica + DO: Dominikanska republiken + NQ: Dronning Maud Land + DD: Östtyskland + EG: Ecuador + EG: Egypten + SV: El Salvador + GQ: Ekvatorialguinea + ER: Eritrea + EE: Estland + ET: Etiopien + FK: Falklandsöarna + FO: Färöarna + FJ: Fiji + FI: Finland + FR: Frankrike + GF: Franska Guyana + PF: Franska Polynesien + TF: Franska Sydterritorierna + FQ: Franska sydliga och antarktiska omrÃ¥dena + GA: Gabon + GM: Gambia + GE: Georgien + DE: Tyskland + GH: Ghana + GI: Gibraltar + GR: Grekland + GL: Grönland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard- och McDonaldöarna + HN: Honduras + HK: Hongkong SAR Kina + HU: Ungern + IS: Island + IN: Indien + ID: Indonesien + IR: Iran + IQ: Irak + IE: Irland + IM: Isle of Man + IL: Israel + IT: Italien + JM: Jamaica + JP: Japan + JE: Jersey + JT: Johnstonön + JO: Jordanien + KZ: Kazakstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kirgizistan + LA: Laos + LV: Lettland + LB: Libanon + LS: Lesotho + LR: Liberia + LY: Libyen + LI: Liechtenstein + LT: Litauen + LU: Luxemburg + MO: Macao SAR Kina + MK: Makedonien + MG: Madagaskar + MW: Malawi + MY: Malaysia + MV: Maldiverna + ML: Mali + MT: Malta + MH: Marshallöarna + MQ: Martinique + MR: Mauretanien + MU: Mauritius + YT: Mayotte + FX: Fankrike, europeiska delen + MX: Mexiko + FM: Mikronesien + MI: Midwayöarna + MD: Moldavien + MC: Monaco + MN: Mongoliet + ME: Montenegro + MS: Montserrat + MA: Marocko + MZ: Moçambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Nederländerna + AN: Nederländska Antillerna + NT: Neutralzon + NC: Nya Kaledonien + NZ: Nya Zeeland + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolkön + KP: Nordkorea + VD: Nordvietnam + MP: Nordmarianerna + "NO": Norge + OM: Oman + PC: Stillahavsöarna - Litateritoriet + PK: Pakistan + PW: Palau + PS: Palestinska territorierna + PA: Panama + PZ: Panamakanalzonen + PG: Papua Nya Guinea + PY: Paraguay + YD: Jemen + PE: Peru + PH: Filippinerna + PN: Pitcairnöarna + PL: Polen + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Rumänien + RU: Ryssland + RW: Rwanda + RE: Réunion + BL: Saint-Barthélemy + SH: Saint Helena + KN: Saint Kitts och Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre och Miquelon + VC: Saint Vincent och Grenadinerna + WS: Samoa + SM: San Marino + SA: Saudiarabien + SN: Senegal + RS: Serbien + CS: Serbien och Montenegro + SC: Seychellerna + SL: Sierra Leone + SG: Singapore + SK: Slovakien + SI: Slovenien + SB: Salomonöarna + SO: Somalia + ZA: Sydafrika + GS: Sydgeorgien och Sydsandwichöarna + KR: Sydkorea + ES: Spanien + LK: Sri Lanka + SD: Sudan + SR: Surinam + SJ: Svalbard och Jan Mayen + SZ: Swaziland + SE: Sverige + CH: Schweiz + SY: Syrien + ST: São Tomé och Príncipe + TW: Taiwan + TJ: Tadzjikistan + TZ: Tanzania + TH: Thailand + TL: Östtimor + TG: Togo + TK: Tokelauöarna + TO: Tonga + TT: Trinidad och Tobago + TN: Tunisien + TR: Turkiet + TM: Turkmenistan + TC: Turks- och Caicosöarna + TV: Tuvalu + UM: USA:s yttre öar + PU: USA:s Stillhavsöar + VI: Amerikanska Jungfruöarna + UG: Uganda + UA: Ukraina + AE: Förenade Arabemiraten + GB: Storbritannien + US: USA + ZZ: Okänd eller Ogiltig Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatikanstaten + VE: Venezuela + VN: Vietnam + WK: Wakeön + WF: Wallis- och Futunaöarna + EH: Västsahara + YE: Jemen + ZM: Zambia + ZW: Zimbabwe + AX: Ã…land + label_crm_cross_project_contacts: TillÃ¥t kontaktrelationer mellan projekt + label_crm_list_board: Tavla + label_crm_default_list_style: Förvald liststil + label_crm_show_in_top_menu: Visa i toppmeny + label_crm_show_in_app_menu: Visa i appmeny + label_crm_money_settings: Pengar + label_crm_disable_taxes: Inaktivera skatter + label_crm_default_tax: Förvalt skattevärde + label_crm_tax_type: Skatttyp + label_crm_tax_type_inclusive: Inklusive skatt + label_crm_tax_type_exclusive: Exklusive skatt + label_crm_default_currency: Förvald valuta + label_crm_thousands_delimiter: Tusentalsavgränsare + label_crm_decimal_separator: Decimalavgränsare + + #3.2.10 + label_crm_add_contact_plural: Lägg till kontakter + label_crm_search_for_contact: Sök efter kontakt + label_crm_major_currencies: Större valutor + + #3.2.11 + label_crm_post_address_format: Postadressformat + label_crm_post_address_format_macros: "Adressformatmakron: %{macros}" + + #3.2.14 + label_crm_last_year: förra Ã¥ret + + #3.2.15 + permission_export_contacts: Exportera kontakter och affärsmöjligheter + + #3.4.0 + label_crm_deal_contact: Affärsmöjlighetens kontakter + + #3.4.1 + label_crm_default_country: Förvald land + label_attribute_of_contact: "Kontaktens %{name}" + label_crm_contact_country: Kontaktens land + label_crm_contact_city: Kontaktens stad + label_attribute_of_deals: "Affärsmöjlighetens %{name}" + + #3.4.2 + permission_manage_public_deals_queries: Hantera affärsmöjlighets publika förfrÃ¥gningar + permission_save_deals_queries: Spara affärsmöjlighets förfrÃ¥gningar + permission_manage_contact_issue_relations: Hantera ärenderelationer + + #3.4.4 + label_crm_pipeline: Pipeline + text_crm_no_deal_statuses_in_project: Ingen affärsmöjlighetsstatus i projekt diff --git a/plugins/redmine_contacts/config/locales/tr.yml b/plugins/redmine_contacts/config/locales/tr.yml new file mode 100644 index 0000000..e89a6ac --- /dev/null +++ b/plugins/redmine_contacts/config/locales/tr.yml @@ -0,0 +1,159 @@ +# encoding: utf-8 +tr: + contacts_title: KiÅŸiler + + label_crm_recently_viewed: Son görüntülenen + label_crm_gravatar_enabled: Gravatar kullan + label_crm_thumbnails_enabled: Notlarda küçük resimleri göster + label_crm_max_thumbnail_file_size: Maksimum küçük resim boyutu + label_crm_view_all_contacts: Tüm kiÅŸileri göster + label_crm_background_info: GeçmiÅŸ bilgisi + label_crm_company: Åžirket + label_contact_plural: KiÅŸi Listesi + label_crm_contact_edit_information: KiÅŸi Bilgisi Düzenleme + label_crm_edit_tags: Etiket düzenleme + label_crm_contact_view: Göster + label_crm_contact_list: Liste + label_crm_contact_new: Yeni + label_crm_at_company: at + label_crm_last_notes: Son notlar + label_crm_tags_plural: Etiketler + label_crm_multi_tags_plural: Çoklu etiket seç + label_crm_single_tag_mode: Tek etiket + label_crm_multiple_tags_mode: Çoklu etiket + label_crm_contact_tag: Etiket + label_crm_time_ago: önce + label_crm_add_note_plural: Not ekle + label_crm_note_plural: Notlar + + label_crm_add_tags_rule: virgül ile ayır + label_crm_contact_search: İsim ile ara + label_crm_note_for: Not -> + label_crm_show_on_map: Haritada göster + label_crm_add_another_phone: telefon ekle + label_contact_note_plural: Tüm notlar + label_crm_remove: Sil + label_crm_related_contacts: İlgili kiÅŸiler + label_crm_assigned_to: İlgili kiÅŸi + label_crm_issue_added: İş eklendi + label_crm_add_emails_rule: virgül ile ayır + label_crm_add_phones_rule: virgül ile ayır + label_crm_add_employee: Yeni çalışan + label_crm_merge_duplicate_plural: BirleÅŸtir + label_crm_duplicate_plural: Potansiyel çift kopyalar + label_crm_duplicate_for_plural: Potansiyel çift kopyalar -> + + label_crm_note_show_extras: İleri düzey (dosya(lar), zaman) + label_crm_note_hide_extras: İleri düzeyi sakla + label_crm_note_added: Not baÅŸarıyla eklendi + label_crm_note_read_more: (fazlasını göster) + + label_deal_plural: AnlaÅŸmalar + label_crm_contractor_plural: Yükleniciler + label_deal: AnlaÅŸma + label_crm_deal_new: Yeni anlaÅŸma + label_crm_deal_edit_information: AnlaÅŸma bilgisi düzenle + label_crm_deal_change_status: Durum deÄŸiÅŸtir + label_crm_deal_pending: Beklemede + label_crm_deal_won: Kazanıldı + label_crm_deal_lost: Kaybedildi + + + field_note_date: Not tarihi + + field_deal_name: İsim + field_deal_background: Bilgi + field_deal_contact: KiÅŸi + field_deal_price: Toplam + field_price: Toplam + + field_contact_avatar: Avatar + field_contact_is_company: Åžirket + field_contact_name: İsim + field_contact_last_name: Soyisim + field_contact_first_name: İlk isim + field_contact_middle_name: Orta isim + field_contact_job_title: Görev + field_contact_company: Åžirket + field_contact_address: Adres + field_contact_phone: Telefon + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Durum + field_contact_background: Bilgi + field_contact_tag_names: Etiketler + field_first_name: İsim + field_last_name: Soyisim + field_company: Åžirket + field_birthday: DoÄŸumgünü + field_contact_department: Bölüm + + field_company_field: İş alanı + + field_color: Renk + + button_add_note: Not ekle + notice_successful_save: BaÅŸarıyla kaydedildi. + notice_successful_add: BaÅŸarıyla yaratıldı. + notice_unsuccessful_save: Kayıt edilemedi. + notice_successful_merged: BaÅŸarıyla birleÅŸtirildi. + + notice_merged_warning: Bu kiÅŸi ile ilgili tüm proje, not ve etiketler seçilen kiÅŸiye taşınacak ve bu kiÅŸi silinecek. + + project_module_contacts: KiÅŸiler + + permission_view_contacts: KiÅŸileri göster + permission_edit_contacts: KiÅŸileri düzenle + permission_delete_contacts: KiÅŸileri sil + permission_view_deals: AnlaÅŸmaları göster + permission_edit_deals: AnlaÅŸmaları düzenle + permission_delete_deals: AnlaÅŸmaları sil + permission_add_notes: Not ekle + permission_delete_notes: Not sil + permission_delete_own_notes: Kendi notlarını sil + + # 2.0.0 + label_crm_deal_category: Deal category + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: New category + text_deal_category_destroy_assignments: Remove category assignments + text_deal_category_destroy_question: "Some deals (%{count}) are assigned to this category. What do you want to do?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'Are you sure you want to delete the selected deal(s)?' + label_crm_deal_status_plural: Deal statuses + label_crm_deal_status: Deal status + field_deal_status_is_closed: Closed + label_crm_deal_status_new: New + permission_manage_contacts: Enumerations + label_crm_sales_funnel: Sales funnel + label_crm_period: Perion + label_crm_count: Count + + #2.0.1 + label_crm_user_format: Contact name format + label_crm_my_contact_plural: Contacts assigned to me + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: View all contacts + label_crm_deal_view_all: View all deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Seçilen tüm kiÅŸileri düzenle + label_crm_bulk_edit_selected_deals: Seçilen tüm fırsatlar Düzenle + label_crm_bulk_send_mail_selected_contacts: Seçili kiÅŸileri, e-posta gönder + field_add_tags: Etiket ekleyin + field_delete_tags: Sil etiketleri + label_crm_send_mail: Posta gönder + error_empty_email: Email boÅŸ olamaz + permission_send_contacts_mail: Posta gönder + field_mail_from: Kimden adresi + text_email_macros: Makrolar% {macro} + field_message: Mesaj + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: İletiÅŸim + field_age: YaÅŸ + label_crm_vcf_import: vCard alma + label_crm_mail_from: + permission_import_contacts: KiÅŸileri al \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/vi.yml b/plugins/redmine_contacts/config/locales/vi.yml new file mode 100644 index 0000000..c73e7a2 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/vi.yml @@ -0,0 +1,230 @@ +# encoding: utf-8 +vi: + contacts_title: Danh bạ + + label_crm_recently_viewed: ÄÆ°á»£c xem gần đây + label_crm_gravatar_enabled: Sá»­ dụng hình biểu tượng + label_thumbnails_enabled: Hiện hình ảnh thu nhá» trong phần ghi chú + label_max_thumbnail_file_size: Kích thước tối Ä‘a cá»§a hình ảnh thu nhá» + label_crm_view_all_contacts: Xem tất cả danh bạ + label_crm_background_info: Thông tin Mô tả + label_crm_company: Công ty + label_contact_plural: Danh bạ + label_crm_contact_edit_information: Thay đổi danh bạ + label_crm_edit_tags: Thay đổi đánh dấu + label_crm_contact_view: View + label_crm_contact_list: List + label_crm_contact_new: Tạo danh bạ má»›i + label_crm_at_company: tại + label_crm_last_notes: Ghu chú má»›i nhất + label_crm_tags_plural: Äánh dấu + label_crm_multi_tags_plural: Chá»n nhiá»u đánh dấu + label_crm_single_tag_mode: Môt đánh dấu + label_crm_multiple_tags_mode: Nhiá»u đánh dấu + label_crm_contact_tag: đánh dấu + label_crm_time_ago: trước + label_crm_add_note_plural: Thêm ghi chú vào + label_crm_note_plural: Ghi chú + + label_crm_add_tags_rule: phân cách bằng dấu phẩy + label_crm_contact_search: Tìm kiếm theo tên + label_crm_note_for: Ghi chú cho + label_crm_show_on_map: Hiện trên bản đồ + label_crm_add_another_phone: thêm số Ä‘iện thoại + label_contact_note_plural: Tất cả ghi chú + label_crm_remove: xóa + label_crm_related_contacts: Danh bạ liên quan + label_crm_assigned_to: Chịu trách nhiệm + label_crm_issue_added: Vấn đỠđã được thêm + label_crm_add_emails_rule: phân cách bằng dấu phẩy + label_crm_add_phones_rule: phân cách bằng dấu phẩy + label_crm_add_employee: Nhân viên má»›i + label_crm_merge_duplicate_plural: Nhập + label_crm_duplicate_plural: Có khả năng bị trùng vá»›i + label_crm_duplicate_for_plural: Có khả năng bị trùng vá»›i (2) + label_crm_add_tag: Thêm má»›i... + + label_crm_note_show_extras: Advanced (type, date, files) + label_crm_note_hide_extras: Hide advanced + label_crm_note_added: The note is successfully added + label_crm_note_read_more: (xem thêm) + + label_deal_plural: Deals + label_crm_contractor_plural: Contacts + label_deal: Deal + label_crm_deal_new: New deal + label_crm_deal_edit_information: Edit deal information + label_crm_deal_change_status: Change status + label_crm_statistics: Statistics + + label_crm_deal_status_new: Má»›i + label_crm_deal_status_first_contact: First contact + label_crm_deal_status_negotiations: Negotiations + label_crm_deal_status_pending: Pending + label_crm_deal_status_won: Won + label_crm_deal_status_lost: Lost + + label_crm_created_on: Tạo lúc + + field_note_date: Ngày ghi chú + + field_deal_name: Tên + field_deal_background: Mô tả + field_deal_contact: Danh bạ + field_deal_price: Tổng + field_price: Tổng + + field_contact_avatar: Biểu tượng + field_contact_is_company: Công ty + field_contact_name: Tên + field_contact_last_name: Tên + field_contact_first_name: Há» + field_contact_middle_name: Chữ lót + field_contact_job_title: Chức danh + field_contact_company: Công ty + field_contact_address: Äịa chỉ + field_contact_phone: Äiện thoại + field_contact_email: Email + field_contact_website: Website + field_contact_skype: Skype + field_contact_status: Trạng thái + field_contact_background: Mô tả + field_contact_tag_names: Äánh dấu + field_first_name: Tên + field_last_name: Há» + field_company: Company + field_birthday: Ngày sinh + field_contact_department: Bá»™ phận + + + field_company_field: LÄ©nh vá»±c Công ty + + field_color: Màu + + button_add_note: Thêm ghi chú + notice_successful_save: Äã lưu thành công + notice_successful_add: Äã tạo thành công + notice_unsuccessful_save: Lưu bị lá»—i + notice_successful_merged: Nhập thành công + + notice_merged_warning: Tất cả các ghi chú, dá»± án, đánh dấu và các công việc liên quan đến ngưá»i này sẽ bị xóa. Thông tin liên lạc sẽ bị xóa. + + project_module_contacts: Danh bạ + + permission_view_contacts: Xem danh bạ liên lạc + permission_edit_contacts: Thay đổi danh bạ + permission_delete_contacts: Xóa các danh bạ + permission_view_deals: Xem hợp đồng + permission_edit_deals: Chỉnh hợp đồng + permission_delete_deals: Xóa hợp đồng + permission_add_notes: Thêm ghi chú + permission_delete_notes: Xóa ghi chú + permission_delete_own_notes: Xóa các ghi chú do mình tạo + + # 2.0.0 + label_crm_deal_category: Deal category + label_crm_deal_category_plural: Deals categories + label_crm_deal_category_new: New category + text_deal_category_destroy_assignments: Remove category assignments + text_deal_category_destroy_question: "Some deals (%{count}) are assigned to this category. What do you want to do?" + text_deal_category_reassign_to: Reassign deals to this category + text_deals_destroy_confirmation: 'Are you sure you want to delete the selected deal(s)?' + label_crm_deal_status_plural: Deal statuses + label_crm_deal_status: Deal status + field_deal_status_is_closed: Closed + label_crm_deal_status_new: New + permission_manage_contacts: Enumerations + label_crm_sales_funnel: Sales funnel + label_crm_period: Period + label_crm_count: Count + + #2.0.1 + label_crm_user_format: Contact name format + label_crm_my_contact_plural: Contacts assigned to me + label_crm_my_deal_plural: Open deals assigned to me + label_crm_contact_view_all: View all contacts + label_crm_deal_view_all: View all deals + + #2.0.2 + label_crm_bulk_edit_selected_contacts: Edit all selected contacts + label_crm_bulk_edit_selected_deals: Edit all selected deals + label_crm_bulk_send_mail_selected_contacts: Send mail to selected contacts + field_add_tags: Add tags + field_delete_tags: Delete tags + label_crm_send_mail: Send mail + error_empty_email: Email can not be blank + permission_send_contacts_mail: Send mail + field_mail_from: From address + text_email_macros: Avaliable macros %{macro} + field_message: Message + + #2.0.3 + label_crm_add_contact: Add contact + label_contact: Contact + field_age: Age + label_crm_vcf_import: Import from vCard + label_crm_mail_from: From + permission_import_contacts: Import contacts + + #2.1.0 + field_company_name: Tên Công ty + label_crm_recently_added_contacts: Recently added contacts + label_crm_created_by_me: Contacts created by me + my_contacts: My contacts + my_deals: My deals + + #2.2.0 + label_crm_note_type_email: Email + label_crm_note_type_call: Call + label_crm_note_type_meeting: Meeting + field_deal_currency: Currency + label_crm_my_contacts_stats: Contacts statistics for this month + label_crm_contacts_created: Contacts created + label_crm_deals_created: Deals created + my_contacts_avatars: My contacts photos + my_contacts_stats: Contacts statistics + label_crm_add_into: Add into + label_crm_delete_from: Delete from + label_crm_show_deaks_tab: Show deals tab + label_crm_show_on_projects_show: Show contacts on projects overview + + #2.2.1 + label_crm_contacts_show_in_list: Show in list + + #2.3.0 + label_crm_module_plural: Modules + label_crm_list_partial_style: Contacts list style + label_crm_list_excerpt: Excerpt list + label_crm_list_cards: Cards + label_crm_list_list: Table + field_contacts: Contact + field_companies: Company + label_crm_added_by: Added by + label_crm_contact_note_authoring_time: Show note time + label_crm_contact_issues_filters: Issues filters + label_crm_csv_import: Import contacts from CSV + label_crm_upload_encoding: File encoding + label_crm_csv_file: CSV file + label_crm_csv_separator: Separator + field_middle_name: Middle Name + field_job_title: Job title + field_company: Company + field_address: Address + field_phone: Phone + field_email: Email + field_tags: Tags + field_last_note: Last note + field_is_company: Is company + field_contact_full_name: Full name + button_contacts_edit_query: Edit query + button_contacts_delete_query: Delete query + permission_manage_public_contacts_queries: Manage public queries + permission_add_deals: Add deals + permission_add_contacts: Add contacts + permission_save_contacts_queries: Save queries + + #2.3.3 + label_crm_contact_show_in_app_menu: Show tabs in app menu + + #2.3.4 + label_crm_contact_show_closed_issues: Show closed issues \ No newline at end of file diff --git a/plugins/redmine_contacts/config/locales/zh-TW.yml b/plugins/redmine_contacts/config/locales/zh-TW.yml new file mode 100644 index 0000000..9e3c13e --- /dev/null +++ b/plugins/redmine_contacts/config/locales/zh-TW.yml @@ -0,0 +1,609 @@ +zh: + contacts_title: è¯ç³»äºº + + label_crm_recently_viewed: 最近ç€è¦½ + label_crm_gravatar_enabled: ä½¿ç”¨é ­åƒ + label_crm_thumbnails_enabled: 在備注顯示縮略圖 + label_crm_max_thumbnail_file_size: 最大縮略圖尺寸 + label_crm_view_all_contacts: 所有è¯ç³»äºº + label_crm_background_info: èƒŒæ™¯ä¿¡æ¯ + label_crm_company: å…¬å¸ + label_contact_plural: è¯ç³»äººåˆ—表 + label_crm_contact_edit_information: 編輯è¯ç³»äººä¿¡æ¯ + label_crm_edit_tags: 編輯標簽 + label_crm_contact_view: 視圖 + label_crm_contact_list: 列表 + label_crm_contact_new: 新建 + label_crm_at_company: 在 + label_crm_last_notes: 最新的備注 + label_crm_tags_plural: 標簽 + label_crm_multi_tags_plural: é¸ä¸­å¤šå€‹æ¨™ç°½ + label_crm_single_tag_mode: 單標簽 + label_crm_multiple_tags_mode: 多標簽 + label_crm_contact_tag: 標簽 + label_crm_time_ago: ä¹‹å‰ + label_crm_add_note_plural: 增加備注 + label_crm_note_plural: 備注 + + label_crm_add_tags_rule: 用逗號分割 + label_crm_contact_search: 按åå­—æœç´¢ + label_crm_note_for: 備注 + label_crm_show_on_map: 在地圖上顯示 + label_crm_add_another_phone: 添加電話號碼 + label_crm_remove: 刪除 + label_crm_related_contacts: 相關è¯ç³»äºº + label_crm_assigned_to: 接å£äºº + label_crm_issue_added: 創建的å•題 + label_crm_add_emails_rule: 用逗號分隔 + label_crm_add_phones_rule: 用逗號分隔 + label_crm_add_employee: 新建雇員 + label_crm_merge_duplicate_plural: åˆå¹¶ + label_crm_duplicate_plural: å¯èƒ½é‡å¾©çš„è¯ç³»äºº + label_crm_duplicate_for_plural: å¯èƒ½èˆ‡ä¸‹åˆ—è¯ç³»äººé‡å¾© + label_crm_add_tag: + 新建標簽 + + label_crm_note_show_extras: 高級 (種類, 日期, 文件) + label_crm_note_hide_extras: éš±è—高級é¸é … + label_crm_note_added: æˆåŠŸæ·»åŠ äº†å‚™æ³¨ + label_crm_note_read_more: (閱讀更多) + label_crm_invoice_import: 從CSV中導入發票 + + label_deal_plural: åˆåŒ + label_crm_contractor_plural: 接觸人 + label_deal: åˆåŒ + label_crm_deal_new: æ–°åˆåŒ + label_crm_deal_edit_information: 編輯åˆåŒä¿¡æ¯ + label_crm_deal_change_status: 更改狀態 + label_crm_statistics: 統計 + label_crm_deals_import: 從CSV中導入åˆåŒ + + label_crm_deal_status_new: 新建 + label_crm_deal_status_first_contact: 第一è¯ç³»äºº + label_crm_deal_status_negotiations: 交涉中 + label_crm_deal_status_pending: 待定 + label_crm_deal_status_won: 锿ˆ + label_crm_deal_status_lost: æœªé”æˆ + + label_crm_created_on: 創建于 + + field_note_date: 注釋日期 + field_background: 背景 + field_currency: 貨幣 + field_contact: è¯ç³»äºº + + field_deal_name: å§“å + field_deal_background: 背景 + field_deal_contact: è¯ç³»äºº + field_deal_price: 總計 + field_price: 總計 + + field_contact_avatar: é ­åƒ + field_contact_is_company: å…¬å¸ + field_contact_name: å§“å + field_contact_last_name: å§“ + field_contact_first_name: å + field_contact_middle_name: 中間å + field_contact_job_title: è·ä½å稱 + field_contact_company: å…¬å¸ + field_contact_address: åœ°å€ + field_contact_phone: 電話 + field_contact_email: 郵件 + field_contact_website: 網站 + field_contact_skype: skype + field_contact_status: 狀態 + field_contact_background: 背景 + field_contact_tag_names: 標簽 + field_first_name: å + field_last_name: å§“ + field_company: å…¬å¸ + field_birthday: 生日 + field_contact_department: 部門 + + + field_company_field: 事業 + + field_color: é¡è‰² + + button_add_note: 新建注釋 + notice_successful_save: ä¿å­˜æˆåŠŸ + notice_successful_add: 創建æˆåŠŸ + notice_unsuccessful_save: ä¿å­˜å¤±æ•— + notice_successful_merged: åˆå¹¶æˆåŠŸ + + notice_merged_warning: 所有與該è¯ç³»äººé—œè¯çš„æ³¨é‡‹ã€é …ç›®ã€æ¨™ç°½å’Œä»»å‹™å°‡æœƒç§»å‹•åˆ°ä»¥ä¸‹é¸æ“‡ä½ç½®.該è¯ç³»äººä¹‹åŽå°‡è¢«åˆªé™¤. + + project_module_contacts: è¯ç³»äºº + + permission_view_contacts: ç€è¦½è¯ç³»äºº + permission_edit_contacts: 編輯è¯ç³»äºº + permission_delete_contacts: 刪除è¯ç³»äºº + permission_view_deals: ç€è¦½åˆåŒ + permission_edit_deals: 編輯åˆåŒ + permission_delete_deals: 刪除åˆåŒ + permission_add_notes: 增加注釋 + permission_delete_notes: 刪除注釋 + permission_delete_own_notes: 刪除自己的注釋 + + # 2.0.0 + label_crm_deal_category: åˆåŒåˆ†é¡ž + label_crm_deal_category_plural: åˆåŒåˆ†é¡ž + label_crm_deal_category_new: 新建類別 + text_deal_category_destroy_assignments: 刪除分類任務 + text_deal_category_destroy_question: "一些åˆåŒ (%{count}) 被分é…至該類別下.您想è¦åšä»€ä¹ˆ?" + text_deal_category_reassign_to: 釿–°åˆ†é…åˆåŒè‡³è©²é¡žåˆ¥ + text_deals_destroy_confirmation: '您確定想è¦åˆªé™¤é¸æ“‡çš„åˆåŒå—Ž(s)?' + label_crm_deal_status_plural: åˆåŒç‹€æ…‹ + label_crm_deal_status: åˆåŒç‹€æ…‹ + field_deal_status_is_closed: 關閉 + label_crm_deal_status_new: 新建 + permission_manage_contacts: 管ç†è¯ç³»äºº + label_crm_sales_funnel: éŠ·å”®æ¼æ–— + label_crm_period: æœŸé™ + label_crm_count: æ•¸é‡ + + #2.0.1 + label_crm_user_format: è¯ç³»äººå§“åæ ¼å¼ + label_crm_my_contact_plural: 分é…è¯ç³»äººè‡³æˆ‘ + label_crm_my_deal_plural: 開啟åˆåŒè‡³æˆ‘ + label_crm_contact_view_all: ç€è¦½æ‰€æœ‰è¯ç³»äºº + label_crm_deal_view_all: ç€è¦½æ‰€æœ‰åˆåŒ + + #2.0.2 + label_crm_bulk_edit_selected_contacts: ç·¨è¼¯æ‰€æœ‰é¸æ“‡çš„è¯ç³»äºº + label_crm_bulk_edit_selected_deals: ç·¨è¼¯ç´ æœ‰é¸æ“‡çš„åˆåŒ + label_crm_bulk_send_mail_selected_contacts: 發é€éƒµä»¶çµ¦æ‰€é¸è¯ç³»äºº + field_add_tags: 新建標簽 + field_delete_tags: 刪除標簽 + label_crm_send_mail: 發é€éƒµä»¶ + error_empty_email: 郵件內容ä¸å¯ç‚ºç©º + permission_send_contacts_mail: 發é€éƒµä»¶ + field_mail_from: å¾žåœ°å€ + text_email_macros: å¯ç”¨å® %{macro} + field_message: æ¶ˆæ¯ + + #2.0.3 + label_crm_add_contact: 新建è¯ç³»äºº + label_contact: è¯ç³»äºº + field_age: 年齡 + label_crm_vcf_import: 從vCard中導入 + label_crm_mail_from: 從 + permission_import_contacts: å°Žå…¥è¯ç³»äºº + + #2.1.0 + field_company_name: å…¬å¸å + label_crm_recently_added_contacts: 最近創建è¯ç³»äºº + label_crm_created_by_me: 由我創建的è¯ç³»äºº + my_contacts: 我的è¯ç³»äºº + my_deals: 我的åˆåŒ + + #2.2.0 + label_crm_note_type_email: 郵件 + label_crm_note_type_call: 電話 + label_crm_note_type_meeting: 會議 + field_deal_currency: 貨幣 + label_crm_my_contacts_stats: è¯ç³»äººç•¶æœˆçµ±è¨ˆ + label_crm_contacts_created: 創建的è¯ç³»äºº + label_crm_deals_created: 創建的åˆåŒ + my_contacts_avatars: 我è¯ç³»äººçš„照片 + my_contacts_stats: è¯ç³»äººçµ±è¨ˆ + label_crm_add_into: 創建至 + label_crm_delete_from: 刪除 + label_crm_show_deaks_tab: 顯示åˆåŒæ¨™ç°½ + label_crm_show_on_projects_show: 顯示項目概述中的è¯ç³»äºº + + #2.2.1 + label_crm_contacts_show_in_list: 在列表中顯示 + + #2.3.0 + label_crm_module_plural: 模塊 + label_crm_list_partial_style: 列表風格 + label_crm_list_excerpt: 摘錄列表 + label_crm_list_cards: å¡ç‰‡ + label_crm_list_list: 表格 + field_contacts: è¯ç³»äºº + field_companies: å…¬å¸ + label_crm_added_by: 添加者 + label_crm_contact_note_authoring_time: 顯示注釋時間 + label_crm_contact_issues_filters: å•é¡ŒéŽæ¿¾ + label_crm_csv_import: 從CSVå°Žå…¥ + label_crm_upload_encoding: 文件編碼 + label_crm_csv_file: CSV文件 + label_crm_csv_separator: 分隔符 + field_middle_name: 中間å + field_job_title: è·ä½ + field_company: å…¬å¸ + field_address: åœ°å€ + field_phone: 電話 + field_email: 郵件 + field_tags: 標簽 + field_last_note: 最新注釋 + field_is_company: å…¬å¸ + field_contact_full_name: å§“å + button_contacts_edit_query: 編輯查詢 + button_contacts_delete_query: 刪除查詢 + permission_manage_public_contacts_queries: 管ç†å…¬å…±æŸ¥è©¢ + permission_add_deals: 新建åˆåŒ + permission_add_contacts: 新建è¯ç³»äºº + permission_save_contacts_queries: ä¿å­˜æŸ¥è©¢ + + #2.3.3 + label_crm_contact_show_in_app_menu: 在應用èœå–®ä¸­é¡¯ç¤ºè¯ç³»äºº + + #2.3.4 + label_crm_contact_show_closed_issues: 顯示關閉的å•題 + + #3.0.0 + label_crm_import: å°Žå…¥ + label_contact_note_plural: è¯ç³»äººæ³¨é‡‹ + label_deal_note_plural: åˆåŒæ³¨é‡‹ + label_crm_contact_all_note_plural: 所有注釋 + error_unable_delete_deal_status: ä¸èƒ½åˆªé™¤åˆåŒç‹€æ…‹ + label_crm_contacts_hidden: éš±è—的設置 + + #3.1.0 + label_crm_contact_added: 新增的è¯ç³»äºº + label_crm_note_added: 新增的注釋 + label_crm_deal_added: 新增的åˆåŒ + label_crm_deal_updated: æ›´æ–°çš„åˆåŒ + text_crm_contact_added: "è¯ç³»äºº %{name} 已經由 %{author}創建." + text_crm_deal_added: "åˆåŒ %{name} 已經由 %{author}創建." + text_crm_deal_status_changed: "åˆåŒç‹€æ…‹ç”± %{old} 更改至 %{new}" + text_crm_deal_updated: "åˆåŒ %{name} 已經由 %{author}æ›´æ–°." + label_crm_contacts_cc: æŠ„é€ + label_crm_contacts_bcc: å¯†é€ + label_crm_contact_import: 從CSVå°Žå…¥ + permission_import_deals: å°Žå…¥åˆåŒ + label_crm_single_quotes: "單引號 (')" + label_crm_double_quotes: "雙引號 (\")" + label_crm_quotes_type: 引號類型 + label_crm_contacts_visibility: å¯è¦‹ + label_crm_contacts_visibility_project: é …ç›®è¨±å¯ + label_crm_contacts_visibility_public: 公開 + label_crm_contacts_visibility_private: éš±è— + permission_view_private_contacts: ç€è¦½éš±è—è¯ç³»äºº + text_crm_error_on_line: '錯誤在行 %{line}: %{error}.' + + #3.2.0 + label_crm_probability: å¯èƒ½æ€§ + label_crm_deal_status_type: 狀態類別 + label_crm_select_companies: 在åˆåŒä¸­éŽæ¿¾å…¬å¸ + label_crm_expected_revenue: 期望收益 + label_crm_deal_due_date: 到期日 + + #3.2.2 + label_crm_show_deals_in_top_menu: 在頂部èœå–®ä¸­é¡¯ç¤ºåˆåŒ + label_crm_show_details: 顯示細節 + label_crm_has_deals: åˆåŒ + label_crm_has_open_issues: 未決å•題 + label_crm_note: 注釋 + + #3.2.5 + notice_failed_to_save_contacts: "ä¸èƒ½ä¿å­˜ %{count} è¯ç³»äºº(s) %{total} å·²é¸: %{ids}." + + #3.2.6 + project_module_deals: åˆåŒ + permission_manage_deals: 管ç†åˆåŒ + label_crm_deals_from_subprojects: 在å­é …目中顯示åˆåŒ + label_crm_megre_tags: åˆå¹¶æ¨™ç°½ + label_crm_monochrome_tags: 單色標簽 + + #3.2.7 + label_crm_address: åœ°å€ + label_crm_street1: è¡—é“ 1 + label_crm_street2: è¡—é“ 2 + label_crm_city: 城市 + label_crm_region: 'çœ' + label_crm_postcode: '郵編' + label_crm_country: 國家 + label_crm_countries: + AF: Afghanistan + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: 澳大利亞 + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: 巴西 + BQ: British Antarctic Territory + IO: British Indian Ocean Territory + VG: British Virgin Islands + BN: Brunei + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: 加拿大 + CT: Canton and Enderbury Islands + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: 中國 + CX: Christmas Island + CC: Cocos [Keeling] Islands + CO: Colombia + KM: Comoros + CG: Congo - Brazzaville + CD: Congo - Kinshasa + CK: Cook Islands + CR: Costa Rica + HR: Croatia + CU: å¤å·´ + CY: Cyprus + CZ: Czech Republic + CI: C?te d’Ivoire + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + NQ: Dronning Maud Land + DD: East Germany + EC: Ecuador + EG: åŸƒåŠ + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: 法國 + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + FQ: French Southern and Antarctic Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: 希臘 + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and McDonald Islands + HN: Honduras + HK: ä¸­åœ‹é¦™æ¸¯ç‰¹åˆ¥è¡Œæ”¿å€ + HU: Hungary + IS: 冰島 + IN: å°åº¦ + ID: Indonesia + IR: Iran + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: æ„大利 + JM: Jamaica + JP: 日本 + JE: Jersey + JT: Johnston Island + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KW: Kuwait + KG: Kyrgyzstan + LA: Laos + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macau SAR China + MK: Macedonia + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + FX: Metropolitan France + MX: 墨西哥 + FM: Micronesia + MI: Midway Islands + MD: Moldova + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar [Burma] + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + AN: Netherlands Antilles + NT: Neutral Zone + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + KP: North Korea + VD: North Vietnam + MP: Northern Mariana Islands + "NO": Norway + OM: Oman + PC: Pacific Islands Trust Territory + PK: Pakistan + PW: Palau + PS: Palestinian Territories + PA: Panama + PZ: Panama Canal Zone + PG: Papua New Guinea + PY: Paraguay + YD: People's Democratic Republic of Yemen + PE: Peru + PH: Philippines + PN: Pitcairn Islands + PL: 波蘭 + PT: Portugal + PR: Puerto Rico + QA: Qatar + RO: Romania + RU: ä¿„ç¾…æ–¯ + RW: Rwanda + RE: Réunion + BL: Saint Barthélemy + SH: Saint Helena + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + SA: Saudi Arabia + SN: Senegal + RS: Serbia + CS: Serbia and Montenegro + SC: Seychelles + SL: Sierra Leone + SG: æ–°åŠ å¡ + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: å—éž + GS: South Georgia and the South Sandwich Islands + KR: å—韓 + ES: 西ç­ç‰™ + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: 瑞典 + CH: Switzerland + SY: Syria + ST: S?o Tomé and Príncipe + TW: è‡ºç£ + TJ: Tajikistan + TZ: Tanzania + TH: 泰國 + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UM: U.S. Minor Outlying Islands + PU: U.S. Miscellaneous Pacific Islands + VI: U.S. Virgin Islands + UG: Uganda + UA: Ukraine + AE: United Arab Emirates + GB: 英國 + US: 美國 + ZZ: Unknown or Invalid Region + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VA: Vatican City + VE: Venezuela + VN: è¶Šå— + WK: Wake Island + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + AX: ?land Islands + label_crm_cross_project_contacts: å…許跨項目è¯ç³»äººé—œç³» + label_crm_list_board: Board + label_crm_default_list_style: 默èªè¯ç³»é¢¨æ ¼ + label_crm_show_in_top_menu: 在頂部èœå–®ä¸­é¡¯ç¤º + label_crm_show_in_app_menu: 在應用èœå–®ä¸­é¡¯ç¤º + label_crm_money_settings: 錢 + label_crm_disable_taxes: ç¦ç”¨ç¨… + label_crm_default_tax: 默èªç¨…值 + label_crm_tax_type: 稅收種類 + label_crm_tax_type_inclusive: å«ç¨… + label_crm_tax_type_exclusive: 稅率 + label_crm_default_currency: 默èªè²¨å¹£ + label_crm_thousands_delimiter: åƒä½åˆ†éš”符 + label_crm_decimal_separator: å進制分隔符 + + #3.2.10 + label_crm_add_contact_plural: 新建è¯ç³»äºº + label_crm_search_for_contact: æœç´¢è¯ç³»äºº + label_crm_major_currencies: 主è¦è²¨å¹£ + + #3.2.11 + label_crm_post_address_format: éƒµæ”¿åœ°å€æ ¼å¼ + label_crm_post_address_format_macros: "åœ°å€æ ¼å¼å®: %{macros}" + + #3.2.14 + label_crm_last_year: 去年 + + #3.2.15 + permission_export_contacts: 導出è¯ç³»äººå’ŒåˆåŒ + + #3.4.0 + label_crm_deal_contact: åˆåŒçš„è¯ç³»äºº + + #3.4.1 + label_crm_default_country: 默èªåœ‹å®¶ + label_attribute_of_contact: "è¯ç³»äººçš„%{name}" + label_crm_contact_country: è¯ç³»äººçš„國家 + label_crm_contact_city: è¯ç³»äººçš„城市 + label_attribute_of_deals: "åˆåŒçš„ %{name}" + + #3.4.2 + permission_manage_public_deals_queries: 管ç†åˆåŒå…¬å…±æŸ¥è©¢ + permission_save_deals_queries: ä¿å­˜åˆåŒæŸ¥è©¢ + permission_manage_contact_issue_relations: 管ç†å•題關系 + + #3.4.4 + label_crm_pipeline: æµæ°´ç·š + text_crm_no_deal_statuses_in_project: 項目中沒有åˆåŒç‹€æ…‹ diff --git a/plugins/redmine_contacts/config/locales/zh.yml b/plugins/redmine_contacts/config/locales/zh.yml new file mode 100644 index 0000000..0c58af8 --- /dev/null +++ b/plugins/redmine_contacts/config/locales/zh.yml @@ -0,0 +1,622 @@ +zh: + contacts_title: è”系人 + + label_crm_recently_viewed: 最近æµè§ˆ + label_crm_gravatar_enabled: ä½¿ç”¨å¤´åƒ + label_crm_thumbnails_enabled: 在备注显示缩略图 + label_crm_max_thumbnail_file_size: 最大缩略图尺寸 + label_crm_view_all_contacts: 所有è”系人 + label_crm_background_info: èƒŒæ™¯ä¿¡æ¯ + label_crm_company: å…¬å¸ + label_contact_plural: è”系人列表 + label_crm_contact_edit_information: 编辑è”ç³»äººä¿¡æ¯ + label_crm_edit_tags: 编辑标签 + label_crm_contact_view: 视图 + label_crm_contact_list: 列表 + label_crm_contact_new: 新建 + label_crm_at_company: 在 + label_crm_last_notes: 最新的备注 + label_crm_tags_plural: 标签 + label_crm_multi_tags_plural: 选中多个标签 + label_crm_single_tag_mode: 啿 ‡ç­¾ + label_crm_multiple_tags_mode: 多标签 + label_crm_contact_tag: 标签 + label_crm_time_ago: ä¹‹å‰ + label_crm_add_note_plural: 增加备注 + label_crm_note_plural: 备注 + + label_crm_add_tags_rule: 用逗å·åˆ†å‰² + label_crm_contact_search: 按åå­—æœç´¢ + label_crm_note_for: 备注 + label_crm_show_on_map: 在地图上显示 + label_crm_add_another_phone: 添加电è¯å·ç  + label_crm_remove: 删除 + label_crm_related_contacts: 相关è”系人 + label_crm_assigned_to: 接å£äºº + label_crm_issue_added: 创建的问题 + label_crm_add_emails_rule: 用逗å·åˆ†éš” + label_crm_add_phones_rule: 用逗å·åˆ†éš” + label_crm_add_employee: 新建雇员 + label_crm_merge_duplicate_plural: åˆå¹¶ + label_crm_duplicate_plural: å¯èƒ½é‡å¤çš„è”系人 + label_crm_duplicate_for_plural: å¯èƒ½ä¸Žä¸‹åˆ—è”系人é‡å¤ + label_crm_add_tag: + 新建标签 + + label_crm_note_show_extras: 高级 (ç§ç±», 日期, 文件) + label_crm_note_hide_extras: éšè—高级选项 + label_crm_note_added: æˆåŠŸæ·»åŠ äº†å¤‡æ³¨ + label_crm_note_read_more: (阅读更多) + label_crm_invoice_import: 从CSV中导入å‘票 + + label_deal_plural: åˆä½œ + label_crm_contractor_plural: 接触人 + label_deal: åˆä½œ + label_crm_deal_new: æ–°åˆä½œ + label_crm_deal_edit_information: 编辑åˆä½œä¿¡æ¯ + label_crm_deal_change_status: æ›´æ”¹çŠ¶æ€ + label_crm_statistics: 统计 + label_crm_deals_import: 从CSV中导入åˆä½œ + + label_crm_deal_status_new: 新建 + label_crm_deal_status_first_contact: 第一è”系人 + label_crm_deal_status_negotiations: 交涉中 + label_crm_deal_status_pending: 待定 + label_crm_deal_status_won: è¾¾æˆ + label_crm_deal_status_lost: æœªè¾¾æˆ + + label_crm_created_on: 创建于 + + field_note_date: 注释日期 + field_background: 背景 + field_currency: è´§å¸ + field_contact: è”系人 + + field_deal_name: åç§° + field_deal_background: 背景 + field_deal_contact: è”系人 + field_deal_price: 总计 + field_price: 总计 + + field_contact_avatar: å¤´åƒ + field_contact_is_company: å…¬å¸ + field_contact_name: åç§° + field_contact_last_name: å§“ + field_contact_first_name: å + field_contact_middle_name: 中间å + field_contact_job_title: èŒä½ + field_contact_company: å…¬å¸ + field_contact_address: åœ°å€ + field_contact_phone: ç”µè¯ + field_contact_email: 邮件 + field_contact_website: 网站 + field_contact_skype: 微信 + field_contact_status: çŠ¶æ€ + field_contact_background: 背景 + field_contact_tag_names: 标签 + field_first_name: åå­— + field_last_name: å§“æ° + field_company: å…¬å¸ + field_birthday: 生日 + field_contact_department: 部门 + + + field_company_field: 事业 + + field_color: 颜色 + + button_add_note: 新建注释 + notice_successful_save: ä¿å­˜æˆåŠŸ + notice_successful_add: 创建æˆåŠŸ + notice_unsuccessful_save: ä¿å­˜å¤±è´¥ + notice_successful_merged: åˆå¹¶æˆåŠŸ + + notice_merged_warning: 所有与该è”系人关è”的注释ã€é¡¹ç›®ã€æ ‡ç­¾å’Œä»»åŠ¡å°†ä¼šç§»åŠ¨åˆ°ä»¥ä¸‹é€‰æ‹©ä½ç½®.该è”系人之åŽå°†è¢«åˆ é™¤. + + project_module_contacts: è”系人 + + permission_view_contacts: æµè§ˆè”系人 + permission_edit_contacts: 编辑è”系人 + permission_delete_contacts: 删除è”系人 + permission_view_deals: æµè§ˆåˆä½œ + permission_edit_deals: 编辑åˆä½œ + permission_delete_deals: 删除åˆä½œ + permission_add_notes: 增加注释 + permission_delete_notes: 删除注释 + permission_delete_own_notes: 删除自己的注释 + + # 2.0.0 + label_crm_deal_category: åˆä½œåˆ†ç±» + label_crm_deal_category_plural: åˆä½œåˆ†ç±» + label_crm_deal_category_new: 新建类别 + text_deal_category_destroy_assignments: 删除分类任务 + text_deal_category_destroy_question: "一些åˆä½œ (%{count}) 被分é…至该类别下. 您想è¦åšä»€ä¹ˆ?" + text_deal_category_reassign_to: 釿–°åˆ†é…åˆä½œè‡³è¯¥ç±»åˆ« + text_deals_destroy_confirmation: '您确定想è¦åˆ é™¤é€‰æ‹©çš„åˆä½œå—(s)?' + label_crm_deal_status_plural: åˆä½œçŠ¶æ€ + label_crm_deal_status: åˆä½œçŠ¶æ€ + field_deal_status_is_closed: 关闭 + label_crm_deal_status_new: 新建 + permission_manage_contacts: 管ç†è”系人 + label_crm_sales_funnel: é”€å”®æ¼æ–— + label_crm_period: æœŸé™ + label_crm_count: æ•°é‡ + + #2.0.1 + label_crm_user_format: è”系人åç§°æ ¼å¼ + label_crm_my_contact_plural: 分é…è”系人至我 + label_crm_my_deal_plural: å¼€å¯åˆä½œè‡³æˆ‘ + label_crm_contact_view_all: æµè§ˆæ‰€æœ‰è”系人 + label_crm_deal_view_all: æµè§ˆæ‰€æœ‰åˆä½œ + + #2.0.2 + label_crm_bulk_edit_selected_contacts: 编辑所有选择的è”系人 + label_crm_bulk_edit_selected_deals: 编辑素有选择的åˆä½œ + label_crm_bulk_send_mail_selected_contacts: å‘é€é‚®ä»¶ç»™æ‰€é€‰è”系人 + field_add_tags: 新建标签 + field_delete_tags: 删除标签 + label_crm_send_mail: å‘é€é‚®ä»¶ + error_empty_email: 邮件内容ä¸å¯ä¸ºç©º + permission_send_contacts_mail: å‘é€é‚®ä»¶ + field_mail_from: 邮件æ¥è‡ª + text_email_macros: å¯ç”¨å® %{macro} + field_message: æ¶ˆæ¯ + + #2.0.3 + label_crm_add_contact: 新建è”系人 + label_contact: è”系人 + field_age: 年龄 + label_crm_vcf_import: 从vCard中导入 + label_crm_mail_from: 从 + permission_import_contacts: 导入è”系人 + + #2.1.0 + field_company_name: å…¬å¸å + label_crm_recently_added_contacts: 最近创建è”系人 + label_crm_created_by_me: 由我创建的è”系人 + my_contacts: 我的è”系人 + my_deals: 我的åˆä½œ + + #2.2.0 + label_crm_note_type_email: 邮件 + label_crm_note_type_call: ç”µè¯ + label_crm_note_type_meeting: 会议 + field_deal_currency: è´§å¸ + label_crm_my_contacts_stats: è”系人当月统计 + label_crm_contacts_created: 创建的è”系人 + label_crm_deals_created: 创建的åˆä½œ + my_contacts_avatars: 我è”系人的照片 + my_contacts_stats: è”系人统计 + label_crm_add_into: 创建至 + label_crm_delete_from: 删除 + label_crm_show_deaks_tab: 显示åˆä½œæ ‡ç­¾ + label_crm_show_on_projects_show: 显示项目概述中的è”系人 + + #2.2.1 + label_crm_contacts_show_in_list: 在列表中显示 + + #2.3.0 + label_crm_module_plural: æ¨¡å— + label_crm_list_partial_style: 列表风格 + label_crm_list_excerpt: 摘录列表 + label_crm_list_cards: å¡ç‰‡ + label_crm_list_list: 表格 + field_contacts: è”系人 + field_companies: å…¬å¸ + label_crm_added_by: 添加者 + label_crm_contact_note_authoring_time: 显示注释时间 + label_crm_contact_issues_filters: 问题过滤 + label_crm_csv_import: 从CSV导入 + label_crm_upload_encoding: æ–‡ä»¶ç¼–ç  + label_crm_csv_file: CSV文件 + label_crm_csv_separator: 分隔符 + field_middle_name: 中间å + field_job_title: èŒä½ + field_company: å…¬å¸ + field_address: åœ°å€ + field_phone: ç”µè¯ + field_email: 邮件 + field_tags: 标签 + field_last_note: 最新注释 + field_is_company: å…¬å¸ + field_contact_full_name: åç§° + button_contacts_edit_query: 编辑查询 + button_contacts_delete_query: 删除查询 + permission_manage_public_contacts_queries: 管ç†å…¬å…±æŸ¥è¯¢ + permission_add_deals: 新建åˆä½œ + permission_add_contacts: 新建è”系人 + permission_save_contacts_queries: ä¿å­˜æŸ¥è¯¢ + + #2.3.3 + label_crm_contact_show_in_app_menu: 在应用èœå•中显示è”系人 + + #2.3.4 + label_crm_contact_show_closed_issues: 显示关闭的问题 + + #3.0.0 + label_crm_import: 导入 + label_contact_note_plural: è”系人注释 + label_deal_note_plural: åˆä½œæ³¨é‡Š + label_crm_contact_all_note_plural: 所有注释 + error_unable_delete_deal_status: ä¸èƒ½åˆ é™¤åˆä½œçŠ¶æ€ + label_crm_contacts_hidden: éšè—的设置 + + #3.1.0 + label_crm_contact_added: 新增的è”系人 + label_crm_note_added: 新增的注释 + label_crm_deal_added: 新增的åˆä½œ + label_crm_deal_updated: æ›´æ–°çš„åˆä½œ + text_crm_contact_added: "è”系人 %{name} å·²ç»ç”± %{author}创建." + text_crm_deal_added: "åˆä½œ %{name} å·²ç»ç”± %{author}创建." + text_crm_deal_status_changed: "åˆä½œçжæ€ç”± %{old} 更改至 %{new}" + text_crm_deal_updated: "åˆä½œ %{name} å·²ç»ç”± %{author}æ›´æ–°." + label_crm_contacts_cc: æŠ„é€ + label_crm_contacts_bcc: å¯†é€ + label_crm_contact_import: 从CSV导入 + permission_import_deals: 导入åˆä½œ + label_crm_single_quotes: "å•å¼•å· (')" + label_crm_double_quotes: "åŒå¼•å· (\")" + label_crm_quotes_type: 引å·ç±»åž‹ + label_crm_contacts_visibility: å¯è§ + label_crm_contacts_visibility_project: é¡¹ç›®è®¸å¯ + label_crm_contacts_visibility_public: 公开 + label_crm_contacts_visibility_private: éšè— + permission_view_private_contacts: æµè§ˆéšè—è”系人 + text_crm_error_on_line: '错误在行 %{line}: %{error}.' + + #3.2.0 + label_crm_probability: åˆä½œå¯èƒ½æ€§ + label_crm_deal_status_type: 状æ€ç±»åˆ« + label_crm_select_companies: 在åˆä½œä¸­è¿‡æ»¤å…¬å¸ + label_crm_expected_revenue: 期望收益 + label_crm_deal_due_date: 到期日 + + #3.2.2 + label_crm_show_deals_in_top_menu: 在顶部èœå•中显示åˆä½œ + label_crm_show_details: 显示细节 + label_crm_has_deals: åˆä½œ + label_crm_has_open_issues: 打开问题 + label_crm_note: 注释 + + #3.2.5 + notice_failed_to_save_contacts: "ä¸èƒ½ä¿å­˜ %{count} è”系人(s) %{total} 已选: %{ids}." + + #3.2.6 + project_module_deals: åˆä½œ + permission_manage_deals: 管ç†åˆä½œ + label_crm_deals_from_subprojects: 在å­é¡¹ç›®ä¸­æ˜¾ç¤ºåˆä½œ + label_crm_megre_tags: åˆå¹¶æ ‡ç­¾ + label_crm_monochrome_tags: å•色标签 + + #3.2.7 + label_crm_address: åœ°å€ + label_crm_street1: è¡—é“ 1 + label_crm_street2: è¡—é“ 2 + label_crm_city: 市 + label_crm_region: 'çœ' + label_crm_postcode: '邮编' + label_crm_country: 国家 + label_crm_country_code: å›½å®¶ä»£ç  + label_crm_countries: + AF: 阿富汗 + AL: 阿尔巴尼亚 + DZ: 阿尔åŠåˆ©äºš + AS: ç¾Žå±žè¨æ‘©äºš + AD: 安é“å°” + AO: 安哥拉 + AI: 安圭拉 + AQ: å—æžæ´² + AG: 安æç“œå’Œå·´å¸ƒè¾¾ + AR: 阿根廷 + AM: 亚美尼亚 + AW: 阿é²å·´ + AU: 澳大利亚 + AT: 奥地利 + AZ: 阿塞拜疆 + BS: 巴哈马 + BH: å·´æž— + BD: 孟加拉国 + BB: 巴巴多斯 + BY: 白俄罗斯 + BE: 比利时 + BZ: 伯利兹 + BJ: è´å® + BM: 百慕大 + BT: ä¸ä¸¹ + BO: 玻利维亚 + BA: 波斯尼亚和黑塞哥维那 + BW: åšèŒ¨ç“¦çº³ + BV: 布韦岛 + BR: 巴西 + BQ: è‹±å±žå—æžé¢†åœ° + IO: 英属å°åº¦æ´‹é¢†åœ° + VG: 英属维尔京群岛 + BN: 文莱 + BG: ä¿åŠ åˆ©äºš + BF: 布基纳法索 + BI: 布隆迪 + KH: 柬埔寨 + CM: 喀麦隆 + CA: 加拿大 + CT: åŽé¡¿å’Œæ©å¾·ä¼¯é‡Œç¾¤å²› + CV: 佛得角 + KY: 开曼群岛 + CF: 中éžå…±å’Œå›½ + TD: ä¹å¾— + CL: 智利 + CN: 中国 + CX: 圣诞岛 + CC: 科科斯群岛 + CO: 哥伦比亚 + KM: ç§‘æ‘©ç½— + CG: 刚果 - 布拉柴维尔 + CD: 刚果 - é‡‘æ²™è¨ + CK: 库克群岛 + CR: 哥斯达黎加 + HR: 克罗地亚 + CU: å¤å·´ + CY: 塞浦路斯 + CZ: æ·å…‹å…±å’Œå›½ + CI: 科特迪瓦 + DK: 丹麦 + DJ: å‰å¸ƒæ + DM: 多米尼加 + DO: 多米尼加共和国 + NQ: 德龙莫德地区 + DD: 东德 + EC: 厄瓜多尔 + EG: åŸƒåŠ + SV: è¨å°”瓦多 + GQ: 赤é“几内亚 + ER: 厄立特里亚 + EE: 爱沙尼亚 + ET: 埃塞俄比亚 + FK: ç¦å…‹å…°ç¾¤å²› + FO: 法罗群岛 + FJ: æ–æµŽ + FI: 芬兰 + FR: 法国 + GF: 法属圭亚那 + PF: 法属波利尼西亚 + TF: 法属å—部领地 + FQ: 法国å—éƒ¨å’Œå—æžé¢†åœ° + GA: 加蓬 + GM: 冈比亚 + GE: æ ¼é²å‰äºš + DE: 德国 + GH: 加纳 + GI: 直布罗陀 + GR: 希腊 + GL: 格陵兰 + GD: 格林纳达 + GP: 瓜德罗普 + GU: 关岛 + GT: å±åœ°é©¬æ‹‰ + GG: 根西岛 + GN: 几内亚 + GW: å‡ å†…äºšæ¯”ç» + GY: 圭亚那 + HT: 海地 + HM: 赫德岛和麦克å”纳群岛 + HN: 洪都拉斯 + HK: 中国香港特别行政区 + HU: 匈牙利 + IS: 冰岛 + IN: å°åº¦ + ID: å°åº¦å°¼è¥¿äºš + IR: 伊朗 + IQ: 伊拉克 + IE: 爱尔兰 + IM: 马æ©å²› + IL: 以色列 + IT: æ„大利 + JM: 牙买加 + JP: 日本 + JE: 泽西 + JT: 约翰斯顿岛 + JO: 约旦 + KZ: 哈è¨å…‹æ–¯å¦ + KE: 肯尼亚 + KI: 基里巴斯 + KW: ç§‘å¨ç‰¹ + KG: å‰å°”剿–¯æ–¯å¦ + LA: è€æŒ + LV: 拉脱维亚 + LB: 黎巴嫩 + LS: 莱索托 + LR: 利比里亚 + LY: 利比亚 + LI: 列支敦士登 + LT: ç«‹é™¶å®› + LU: 墿£®å ¡ + MO: 中国澳门 + MK: 马其顿 + MG: 马达加斯加 + MW: 马拉维 + MY: 马æ¥è¥¿äºš + MV: 马尔代夫 + ML: 马里 + MT: 马耳他 + MH: 马ç»å°”群岛 + MQ: 马æå°¼å…‹ + MR: 毛里塔尼亚 + MU: 毛里求斯 + YT: 马约特 + FX: 法国 + MX: 墨西哥 + FM: 密克罗尼西亚 + MI: 中途岛 + MD: 摩尔多瓦 + MC: 摩纳哥 + MN: è’™å¤ + ME: 黑山 + MS: 蒙特塞拉特 + MA: 摩洛哥 + MZ: 莫桑比克 + MM: 缅甸 + NA: 纳米比亚 + NR: ç‘™é² + NP: 尼泊尔 + NL: è·å…° + AN: è·å±žå®‰çš„列斯 + NT: 中性区 + NC: 新喀里多尼亚 + NZ: 新西兰 + NI: 尼加拉瓜 + NE: 尼日尔 + NG: 尼日利亚 + NU: 纽埃 + NF: 诺ç¦å…‹å²› + KP: æœé²œ + VD: 北越 + MP: 北马里亚纳群岛 + "NO": æŒªå¨ + OM: 阿曼 + PC: 太平洋岛屿托管领土 + PK: å·´åŸºæ–¯å¦ + PW: 帕劳 + PS: å·´å‹’æ–¯å¦é¢†åœŸ + PA: 巴拿马 + PZ: å·´æ‹¿é©¬è¿æ²³åŒº + PG: 巴布亚新几内亚 + PY: 巴拉圭 + YD: 也门人民民主共和国 + PE: ç§˜é² + PH: è²å¾‹å®¾ + PN: 皮特凯æ©ç¾¤å²› + PL: 波兰 + PT: è‘¡è„牙 + PR: æ³¢å¤šé»Žå„ + QA: å¡å¡”å°” + RO: 罗马尼亚 + RU: ä¿„ç½—æ–¯ + RW: 墿—ºè¾¾ + RE: 法属留尼旺 + BL: 圣巴泰勒米 + SH: 圣赫勒拿 + KN: 圣基茨和尼维斯 + LC: 圣å¢è¥¿äºš + MF: åœ£é©¬ä¸ + PM: 圣皮埃尔和密克隆 + VC: åœ£æ–‡æ£®ç‰¹å’Œæ ¼æž—çº³ä¸æ–¯ + WS: è¨æ‘©äºš + SM: 圣马力诺 + SA: 沙特阿拉伯 + SN: 塞内加尔 + RS: 塞尔维亚 + CS: 塞尔维亚和黑山 + SC: 塞舌尔 + SL: 塞拉利昂 + SG: æ–°åŠ å¡ + SK: 斯洛ä¼å…‹ + SI: 斯洛文尼亚 + SB: 所罗门群岛 + SO: 索马里 + ZA: å—éž + GS: å—ä¹”æ²»äºšå’Œå—æ¡‘å¨å¥‡ç¾¤å²› + KR: 韩国 + ES: 西ç­ç‰™ + LK: æ–¯é‡Œå…°å¡ + SD: è‹ä¸¹ + SR: è‹é‡Œå— + SJ: 斯瓦尔巴德和扬马延 + SZ: æ–¯å¨å£«å…° + SE: 瑞典 + CH: 瑞士 + SY: å™åˆ©äºš + ST: 圣多美和普林西比 + TW: å°æ¹¾ + TJ: å¡”å‰å…‹æ–¯å¦ + TZ: 妿¡‘尼亚 + TH: 泰国 + TL: ä¸œå¸æ±¶ + TG: 多哥 + TK: 托克劳 + TO: 汤加 + TT: 特立尼达和多巴哥 + TN: çªå°¼æ–¯ + TR: 土耳其 + TM: åœŸåº“æ›¼æ–¯å¦ + TC: 特克斯和凯科斯群岛 + TV: å›¾ç“¦å¢ + UM: 美国å°åž‹ç¦»å²› + PU: 美国其他太平洋群岛 + VI: 美属维尔京群岛 + UG: 乌干达 + UA: 乌克兰 + AE: 阿拉伯è”åˆé…‹é•¿å›½ + GB: 英国 + US: 美国 + ZZ: 未知或无效区域 + UY: 乌拉圭 + UZ: ä¹Œå…¹åˆ«å…‹æ–¯å¦ + VU: 瓦努阿图 + VA: 梵蒂冈城 + VE: 委内瑞拉 + VN: è¶Šå— + WK: 唤醒岛 + WF: 瓦利斯和富图纳 + EH: 西撒哈拉 + YE: 也门 + ZM: 赞比亚 + ZW: 津巴布韦 + AX: 奥兰群岛 + label_crm_cross_project_contacts: å…许跨项目è”系人关系 + label_crm_list_board: æ¿ + label_crm_default_list_style: 默认è”系风格 + label_crm_show_in_top_menu: 在顶部èœå•中显示 + label_crm_show_in_app_menu: 在应用èœå•中显示 + label_crm_money_settings: é’± + label_crm_disable_taxes: ç¦ç”¨ç¨Ž + label_crm_default_tax: 默认税值 + label_crm_tax_type: 税收ç§ç±» + label_crm_tax_type_inclusive: å«ç¨Ž + label_crm_tax_type_exclusive: 税率 + label_crm_default_currency: é»˜è®¤è´§å¸ + label_crm_thousands_delimiter: åƒä½åˆ†éš”符 + label_crm_decimal_separator: å进制分隔符 + + #3.2.10 + label_crm_add_contact_plural: 新建è”系人 + label_crm_search_for_contact: æœç´¢è”系人 + label_crm_major_currencies: 主è¦è´§å¸ + + #3.2.11 + label_crm_post_address_format: é‚®æ”¿åœ°å€æ ¼å¼ + label_crm_post_address_format_macros: "åœ°å€æ ¼å¼å®: %{macros}" + + #3.2.14 + label_crm_last_year: 去年 + + #3.2.15 + permission_export_contacts: 导出è”系人和åˆä½œ + + #3.4.0 + label_crm_deal_contact: åˆä½œçš„è”系人 + + #3.4.1 + label_crm_default_country: 默认国家 + label_attribute_of_contact: "è”系人的%{name}" + label_crm_contact_country: è”系人的国家 + label_crm_contact_city: è”系人的城市 + label_attribute_of_deals: "åˆä½œçš„ %{name}" + + #3.4.2 + permission_manage_public_deals_queries: 管ç†åˆä½œå…¬å…±æŸ¥è¯¢ + permission_save_deals_queries: ä¿å­˜åˆä½œæŸ¥è¯¢ + permission_manage_contact_issue_relations: 管ç†é—®é¢˜å…³ç³» + + #3.4.4 + label_crm_pipeline: æµæ°´çº¿ + text_crm_no_deal_statuses_in_project: 项目中没有åˆä½œçŠ¶æ€ + + #3.4.6 + label_crm_contact_email: è”系邮件 + label_crm_light_free_version: CRM Light å…费版本 + label_crm_link_to_pro: å‡çº§åˆ°PRO版本 + label_crm_link_to_pro_demo: PRO 版本演示Demo + label_crm_link_to_more_plugins: 查找更多的 RedmineCRM æ’ä»¶ + + + text_crm_string_incorrect_format: æ ¼å¼ä¸æ­£ç¡® + + label_deal_items: åˆä½œåç›® diff --git a/plugins/redmine_contacts/config/routes.rb b/plugins/redmine_contacts/config/routes.rb new file mode 100644 index 0000000..7e82de0 --- /dev/null +++ b/plugins/redmine_contacts/config/routes.rb @@ -0,0 +1,124 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +#custom routes for this plugin + resources :contacts, :path_names => {:contacts_notes => 'notes'} do + collection do + get :bulk_edit, :context_menu, :edit_mails, :contacts_notes + post :bulk_edit, :bulk_update, :send_mails, :preview_email + delete :bulk_destroy + end + member do + get 'tabs/:tab' => 'contacts#show', :as => "tabs" + get 'load_tab' => 'contacts#load_tab', :as => "load_tab" + end + resources :contacts_projects, :path => "projects", :only => [:new, :create, :destroy] + end + + resources :projects do + resources :contacts, :path_names => {:contacts_notes => 'notes'} do + collection do + get :contacts_notes + end + end + resources :contact_imports, :only => [:new, :create, :show] do + member do + get :settings + post :settings + get :mapping + post :mapping + get :run + post :run + end + end + resources :deal_imports, :only => [:new, :create, :show] do + member do + get :settings + post :settings + get :mapping + post :mapping + get :run + post :run + end + end + resources :deal_categories + + end + resources :deals do + collection do + get :bulk_edit, :context_menu, :edit_mails, :preview_email + post :bulk_edit, :bulk_update, :send_mails, :update_form + put :update_form + delete :bulk_destroy + end + end + + resources :projects do + resources :deals, :only => [:new, :create, :index] + resources :deal_categories, :only => [:new, :create, :index] + end + + resources :deal_categories, :only => [:edit, :update, :destroy] + + resources :deal_statuses, :except => :show do + collection do + put :assing_to_project + end + end + + resources :projects do + resources :crm_queries, :only => [:new, :create] + end + + resources :crm_queries, :except => [:show] + + resources :notes + + match '/contacts_tags', :controller => 'contacts_tags', :action => 'destroy', :via => :delete + + resources :contacts_tags do + collection do + post :merge, :context_menucha + get :context_menu, :merge + end + end + + match 'projects/:project_id/contacts/:contact_id/new_task' => 'contacts_issues#new', :via => :post + + match 'contacts/:contact_id/duplicates' => 'contacts_duplicates#index', :via => [:get, :post] + + match 'projects/:project_id/deal_categories/new' => 'deal_categories#new', :via => [:get, :post] + + + match 'auto_completes/taggable_tags' => 'auto_completes#taggable_tags', :via => :get, :as => 'auto_complete_taggable_tags' + match 'auto_completes/contact_tags' => 'auto_completes#contact_tags', :via => :get, :as => 'auto_complete_contact_tags' + match 'auto_completes/contacts' => 'auto_completes#contacts', :via => :get, :as => 'auto_complete_contacts' + match 'auto_completes/companies' => 'auto_completes#companies', :via => :get, :as => 'auto_complete_companies' + match 'auto_completes/deals' => 'auto_completes#deals', :via => :get, :as => 'auto_complete_deals' + + match 'users/new_from_contact/:id' => 'users#new_from_contact', :via => :get + match 'contacts_duplicates/:action' => 'contacts_duplicates', :via => [:get, :post] + match 'contacts_duplicates/search' => 'contacts_duplicates#search', :via => :get, :as => 'contacts_duplicates_search' + match 'contacts_issues/:action' => 'contacts_issues', :via => [:get, :post, :delete, :put] + match 'contacts_vcf/:action' => 'contacts_vcf', :via => [:get, :post] + match 'deal_contacts/:action' => 'deal_contacts', :via => [:get, :post, :delete] + match 'deals_tasks/:action' => 'deals_tasks', :via => [:get, :post, :put] + match 'contacts_settings/:action' => 'contacts_settings', :via => [:get, :post] + match 'contacts_mailer/:action' => 'contacts_mailer', :via => [:get, :post] + match 'attachments/contacts_thumbnail/:id(/:size)', :controller => 'attachments', :action => 'contacts_thumbnail', :id => /\d+/, :via => :get diff --git a/plugins/redmine_contacts/db/migrate/016_create_contacts.rb b/plugins/redmine_contacts/db/migrate/016_create_contacts.rb new file mode 100644 index 0000000..01fd7a9 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/016_create_contacts.rb @@ -0,0 +1,55 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateContacts < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :contacts do |t| + t.string :first_name + t.string :last_name + t.string :middle_name + t.string :company + t.text :address + t.string :phone + t.string :email + t.string :website + t.string :skype_name + t.date :birthday + t.string :avatar + t.text :background + t.string :job_title + t.boolean :is_company, :default => false + t.integer :author_id, :default => 0, :null => false + t.integer :assigned_to_id + t.datetime :created_on + t.datetime :updated_on + end + + add_index :contacts, :author_id + add_index :contacts, :company + add_index :contacts, :is_company + add_index :contacts, :email + add_index :contacts, :first_name + add_index :contacts, :assigned_to_id + + end + + def self.down + drop_table :contacts + end +end diff --git a/plugins/redmine_contacts/db/migrate/017_create_contacts_relations.rb b/plugins/redmine_contacts/db/migrate/017_create_contacts_relations.rb new file mode 100644 index 0000000..2bce846 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/017_create_contacts_relations.rb @@ -0,0 +1,50 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateContactsRelations < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :contacts_deals, :id => false do |t| + t.integer :deal_id + t.integer :contact_id + end + add_index :contacts_deals, :deal_id + add_index :contacts_deals, :contact_id + + create_table :contacts_issues, :id => false do |t| + t.integer :issue_id, :default => 0, :null => false + t.integer :contact_id, :default => 0, :null => false + end + add_index :contacts_issues, :issue_id + add_index :contacts_issues, :contact_id + + create_table :contacts_projects, :id => false do |t| + t.integer :project_id, :default => 0, :null => false + t.integer :contact_id, :default => 0, :null => false + end + add_index :contacts_projects, :project_id + add_index :contacts_projects, :contact_id + + end + + def self.down + drop_table :contacts_deals + drop_table :contacts_issues + drop_table :contacts_projects + end +end diff --git a/plugins/redmine_contacts/db/migrate/018_create_deals.rb b/plugins/redmine_contacts/db/migrate/018_create_deals.rb new file mode 100644 index 0000000..69f7b26 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/018_create_deals.rb @@ -0,0 +1,49 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateDeals < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :deals do |t| + t.string :name + t.text :background + t.integer :currency + t.integer :duration + t.decimal :price, :precision => 10, :scale => 2 + t.integer :price_type + t.integer :project_id + t.integer :author_id + t.integer :assigned_to_id + t.integer :status_id + t.integer :contact_id + t.integer :category_id + t.datetime :created_on + t.datetime :updated_on + end + add_index :deals, :contact_id + add_index :deals, :project_id + add_index :deals, :status_id + add_index :deals, :author_id + add_index :deals, :category_id + + end + + def self.down + drop_table :deals + end +end diff --git a/plugins/redmine_contacts/db/migrate/019_create_deals_relations.rb b/plugins/redmine_contacts/db/migrate/019_create_deals_relations.rb new file mode 100644 index 0000000..2e310f1 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/019_create_deals_relations.rb @@ -0,0 +1,61 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateDealsRelations < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :deal_categories do |t| + t.string :name, :null => false + t.integer :project_id + end + add_index :deal_categories, :project_id + + create_table :deal_processes do |t| + t.integer :deal_id, :null => false + t.integer :author_id, :null => false + t.integer :old_value + t.integer :value, :null => false + t.datetime :created_at + end + add_index :deal_processes, [:author_id] + add_index :deal_processes, [:deal_id] + + create_table :deal_statuses do |t| + t.string :name, :null => false + t.integer :position + t.boolean :is_default, :default => false, :null => false + t.boolean :is_closed, :default => false, :null => false + t.integer :color, :default => 11184810, :null => false + end + add_index :deal_statuses, [:is_closed] + + create_table :deal_statuses_projects, :id => false do |t| + t.integer :project_id, :default => 0, :null => false + t.integer :deal_status_id, :default => 0, :null => false + end + add_index :deal_statuses_projects, [:project_id, :deal_status_id] + + end + + def self.down + drop_table :deal_categories + drop_table :deal_processes + drop_table :deal_statuses + drop_table :deal_statuses_projects + end +end diff --git a/plugins/redmine_contacts/db/migrate/020_create_notes.rb b/plugins/redmine_contacts/db/migrate/020_create_notes.rb new file mode 100644 index 0000000..df49595 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/020_create_notes.rb @@ -0,0 +1,39 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateNotes < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :notes do |t| + t.string :subject + t.text :content + t.integer :source_id + t.string :source_type + t.integer :author_id + t.datetime :created_on + t.datetime :updated_on + end + add_index :notes, [:source_id, :source_type] + add_index :notes, [:author_id] + + end + + def self.down + drop_table :notes + end +end diff --git a/plugins/redmine_contacts/db/migrate/021_create_tags.rb b/plugins/redmine_contacts/db/migrate/021_create_tags.rb new file mode 100644 index 0000000..2dc44e8 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/021_create_tags.rb @@ -0,0 +1,30 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateTags < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + # unless table_exists?(:viewings) + ActiveRecord::Base.create_taggable_table + # end + end + + def self.down + ActiveRecord::Base.drop_taggable_table + end +end diff --git a/plugins/redmine_contacts/db/migrate/022_create_recently_vieweds.rb b/plugins/redmine_contacts/db/migrate/022_create_recently_vieweds.rb new file mode 100644 index 0000000..e5595d9 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/022_create_recently_vieweds.rb @@ -0,0 +1,37 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateRecentlyVieweds < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :recently_vieweds do |t| + t.references :viewer + t.references :viewed, :polymorphic => true + t.column :views_count, :integer + t.timestamps :null => false + end + + add_index :recently_vieweds, [:viewed_id, :viewed_type] + add_index :recently_vieweds, :viewer_id + + end + + def self.down + drop_table :recently_vieweds + end +end diff --git a/plugins/redmine_contacts/db/migrate/023_create_contacts_settings.rb b/plugins/redmine_contacts/db/migrate/023_create_contacts_settings.rb new file mode 100644 index 0000000..2532512 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/023_create_contacts_settings.rb @@ -0,0 +1,34 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateContactsSettings < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :contacts_settings do |t| + t.column :name, :string + t.column :value, :text + t.column :project_id, :integer + t.column :updated_on, :datetime + end + add_index :contacts_settings, :project_id + end + + def self.down + drop_table :contacts_settings + end +end diff --git a/plugins/redmine_contacts/db/migrate/024_add_type_to_notes.rb b/plugins/redmine_contacts/db/migrate/024_add_type_to_notes.rb new file mode 100644 index 0000000..fc0f4b1 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/024_add_type_to_notes.rb @@ -0,0 +1,29 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class AddTypeToNotes < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + add_column :notes, :type_id, :integer + add_index :notes, :type_id + end + + def self.down + remove_column :notes, :type_id + end +end diff --git a/plugins/redmine_contacts/db/migrate/025_add_fields_to_deals.rb b/plugins/redmine_contacts/db/migrate/025_add_fields_to_deals.rb new file mode 100644 index 0000000..62fe02b --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/025_add_fields_to_deals.rb @@ -0,0 +1,32 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class AddFieldsToDeals < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + change_column :deals, :duration, :integer, :default => 1 + add_column :deals, :due_date, :timestamp + add_column :deals, :probability, :integer + + end + + def self.down + remove_column :deals, :due_date + remove_column :deals, :probability + end +end diff --git a/plugins/redmine_contacts/db/migrate/026_create_contacts_queries.rb b/plugins/redmine_contacts/db/migrate/026_create_contacts_queries.rb new file mode 100644 index 0000000..3bbd7ed --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/026_create_contacts_queries.rb @@ -0,0 +1,42 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateContactsQueries < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def self.up + create_table :contacts_queries do |t| + t.integer :project_id + t.string :name, :default => "", :null => false + t.text :filters + t.integer :user_id, :default => 0, :null => false + t.boolean :is_public, :default => false, :null => false + t.text :column_names + t.text :sort_criteria + t.string :group_by + t.string :type + end + add_index :contacts_queries, :project_id + add_index :contacts_queries, :user_id + + + end + + def self.down + drop_table :contacts_queries + end +end diff --git a/plugins/redmine_contacts/db/migrate/027_change_deals_currency_type.rb b/plugins/redmine_contacts/db/migrate/027_change_deals_currency_type.rb new file mode 100644 index 0000000..d5e2edc --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/027_change_deals_currency_type.rb @@ -0,0 +1,27 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ChangeDealsCurrencyType < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + change_column :deals, :currency, :string + end + + def down + end +end diff --git a/plugins/redmine_contacts/db/migrate/028_add_cached_tag_list_to_contacts.rb b/plugins/redmine_contacts/db/migrate/028_add_cached_tag_list_to_contacts.rb new file mode 100644 index 0000000..284239f --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/028_add_cached_tag_list_to_contacts.rb @@ -0,0 +1,29 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class AddCachedTagListToContacts < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + add_column :contacts, :cached_tag_list, :string + + end + + def down + remove_column :contacts, :cached_tag_list + end +end diff --git a/plugins/redmine_contacts/db/migrate/029_add_visibility_to_contacts.rb b/plugins/redmine_contacts/db/migrate/029_add_visibility_to_contacts.rb new file mode 100644 index 0000000..90b2889 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/029_add_visibility_to_contacts.rb @@ -0,0 +1,38 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class AddVisibilityToContacts < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + add_column :contacts, :visibility, :integer, :default => Contact::VISIBILITY_PROJECT, :null => false + + Contact.find_each(:batch_size => 1000) do |contact| + contact.tag_list + contact.save + end + + ContactsSetting.all.each do |setting| + setting.value = YAML::load(setting.value.respond_to?(:force_encoding) ? setting.value.force_encoding('utf-8') : setting.value) if setting.value.is_a?(String) rescue '' + setting.save! + end + end + + def down + remove_column :contacts, :visibility + end +end diff --git a/plugins/redmine_contacts/db/migrate/030_change_deal_statuses_is_closed.rb b/plugins/redmine_contacts/db/migrate/030_change_deal_statuses_is_closed.rb new file mode 100644 index 0000000..50f73f8 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/030_change_deal_statuses_is_closed.rb @@ -0,0 +1,30 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ChangeDealStatusesIsClosed < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + remove_column :deal_statuses, :is_closed + add_column :deal_statuses, :status_type, :integer, :default => 0, :null => false + end + + def down + remove_column :deal_statuses, :status_type + add_column :deal_statuses, :is_closed, :boolean, :default => false, :null => false + end +end diff --git a/plugins/redmine_contacts/db/migrate/031_populate_contacts_module.rb b/plugins/redmine_contacts/db/migrate/031_populate_contacts_module.rb new file mode 100644 index 0000000..bbe8e36 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/031_populate_contacts_module.rb @@ -0,0 +1,30 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class PopulateContactsModule < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + EnabledModule.where(:name => 'contacts_module').update_all(:name => 'contacts') + EnabledModule.where(:name => 'contacts').select(:project_id).map(&:project_id).each{|p| EnabledModule.create(:project_id => p, :name => "deals")} + end + + def down + EnabledModule.where(:name => 'contacts').update_all(:name => 'contacts_module') + EnabledModule.where(:name => 'deals').delete_all + end +end diff --git a/plugins/redmine_contacts/db/migrate/032_create_addresses.rb b/plugins/redmine_contacts/db/migrate/032_create_addresses.rb new file mode 100644 index 0000000..65e16d2 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/032_create_addresses.rb @@ -0,0 +1,49 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateAddresses < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + create_table :addresses do |t| + t.string :street1 + t.string :street2 + t.string :city + t.string :region + t.string :postcode + t.string :country_code, :limit => 2 + t.text :full_address + t.string :address_type, :limit => 16 + t.references :addressable, :polymorphic => true + t.timestamps :null => false + end + add_index :addresses, [ :addressable_id, :addressable_type ] + add_index :addresses, :address_type + + Contact.all.each do |asset| + Address.create(:street1 => asset.attributes["address"].gsub(/\n/, ' ').first(250), :full_address => asset.attributes["address"], :address_type => "business", :addressable => asset) unless asset.attributes["address"].blank? + end + + remove_column(:contacts, :address) + end + + def down + add_column :contacts, :address, :text + drop_table :addresses + end + +end diff --git a/plugins/redmine_contacts/db/migrate/033_create_deals_issues.rb b/plugins/redmine_contacts/db/migrate/033_create_deals_issues.rb new file mode 100644 index 0000000..6d08168 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/033_create_deals_issues.rb @@ -0,0 +1,31 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class CreateDealsIssues < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def change + create_table :deals_issues do |t| + t.integer :issue_id, :default => 0, :null => false + t.integer :deal_id, :default => 0, :null => false + end + add_index :deals_issues, :issue_id + add_index :deals_issues, :deal_id + end + +end + diff --git a/plugins/redmine_contacts/db/migrate/034_change_deals_price_precision.rb b/plugins/redmine_contacts/db/migrate/034_change_deals_price_precision.rb new file mode 100644 index 0000000..7c77b37 --- /dev/null +++ b/plugins/redmine_contacts/db/migrate/034_change_deals_price_precision.rb @@ -0,0 +1,27 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ChangeDealsPricePrecision < Rails.version < '5.1' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2] + def up + change_column :deals, :price, :decimal, :precision => 20, :scale => 2 + end + + def down + end +end diff --git a/plugins/redmine_contacts/doc/CHANGELOG b/plugins/redmine_contacts/doc/CHANGELOG new file mode 100644 index 0000000..c315586 --- /dev/null +++ b/plugins/redmine_contacts/doc/CHANGELOG @@ -0,0 +1,406 @@ +== Redmine CRM plugin changelog + +Redmine CRM plugin - customer relationship management plugin for redmine +Copyright (C) 2010-2018 RedmineUP +http://www.redmineup.com/ + +== 2018-02-22 v4.1.2 + +* Redmine 4.0 dev support +* Select2 control issue's deal +* New CSV export UI for Contacts +* Deals layout fixes +* Show contacts deals on a company profile +* Avatar image on click action changed to show +* Fixed contacts column output for issues table +* Fixed deals pipeline bugs +* Fixed bug with import_settings +* Added missing assigned_to to Excel export +* Email validator fixes +* Duplications seach fixes + +== 2017-07-07 v4.1.1 + +* Redmine 3.4 support +* New styles for monochrome tags +* Fixed company rename bug +* Email validation fix +* Select2 drop box fix + +== 2017-05-30 v4.1.0 + +* Added Select2 to contacts fields +* Primary email was added to dublicate search +* Chinese translation update +* Fixed deal price precision bug +* Fixed avatar upload on modal form +* Fixed multivalues field import bug + +== 2017-03-02 v4.0.8 + +* Fixed migration requirements with new redmine_crm gem + +== 2017-03-02 v4.0.7 + +* Select2 control for selecting contacts and companies +* Modal for adding contacts to deal +* Fixed history log for done issue from contact page +* Fixed delete confirmation message for contacts, deals and notes +* Serbian translations (Aleksandar Pavic) + +== 2017-02-07 v4.0.6 + +* Deal lines with Products plugin +* Email incorrect format validation +* Fixed bulk edit tag fields + +== 2016-11-17 v4.0.5 + +* "+" button support +* German translation update (Marcel Müller) +* New pagination styles +* IDs filters for contacts and deals +* User activity duplication bug fixed +* Sales funnel bug fixed +* Fixed bug with select all contacts +* Fixed russian "б" in tags field +* Fixed enconding bug in vCard export +* Fixed bug with deal amount delimiter +* Fixed bug with sending attaches via email +* Import views compatibility fixes + +== 2015-12-02 v4.0.4 + +* Fixed major currencies bug +* Fixed bug with deals statistics +* Fixed bug with last notes duplication + +== 2015-11-06 v4.0.3 + +* New import UI for redmine 3.2+ +* Fixed company contacts show bug +* Fixed notes custom field bug +* Fixed bug with contacts queries +* Fixed bug with bigtext in contact notes +* Fixed custom field bug in Windows Edge +* Fixed bug with deal watchers delete link +* Fixed bug with contact url on contacts list view + +== 2015-08-24 v4.0.2 + +* Recalculate deals amount on board view +* Fixed bug with import State and Country fields +* Fixed bug with search in SQL Server + +== 2015-06-18 v4.0.1 + +* Redmine 3.1 support +* Fixed but with contact avatar preview +* Fixed bug on user creation +* Fixed CSV export error for trunk Redmine version + +== 2015-06-15 v4.0.0 + +* Redmine 3 support +* Spanish Translation update (Luis Blasco) +* Chinese translation (zhoutt) +* Fixed bug with sorting contacts by assignee +* ID field for contacts list + +== 2015-03-19 v3.4.5 + +* List optional filter for contact on issues list +* French translation update (Olivier Houdas) +* Swedish translation update (Khedron Wilk) +* Fixed recursion in contact creation +* redirect_on_sucess param for REST API contact creation +* Portuguese Brazilian Translation update (Leandro Gehlen) +* Spanish Translation update (Luis Blasco) +* Deal probability filter added (Jacek DyÅ‚o) +* Fixed bug with deal category deletion (Jacek DyÅ‚o) + +== 2014-11-14 v3.4.4 + +* Custom fields fixes for Redmine < 2.5 +* REST API for managing contact avatars +* Deals pipeline list view + +== 2014-11-10 v3.4.3 + +* Deals calendar view +* Company custom field +* Permissions for managing contact issue relations +* Expected revenue field and totals for deals table +* Serbian translation (Radenko) +* Fixed bug with create contact select (Modal window) +* Fixed bug with contacts table columns after quick search + +== 2014-10-16 v3.4.2 + +* Deal custom fields styles +* Contact name formats +* Global search by custom values +* Autocomplete contacts and deals search by tokens +* Deal add contact select limited by 50 contacts +* Cross-project deals autocomplete on issue edit form +* Fixed bug with group by custom field +* Notifications fixes + +== 2014-10-02 v3.4.1 + +* Seaching contacts by email in global redmine search +* Default country for address selection +* Greek translation (Filippos Karapetis) +* CSV import utf-8 fixes +* Fixed global list assignees and authors filters +* Fixed bug with contacts list custom field sorting +* Add uniqueness for contact first name on scope last name, company and email + +== 2014-09-21 v3.4.0 + +* Deal issues +* Spent time reports for deal and deal's contact +* Added currency and background filters for deals +* Added currency field for deals table +* Added formating for probability field +* Permissions check for issue list contact columns and filters +* Fixed bug with cached tags on tags merge, edit and delete +* REST API for contact projects create/destroy +* REST API for contact tag list + +== 2014-09-15 v3.3.0 + +* Deal queries +* REST API for deal statuses, contacts queries and deal categories +* CSV export custsom field columns order fixes (Sascha Hübner) +* CSV deals export empty price fixes +* Added deal name field to REST API +* Fixed bug with user creation from contact page +* Mark Deals menu item as selected on import + +== 2014-05-16 v3.2.17 + +* Deal macro deleted from light version +* New styles for color picker +* Compatibility fixes with other RedmineCRM plugins + +== 2014-05-03 v3.2.16 + +* Permission for export contacs & deals +* French translation update by Olivier Houdas +* Spanish tranlsation update by Leandro Russo +* Merge contacts fixes + +== 2014-03-31 v3.2.15 + +* Translation fixes for notes global search +* Disable unique validation by contact name +* Ajax contact tabs +* Fixed bug with Liquid gem requirements +* Fixed global search by contact note + +== 2014-02-25 v3.2.14 + +* Update Polish translation (Szymon Anders) +* Fixed bug with custom fields grouping in Contacts queries +* Fixed bug with bulk edit contacts with multiple value custom fields +* Fixed bug with date filters in contacts list + +== 2014-01-23 v3.2.13 + +* Fixed import deals link +* Show formated address on contact page + +== 2014-01-03 v3.2.12 + +* French translation update (Olivier Houdas) +* Check if deal status is in use before delete +* Fixed bug with top and app menu links with project id +* Fixed bug with show and export multiple values custom field + +== 2013-12-09 v3.2.11 + +* Livesearch case insensitive for PostgreSQL +* Added post address format settings +* Spanish translation update (Luis Blasco) +* German translation update (Alex Meindl) + +== 2013-12-06 v3.2.10 + +* Contacts autocomplete UI cleanup +* Export post code to CSV and Excel +* New modal form for connect contacts and issues +* Liquid drops and filters (new custom invoice template support) +* Setting for major currencies list +* Spanish translation update (Luis Blasco) +* German translation update (Alex Meindl) + +== 2013-11-20 v3.2.9 + +* Fixed bug with Redmine 2.4 compatibility (Show issue page SQL syntax error) +* Brazilian Portuguese translation (Marcelo Fernandes) +* Ajax moving deals on the board + +== 2013-11-16 v3.2.8 + +* Deals board view +* Coping deals +* Create deals from global list (autorefresh deal form) +* Select contacts and deals list view type +* Show authoring info for deals + +== 2013-10-22 v3.2.7 + +* Structured address for contacts + +== 2013-09-30 v3.2.6 + +* Merging tags +* New tags styles for redmine_tags plugin support +* Deals probability and close_date in excerpt list + +== 2013-09-20 v3.2.5 + +* Deals as a separate project module +* Fixed employees and employees notes security issues +* Send attachments to contact fixes +* Deals tab always present for projects with deals module +* Autocomplete for issues and deals contacts selection +* Support for contacts_google_sync plugin +* Czech translation fixes (Martin Å torkán) +* vCard export limit takes from issues export limit +* Fixed permission check for add and delete contact to project +* Contact avatar as deal logo + +== 2013-07-02 v3.2.4 + +* Ruby 2.0.0 support (vpim gem changed to vcard) +* Fixed bug with bulk update +* REST API for notes + +== 2013-04-25 v3.2.3 + +* Contact avatars and companies in autocompletes +* CSV export deals with custom fields +* UTF-8 encoding for CSV export files +* Contact tabs for Invoices and Helpdesk tickets +* Korean translation (eunsu) +* Fixed contact custom fields table sorting +* Fixed add note with custom fields + +== 2013-04-15 v3.2.2 + +* Contact page tabs +* New add note form with time field +* Ajax attachments for notes +* Fixed gravatar for companies +* Performance optimization +* Has deals and Has open issues filters for contacts +* Fixed deals filters with empty status field +* Updated field sorting for deals and contacts +* Exntended add contact modal form +* Open deals sum on contact deals tab +* Show and edit watchers on deal page +* Show deals on top menu setting + +== 2013-04-03 v3.2.1 + +* New default currencies CHF, SGD +* Core methods clean up +* Global contacts list performance optimization +* Fixed redirects after bulk deals destroy and update + +== 2013-03-18 v3.2.0 + +* Show deal status workflow +* Deals probability and due date +* Deal status open, won, lost types +* Export deals to CSV +* Create contact modal form +* Excerpt list sorting +* Deals table view +* Azerbaijanian translation (Saadat Mutallimova) +* Hungarian translation (Márk Sági-Kazár) +* Searching contacts by email address + +== 2013-02-04 v3.1.2 + +* Redmine 2.3 trunk support +* Performance optimization +* Fixed: Links changed to absolute +* Fixed: Contact thumbnail size + +== 2013-01-20 v3.1.1 + +* Redmine 2.2.2 support + +== 2013-01-10 v3.1.0 + +* Deals csv import +* Autocomplete for contact count > 50 on deal select +* Japan locale (Kitahara Kosei) +* Private and public contacts +* Search for merging contacts on contact edit page +* Notifications for contact/deal added and deal status changed +* Bug: Assigned to always shows in contact attributes sidebar +* Bug: "Position" field is shown on the Company editing page +* Bug: Deal macros is rendered incorrectly on FAQ page +* License files + +== 2012-11-01 v3.0.1 + +* Show new contact/deal events in activity pane +* Bulk change project for deals +* Setting for showing closes issues on contact page +* Support for Redmine 2.1 and Ruby on Rails 3.2 +* Deals in Activity tab +* Macro for referencing notes in the wiki +* Add assignee field to the contacts table in table view +* Ruby 1.9.3 support + +== 2012-04-x v2.3.3 + +* Move deals between projects +* Polish currency PLN +* Export contacts to XLS (MS Excel 2003) +* Option for hide the crm menu +* Bug #903: New contact cannot be added +* Bug #911: Note is not editable + +== 2012-04-12 v2.3.2 + +* Contact column for issues table + +== 2012-04-10 v2.3.1 + +* Bug #835: 500 error on view user page with contact relation +* Bug #611: Conflicts in label for global search and contact search + +== 2012-04-09 v2.3.0 + +* Import contacts via csv +* User defined filter for contact list +* Create and continue button for contact and deal +* Permissions for Add, Delete contacts and deals +* Better compatibility with high-rise and a1 theme +* Default currency for deals +* Select Inverse (tags) +* Filter Contacts that do not have a tag +* Selected Contacts that persist over multiple pages +* Â¥ currency for deals +* Rs currency for deals +* API for adding new note types +* New contacts list styles (excerpt, table, cards) +* Redmine 1.4 suport (new routings) +* Filter issues for contatcs (multiselect by natural person or company) +* No acts-as-taggable-on plugin/gem needed +* Find all issues for a contact or responsible person +* Brazilian translation (many thanks to Batista Hallison) +* Json API for contacts +* RSS feed for contacts notes +* Deals per page settings +* Last note filter +* Contacts columns selection for table view +* Bug #502: Contactors link +* Bug #523: Revised method for displaying notes +* Bug #555: Contacts CSV VCF export errors diff --git a/plugins/redmine_contacts/doc/COPYING b/plugins/redmine_contacts/doc/COPYING new file mode 100644 index 0000000..63e41a4 --- /dev/null +++ b/plugins/redmine_contacts/doc/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/plugins/redmine_contacts/doc/LICENSE b/plugins/redmine_contacts/doc/LICENSE new file mode 100644 index 0000000..e2e50b5 --- /dev/null +++ b/plugins/redmine_contacts/doc/LICENSE @@ -0,0 +1,26 @@ +LICENSING + +RedmineUP Licencing + +This End User License Agreement is a binding legal agreement between you and RedmineUP. Purchase, installation or use of RedmineUP Extensions provided on redmineup.com signifies that you have read, understood, and agreed to be bound by the terms outlined below. + +RedmineUP GPL Licencing + +All Redmine Extensions produced by RedmineUP are released under the GNU General Public License, version 2 (http://www.gnu.org/licenses/gpl-2.0.html). Specifically, the Ruby code portions are distributed under the GPL license. If not otherwise stated, all images, manuals, cascading style sheets, and included JavaScript are NOT GPL, and are released under the RedmineUP Proprietary Use License v1.0 (See below) unless specifically authorized by RedmineUP. Elements of the extensions released under this proprietary license may not be redistributed or repackaged for use other than those allowed by the Terms of Service. + +RedmineUP Proprietary Use License (v1.0) + +The RedmineUP Proprietary Use License covers any images, cascading stylesheets, manuals and JavaScript files in any extensions produced and/or distributed by redmineup.com. These files are copyrighted by redmineup.com (RedmineUP) and cannot be redistributed in any form without prior consent from redmineup.com (RedmineUP) + +Usage Terms + +You are allowed to use the Extensions on one or many "production" domains, depending on the type of your license +You are allowed to make any changes to the code, however modified code will not be supported by us. + +Modification Of Extensions Produced By RedmineUP. + +You are authorized to make any modification(s) to RedmineUP extension Ruby code. However, if you change any Ruby code and it breaks functionality, support may not be available to you. + +In accordance with the RedmineUP Proprietary Use License v1.0, you may not release any proprietary files (modified or otherwise) under the GPL license. The terms of this license and the GPL v2 prohibit the removal of the copyright information from any file. + +Please contact us if you have any requirements that are not covered by these terms. \ No newline at end of file diff --git a/plugins/redmine_contacts/init.rb b/plugins/redmine_contacts/init.rb new file mode 100755 index 0000000..fc98137 --- /dev/null +++ b/plugins/redmine_contacts/init.rb @@ -0,0 +1,164 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +requires_redmine_crm :version_or_higher => '0.0.33' rescue raise "\n\033[31mRedmine requires newer redmine_crm gem version.\nPlease update with 'bundle update redmine_crm'.\033[0m" + +require 'redmine' + +CONTACTS_VERSION_NUMBER = '4.1.2' +CONTACTS_VERSION_TYPE = "PRO version" + +if ActiveRecord::VERSION::MAJOR >= 4 + require 'csv' + FCSV = CSV +end + +Redmine::Plugin.register :redmine_contacts do + name "Redmine CRM plugin (#{CONTACTS_VERSION_TYPE})" + author 'RedmineUP' + description 'This is a CRM plugin for Redmine that can be used to track contacts and deals information' + version CONTACTS_VERSION_NUMBER + url 'https://www.redmineup.com/pages/plugins/crm' + author_url 'mailto:support@redmineup.com' + + requires_redmine :version_or_higher => '2.3' + + settings :default => { + :name_format => :lastname_firstname.to_s, + :auto_thumbnails => true, + :major_currencies => "USD, EUR, GBP, RUB, CHF", + :contact_list_default_columns => ["first_name", "last_name"], + :max_thumbnail_file_size => 300 + }, :partial => 'settings/contacts/contacts' + project_module :deals do + permission :delete_deals, :deals => [:destroy, :bulk_destroy] + permission :view_deals, { + :deals => [:index, :show, :context_menu], + :notes => [:show], + :deal_categories => [:index] + }, :read => true + permission :edit_deals, { + :deals => [:edit, :update, :add_attachment, :bulk_update, :bulk_edit, :update_form], + :deal_contacts => [:search, :autocomplete, :add, :delete], + :notes => [:create, :destroy, :update] + } + permission :add_deals, { + :deals => [:new, :create, :update_form] + } + + permission :manage_deals, { + :deal_categories => [:new, :edit, :destroy, :update, :create], + :deal_statuses => [:assing_to_project], :require => :member + } + + permission :delete_deal_watchers, { :watchers => :destroy } + permission :import_deals, {:deal_imports => [:new, :create, :show, :settings, :mapping, :run]} + end + + project_module :contacts do + permission :view_contacts, { + :contacts => [:show, :index, :live_search, :contacts_notes, :context_menu], + :notes => [:show] + }, :read => true + permission :view_private_contacts, { + :contacts => [:show, :index, :live_search, :contacts_notes, :context_menu], + :notes => [:show] + }, :read => true + + permission :add_contacts, { + :contacts => [:new, :create], + :contacts_duplicates => [:index, :duplicates], + :contacts_vcf => [:load] + } + + permission :edit_contacts, { + :contacts => [:edit, :update, :bulk_update, :bulk_edit], + :notes => [:create, :destroy, :edit, :update], + :contacts_duplicates => [:index, :merge, :duplicates], + :contacts_projects => [:new, :destroy, :create], + :contacts_vcf => [:load] + } + + permission :manage_contact_issue_relations, { + :contacts_issues => [:new, :create_issue, :create, :delete, :close, :autocomplete_for_contact], + } + + permission :delete_contacts, :contacts => [:destroy, :bulk_destroy] + permission :add_notes, :notes => [:create] + permission :delete_notes, :notes => [:destroy, :edit, :update] + permission :delete_own_notes, :notes => [:destroy, :edit, :update] + + permission :manage_contacts, { + :projects => :settings, + :contacts_settings => :save, + } + permission :import_contacts, {:contact_imports => [:new, :create, :show, :settings, :mapping, :run]} + permission :export_contacts, {} + permission :send_contacts_mail, :contacts => [:edit_mails, :send_mails, :preview_email] + permission :manage_public_contacts_queries, {}, :require => :member + permission :save_contacts_queries, {}, :require => :loggedin + permission :manage_public_deals_queries, {}, :require => :member + permission :save_deals_queries, {}, :require => :loggedin + + end + + menu :project_menu, :contacts, {:controller => 'contacts', :action => 'index'}, :caption => :contacts_title, :param => :project_id + menu :project_menu, :new_contact, {:controller => 'contacts', :action => 'new'}, :caption => :label_crm_contact_new, :param => :project_id, :parent => :new_object + + menu :top_menu, :contacts, + {:controller => 'contacts', :action => 'index', :project_id => nil}, + :caption => :label_contact_plural, + :if => Proc.new{ User.current.allowed_to?({:controller => 'contacts', :action => 'index'}, + nil, {:global => true}) && ContactsSetting.contacts_show_in_top_menu? } + + menu :application_menu, :contacts, + {:controller => 'contacts', :action => 'index'}, + :caption => :label_contact_plural, + :if => Proc.new{ User.current.allowed_to?({:controller => 'contacts', :action => 'index'}, + nil, {:global => true}) && ContactsSetting.contacts_show_in_app_menu? } + menu :top_menu, :deals, + {:controller => 'deals', :action => 'index', :project_id => nil}, + :caption => :label_deal_plural, + :if => Proc.new{ User.current.allowed_to?({:controller => 'deals', :action => 'index'}, + nil, {:global => true}) && ContactsSetting.deals_show_in_top_menu? } + menu :application_menu, :deals, + {:controller => 'deals', :action => 'index'}, + :caption => :label_deal_plural, + :if => Proc.new{ User.current.allowed_to?({:controller => 'deals', :action => 'index'}, + nil, {:global => true}) && ContactsSetting.deals_show_in_app_menu? } + + menu :project_menu, :deals, {:controller => 'deals', :action => 'index' }, + :caption => :label_deal_plural, + :param => :project_id + + menu :project_menu, :new_deal, {:controller => 'deals', :action => 'new'}, :caption => :label_crm_deal_new, :param => :project_id, :parent => :new_object + + menu :admin_menu, :contacts, {:controller => 'settings', :action => 'plugin', :id => "redmine_contacts"}, :caption => :contacts_title, :html => {:class => 'icon'} + + activity_provider :contacts, :default => false, :class_name => ['ContactNote', 'Contact'] + activity_provider :deals, :default => false, :class_name => ['DealNote', 'Deal'] + + Redmine::Search.map do |search| + search.register :contacts + search.register :deals + end + +end + +require 'redmine_contacts' diff --git a/plugins/redmine_contacts/lib/acts_as_priceable/init.rb b/plugins/redmine_contacts/lib/acts_as_priceable/init.rb new file mode 100644 index 0000000..14de33e --- /dev/null +++ b/plugins/redmine_contacts/lib/acts_as_priceable/init.rb @@ -0,0 +1,21 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.dirname(__FILE__) + '/lib/acts_as_priceable' +ActiveRecord::Base.send(:include, RedmineContacts::Acts::Priceable) diff --git a/plugins/redmine_contacts/lib/acts_as_priceable/lib/acts_as_priceable.rb b/plugins/redmine_contacts/lib/acts_as_priceable/lib/acts_as_priceable.rb new file mode 100644 index 0000000..165f0ef --- /dev/null +++ b/plugins/redmine_contacts/lib/acts_as_priceable/lib/acts_as_priceable.rb @@ -0,0 +1,66 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Acts + module Priceable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_priceable(*args) + priceable_options = args + priceable_options << :price if priceable_options.empty? + priceable_methods = "" + priceable_options.each do |priceable_attr| + priceable_methods << %( + def #{priceable_attr.to_s}_to_s + object_price( + self, + :#{priceable_attr}, + { + :decimal_mark => ContactsSetting.decimal_separator, + :thousands_separator => ContactsSetting.thousands_delimiter + } + ) if self.respond_to?(:#{priceable_attr}) + end + ) + end + + class_eval <<-EOV + include RedmineCrm::MoneyHelper + include RedmineContacts::Acts::Priceable::InstanceMethods + + #{priceable_methods} + EOV + + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + end + + end + end +end diff --git a/plugins/redmine_contacts/lib/acts_as_taggable_on_patch.rb b/plugins/redmine_contacts/lib/acts_as_taggable_on_patch.rb new file mode 100644 index 0000000..e9011c4 --- /dev/null +++ b/plugins/redmine_contacts/lib/acts_as_taggable_on_patch.rb @@ -0,0 +1,59 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require 'acts-as-taggable-on' + +module ActsAsTaggableOn::Taggable + module Core + module InstanceMethods + + def process_dirty_object(context, new_list) + value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list + attrib = "#{context.to_s.singularize}_list" + + if changed_attributes.include?(attrib) + old = changed_attributes[attrib] + @changed_attributes.delete(attrib) if old.to_s == value.to_s + else + old = tag_list_on(context) + if self.class.preserve_tag_order + @changed_attributes[attrib] = old if old.to_s != value.to_s + else + @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn::TagList.new(value.split(',')).sort + end + end + end + + def attributes_for_update(attribute_names) + filter_tag_lists(super) + end + + + def attributes_for_create(attribute_names) + filter_tag_lists(super) + end + + def filter_tag_lists(attributes) + tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"} + attributes.delete_if {|attr| tag_lists.include? attr } + end + + end + end +end diff --git a/plugins/redmine_contacts/lib/acts_as_viewable/init.rb b/plugins/redmine_contacts/lib/acts_as_viewable/init.rb new file mode 100644 index 0000000..23a36d2 --- /dev/null +++ b/plugins/redmine_contacts/lib/acts_as_viewable/init.rb @@ -0,0 +1,30 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +$LOAD_PATH.unshift(File.dirname(__FILE__)) + +require "lib/acts_as_viewable" + +$LOAD_PATH.shift + +# Rails.configuration.to_prepare do +# unless ActiveRecord::Base.included_modules.include?(ActsAsViewable::Viewable) +ActiveRecord::Base.send(:include, ActsAsViewable::Viewable) +# end +# end diff --git a/plugins/redmine_contacts/lib/acts_as_viewable/lib/acts_as_viewable.rb b/plugins/redmine_contacts/lib/acts_as_viewable/lib/acts_as_viewable.rb new file mode 100644 index 0000000..bde7155 --- /dev/null +++ b/plugins/redmine_contacts/lib/acts_as_viewable/lib/acts_as_viewable.rb @@ -0,0 +1,59 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module ActsAsViewable + module Viewable + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def acts_as_viewable(options = {}) + cattr_accessor :viewable_options + self.viewable_options = {} + viewable_options[:info] = options.delete(:info) || "info".to_sym + if ActiveRecord::VERSION::MAJOR >= 4 + has_many :views, lambda { order("#{RecentlyViewed.table_name}.updated_at DESC") }, :class_name => "RecentlyViewed", :as => :viewed, :dependent => :delete_all + else + has_many :views, :order => "#{RecentlyViewed.table_name}.updated_at DESC", :class_name => "RecentlyViewed", :as => :viewed, :dependent => :delete_all + end + + # attr_reader :info + + send :include, ActsAsViewable::Viewable::InstanceMethods + end + end + + module InstanceMethods + def self.included(base) + base.extend ClassMethods + end + + def viewed(user = User.current) + rv = (self.views.where(:viewer_id => User.current.id).first || self.views.new(:viewer => user)) + rv.increment(:views_count) + rv.save! + end + + module ClassMethods + end + end + + end +end diff --git a/plugins/redmine_contacts/lib/company_custom_field_format.rb b/plugins/redmine_contacts/lib/company_custom_field_format.rb new file mode 100644 index 0000000..8a5547d --- /dev/null +++ b/plugins/redmine_contacts/lib/company_custom_field_format.rb @@ -0,0 +1,60 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module Redmine + module FieldFormat + + class CompanyFormat < RecordList + + add 'company' + self.customized_class_names = nil + self.multiple_supported = false + + def label + "label_crm_company" + end + + def target_class + @target_class ||= Contact + end + + def edit_tag(view, tag_id, tag_name, custom_value, options={}) + contact = Contact.where(:id => custom_value.value).first unless custom_value.value.blank? + view.select_contact_tag(tag_name, contact, options.merge(:id => tag_id, + :is_company => true, + :add_contact => true, + :include_blank => !custom_value.custom_field.is_required)) + end + + def cast_single_value(custom_field, value, customized = nil) + Contact.where(:id => value).first unless value.blank? + end + + def query_filter_options(custom_field, query) + super.merge({:field_format => 'company'}) + end + def possible_values_options(custom_field, object = nil) + [] + end + end + + end +end + +Redmine::FieldFormat.add 'company', Redmine::FieldFormat::CompanyFormat diff --git a/plugins/redmine_contacts/lib/csv_importable.rb b/plugins/redmine_contacts/lib/csv_importable.rb new file mode 100644 index 0000000..cce493d --- /dev/null +++ b/plugins/redmine_contacts/lib/csv_importable.rb @@ -0,0 +1,121 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module CSVImportable + class << self; attr_accessor :klass end + + def persisted? + false + end + + def initialize(attributes = {}) + attributes.each{|name, value| send("#{name}=", value)} + end + + def save + if imported_instances.any? && imported_instances.map(&:valid?).all? && imported_instances.map{|c| c.new_record? ? true : (c.respond_to?('editable?') ? c.editable? : true)}.all? + begin + klass.transaction do + imported_instances.each(&:save!) + end + rescue + Rails.logger.info $!.message + Rails.logger.info $!.backtrace.join("\n") + return false + end + true + else + imported_instances.each_with_index do |instance, index| + if !instance.new_record? && !instance.editable? + errors.add :base, "Row #{index + 2}: Permission restricted for changing #{klass.name}" + else + instance.errors.full_messages.each do |message| + errors.add :base, "Row #{index + 2}: #{message}" + end + end + end + false + end + end + + def imported_instances + @imported_instances ||= load_imported_instances + end + +protected + def force_utf8(v) + if v.respond_to? :force_encoding then v.force_encoding('utf-8') else v end + end + +private + def build_custom_fields + custom_fields_attributes = {} + klass.new.custom_field_values.each do |custom_field_value| + custom_fields_attributes[custom_field_value.custom_field.id.to_s] = custom_field_value.custom_field.cast_value(row[custom_field_value.custom_field.name]).to_s + end + custom_fields_attributes + end + + def load_imported_instances + instance_rows = open_import_source + klass.transaction do + begin + line_counter = 0 + instance_rows.map do |row| + line_counter += 1 + instance = klass.find_by_id(row['#']) || klass.new + instance.attributes = build_from_fcsv_row(row) + if instance.respond_to?(:custom_field_values) + instance.custom_field_values.each do |custom_field_value| + custom_field_value.value = custom_field_value.custom_field.cast_value(row[custom_field_value.custom_field.name.underscore]).to_s + end + end + + instance.project = project if instance.respond_to?(:project=) + puts instance.errors.full_messages unless instance.valid? + instance + end + rescue + Rails.logger.info $!.message + Rails.logger.info $!.backtrace.join("\n") + errors.add :base, I18n.t(:text_crm_error_on_line, :line => line_counter, :error => $!.message) + [] + end + end + end + + def open_import_source + begin + content = file.read + FCSV.parse(content, :headers => true, :header_converters => [:downcase], :encoding => 'utf-8', :col_sep => guess_separator(force_utf8(content)), :quote_char => quotes_type) + rescue Exception => e + Rails.logger.info $!.message + Rails.logger.info $!.backtrace.join("\n") + errors.add :base, e.message + [] + end + end + + def guess_separator(content) + first_line = content.split("\n").first + commas = first_line.count(",") + semicolons = first_line.count(";") + commas > semicolons ? ',' : ';' + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts.rb b/plugins/redmine_contacts/lib/redmine_contacts.rb new file mode 100644 index 0000000..9bad4fa --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts.rb @@ -0,0 +1,94 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require 'redmine_contacts/patches/action_controller_patch' +require 'redmine_contacts/patches/compatibility/application_helper_patch' +require 'redmine_contacts/helpers/contacts_helper' +require 'redmine_contacts/helpers/crm_calendar_helper' + +# Plugins +require 'acts_as_viewable/init' +require 'acts_as_priceable/init' +require 'company_custom_field_format' if Redmine::VERSION.to_s > '2.5' + +require 'redmine_contacts/utils/thumbnail' +require 'redmine_contacts/utils/check_mail' +require 'redmine_contacts/utils/date_utils' +require 'redmine_contacts/utils/csv_utils' +require 'redmine_contacts/contacts_project_setting' + +# Patches +require 'redmine_contacts/patches/compatibility/active_record_base_patch' +require 'redmine_contacts/patches/compatibility/active_record_sanitization_patch.rb' +require 'redmine_contacts/patches/compatibility/user_patch.rb' +require 'redmine_contacts/patches/compatibility_patch' +require 'redmine_contacts/patches/issue_patch' +require 'redmine_contacts/patches/project_patch' +require 'redmine_contacts/patches/mailer_patch' +require 'redmine_contacts/patches/notifiable_patch' +require 'redmine_contacts/patches/application_controller_patch' +require 'redmine_contacts/patches/attachments_controller_patch' +require 'redmine_contacts/patches/auto_completes_controller_patch' +require 'redmine_contacts/patches/issue_query_patch' +require 'redmine_contacts/patches/time_entry_query_patch' +require 'redmine_contacts/patches/query_patch' +if Redmine::VERSION.to_s >= '3.4' || Redmine::VERSION::BRANCH != 'stable' + require 'redmine_contacts/patches/query_filter_patch' + require 'redmine_contacts/patches/issues_helper_patch' +end +require 'redmine_contacts/patches/users_controller_patch' +require 'redmine_contacts/patches/issues_controller_patch' +require 'redmine_contacts/patches/custom_fields_helper_patch' +require 'redmine_contacts/patches/time_report_patch' +require 'redmine_contacts/patches/queries_helper_patch' +require 'redmine_contacts/patches/timelog_helper_patch' +require 'redmine_contacts/patches/projects_helper_patch' + +require 'redmine_contacts/wiki_macros/contacts_wiki_macros' + +# Hooks +require 'redmine_contacts/hooks/views_projects_hook' +require 'redmine_contacts/hooks/views_issues_hook' +require 'redmine_contacts/hooks/views_layouts_hook' +require 'redmine_contacts/hooks/views_users_hook' +require 'redmine_contacts/hooks/views_custom_fields_hook' +require 'redmine_contacts/hooks/controllers_time_entry_reports_hook' + +require 'redmine_contacts/liquid/liquid' if Object.const_defined?("Liquid") rescue false + +module RedmineContacts + def self.companies_select + RedmineContacts.settings["select_companies_to_deal"] + end + + def self.settings() Setting[:plugin_redmine_contacts].blank? ? {} : Setting[:plugin_redmine_contacts] end + + def self.default_list_style + return (%w(list list_excerpt list_cards) && [RedmineContacts.settings["default_list_style"]]).first || "list_excerpt" + return 'list_excerpt' + end + + def self.products_plugin_installed? + @@products_plugin_installed ||= (Redmine::Plugin.installed?(:redmine_products) && Redmine::Plugin.find(:redmine_products).version >= '2.0.2') + end + + def self.unstable_branch? + Redmine::VERSION::BRANCH != 'stable' + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/contacts_project_setting.rb b/plugins/redmine_contacts/lib/redmine_contacts/contacts_project_setting.rb new file mode 100644 index 0000000..47ce016 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/contacts_project_setting.rb @@ -0,0 +1,48 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsProjectSetting + unloadable + + def initialize(project, plugin_name) + @project = project + @plugin_settings_name = plugin_name + Setting["plugin_" + @plugin_settings_name] + end + + def method_missing(method_name, *args, &block) + return super if /^(.*=)$/ =~ method_name.to_s + setting_name = method_name.to_s.gsub(/\?|=/, '') + setting_value = if ContactsSetting[@plugin_settings_name + '_' + setting_name, @project].blank? + if ContactsSetting.respond_to?(method_name) + ContactsSetting.send(method_name) + else + Setting["plugin_" + @plugin_settings_name][setting_name] + end + else + ContactsSetting[@plugin_settings_name + '_' + setting_name, @project] + end + + if /.*\?$/ =~ method_name.to_s + setting_value.to_i > 0 + else + setting_value + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/helpers/contacts_helper.rb b/plugins/redmine_contacts/lib/redmine_contacts/helpers/contacts_helper.rb new file mode 100644 index 0000000..ba65a77 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/helpers/contacts_helper.rb @@ -0,0 +1,333 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Helper + def contact_tag_url(tag_name, options = {}) + { :controller => 'contacts', + :action => 'index', + :set_filter => 1, + :project_id => @project, + :fields => [:tags], + :values => { :tags => [tag_name] }, + :operators => { :tags => '=' } }.merge(options) + end + + def skype_to(skype_name, _name = nil) + return link_to skype_name, 'skype:' + skype_name + '?call' unless skype_name.blank? + end + + def tag_link(tag_name, options = {}) + style = ContactsSetting.monochrome_tags? ? { :class => 'tag-label' } : { :class => 'tag-label-color', :style => "background-color: #{tag_color(tag_name)}" } + tag_count = options.delete(:count) + link = link_to tag_name, contact_tag_url(tag_name), options + link += content_tag(:span, "(#{tag_count})", :class => 'tag-count') if tag_count + content_tag(:span, link, {}.merge(style)) + end + + def tag_color(tag_name) + "##{'%06x' % (tag_name.unpack('H*').first.hex % 0xffffff)}" + # "##{"%06x" % (Digest::MD5.hexdigest(tag_name).hex % 0xffffff)}" + # "##{"%06x" % (tag_name.hash % 0xffffff).to_s}" + end + + def tag_links(tag_list, options = {}) + content_tag(:span, safe_join(tag_list.map { |tag| tag_link(tag, options) }, ContactsSetting.monochrome_tags? ? ', ' : ' ').html_safe, + :class => "tag_list#{' icon icon-tag' if ContactsSetting.monochrome_tags?}") if tag_list + end + + def contacts_for_select(project, options = {}) + scope = Contact.where(options[:where]) + scope = scope.limit(options[:limit] || 500) + scope = scope.companies if options.delete(:is_company) + scope = scope.joins(:projects) + scope = Rails.version >= '5.1' ? scope.distinct : scope.uniq + scope = scope.where(Contact.visible_condition(User.current)) + scope = scope.by_project(project) if project + scope.to_a.sort! { |x, y| x.name <=> y.name }.collect { |m| [options[:short_label] ? m.name : m.name_with_company, m.id.to_s] } + end + + def link_to_remote_list_update(text, url_params) + link_to_remote(text, { :url => url_params, :method => :get, :update => 'contact_list', :complete => 'window.scrollTo(0,0)' }, + { :href => url_for(:params => url_params) } + ) + end + + def contacts_check_box_tags(name, contacts) + s = '' + contacts.each do |contact| + s << "\n" + end + s.html_safe + end + + def note_source_url(note_source, options = {}) + polymorphic_path(note_source, options.merge(:project_id => @project)) + # return {:controller => note_source.class.name.pluralize.downcase, :action => 'show', :project_id => @project, :id => note_source.id } + end + + def link_to_source(note_source, options = {}) + link_to note_source.name, note_source_url(note_source, options) + end + + def countries_options_for_select(selected = nil) + default_country = l(:label_crm_countries)[ContactsSetting.default_country.to_s.upcase.to_sym] if ContactsSetting.default_country + countries = countries_for_select + countries = [[default_country, ContactsSetting.default_country.to_s.upcase], ['---', '']] | countries if default_country + options_for_select(countries, :disabled => '', :selected => selected) + end + + def countries_for_select + l(:label_crm_countries).map { |k, v| [v, k.to_s] }.sort + end + + def select_contact_tag(name, contact, options={}) + cross_project_contacts = ContactsSetting.cross_project_contacts? || !!options.delete(:cross_project_contacts) + field_id = sanitize_to_id(name) + include_blank = !!options[:include_blank] + is_company = !!options[:is_company] + add_contact = !!options[:add_contact] + + s = '' + s << select_tag(name, options_for_select([[contact.try(:name_with_company), contact.try(:id)]], contact.try(:id)), :include_blank => true) + s << javascript_tag("$('##{field_id}').select2({ + ajax: { + url: '#{auto_complete_contacts_path(:project_id => (cross_project_contacts ? nil : @project), :is_company => (options[:is_company] ? '1' : nil))}', + dataType: 'json', + delay: 250, + data: function (params) { + return { q: params.term }; + }, + processResults: function (data, params) { + return { results: data }; + }, + cache: true + }, + placeholder: ' ', + allowClear: #{include_blank}, + minimumInputLength: 0, + width: '60%', + templateResult: formatState + }).on('select2:open', function (e) { + $('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val(''); + }); + function formatState (opt) { + var $opt = $('' + opt.avatar + ' ' + opt.text + ''); + return $opt; + };") + + if add_contact + s << link_to(image_tag('add.png', :style => 'vertical-align: middle; margin-left: 5px;'), + new_project_contact_path(@project, :contact_field_name => name, :contacts_is_company => is_company), + :remote => true, + :method => 'get', + :title => l(:label_crm_contact_new), + :id => "#{field_id}_add_link", + :tabindex => 200) if authorize_for('contacts', 'new') + s << javascript_include_tag('attachments') + end + + s.html_safe + end + + # TODO: Need to add tests for this method (avatar_to). + def avatar_to(obj, options = {}) + # "https://avt.appsmail.ru/mail/sin23matvey/_avatar" + + options[:size] ||= '64' + if ActiveRecord::VERSION::MAJOR >= 4 + unless options[:size].to_s.include?('x') + options[:size] = "#{options[:size]}x#{options[:size]}" + end + else + options[:width] ||= options[:size] + options[:height] ||= options[:size] + end + + options[:class] = 'gravatar' + + obj_icon = obj.is_a?(Contact) ? (obj.is_company ? 'company.png' : 'person.png') : (obj.is_a?(Deal) ? 'deal.png' : 'unknown.png') + + return image_tag(obj_icon, options.merge(:plugin => 'redmine_contacts')) if ENV['NO_AVATAR'] + + if obj.is_a?(Deal) + if obj.contact + avatar_to(obj.contact, options) + else + image_tag(obj_icon, options.merge(:plugin => 'redmine_contacts')) + end + elsif obj.is_a?(Contact) && (avatar = obj.avatar) && avatar.readable? + avatar_url = url_for :controller => 'attachments', :action => 'contacts_thumbnail', :id => avatar, :size => options[:size] + if options[:full_size] + link_to(image_tag(avatar_url, options), :controller => 'attachments', :action => 'show', :id => avatar, :filename => avatar.filename) + else + image_tag(avatar_url, options) + end + elsif obj.respond_to?(:facebook) && !obj.facebook.blank? + image_tag("https://graph.facebook.com/#{obj.facebook.gsub('.*facebook.com\/', '')}/picture?type=square#{'&return_ssl_resources=1' if (request && request.ssl?)}", options) + elsif Setting.gravatar_enabled? && obj.is_a?(Contact) && obj.primary_email + # options.merge!({:ssl => (request && request.ssl?), :default => "#{request.protocol}#{request.host_with_port}/plugin_assets/redmine_contacts/images/#{obj_icon}"}) + # gravatar(obj.primary_email.downcase, options) rescue image_tag(obj_icon, options.merge({:plugin => "redmine_contacts"})) + avatar("<#{obj.primary_email}>", options) + else + image_tag(obj_icon, options.merge(:plugin => 'redmine_contacts')) + end + end + + def contact_tag(contact, options={}) + avatar_size = options.delete(:size) || 16 + if contact.visible? && !options[:no_link] + contact_avatar = link_to(avatar_to(contact, :size => avatar_size), contact_path(contact, :project_id => @project), :id => "avatar") + contact_name = link_to_source(contact, :project_id => @project) + else + contact_avatar = avatar_to(contact, :size => avatar_size) + contact_name = contact.name + end + + case options.delete(:type).to_s + when 'avatar' + contact_avatar.html_safe + when 'plain' + contact_name.html_safe + else + content_tag(:span, "#{contact_avatar} #{contact_name}".html_safe, :class => 'contact') + end + end + + def render_contact_tooltip(contact, options = {}) + @cached_label_crm_company ||= l(:field_contact_company) + @cached_label_job_title = contact.is_company ? l(:field_company_field) : l(:field_contact_job_title) + @cached_label_phone ||= l(:field_contact_phone) + @cached_label_email ||= l(:field_contact_email) + + emails = contact.emails.any? ? contact.emails.map { |email| "#{mail_to email}" }.join(', ') : '' + phones = contact.phones.any? ? contact.phones.map { |phone| "#{phone}" }.join(', ') : '' + + s = link_to_contact(contact, options) + '

        '.html_safe + s << "#{@cached_label_job_title}: #{contact.job_title}
        ".html_safe unless contact.job_title.blank? + s << "#{@cached_label_crm_company}: #{link_to(contact.contact_company.name, { :controller => 'contacts', :action => 'show', :id => contact.contact_company.id })}
        ".html_safe if !contact.contact_company.blank? && !contact.is_company + s << "#{@cached_label_email}: #{emails}
        ".html_safe if contact.emails.any? + s << "#{@cached_label_phone}: #{phones}
        ".html_safe if contact.phones.any? + s + end + + def link_to_contact(contact, options = {}) + s = '' + html_options = {} + html_options = { :class => 'icon icon-vcard' } if options[:icon] == true + s << avatar_to(contact, :size => '16') if options[:avatar] == true + s << link_to_source(contact, html_options) + + s << "(#{contact.job_title}) " if (options[:job_title] == true) && !contact.job_title.blank? + s << " #{l(:label_crm_at_company)} " if (options[:job_title] == true) && !(contact.job_title.blank? || contact.company.blank?) + if (options[:company] == true) && contact.contact_company + s << link_to(contact.contact_company.name, { :controller => 'contacts', :action => 'show', :id => contact.contact_company.id }) + else + h contact.company + end + s << "(#{l(:field_contact_tag_names)}: #{contact.tag_list.join(', ')}) " if (options[:tag_list] == true) && !contact.tag_list.blank? + s.html_safe + end + + def tagsedit_with_source_for(field_id, url) + s = '' + unless @heads_for_tagsedit_included + s << javascript_include_tag(:"tag-it", :plugin => 'redmine_contacts') + s << stylesheet_link_tag(:"jquery.tagit.css", :plugin => 'redmine_contacts') + @heads_for_tagsedit_included = true + end + s << javascript_tag("$('#{field_id}').tagit({ + tagSource: function(search, showChoices) { + var that = this; + $.ajax({ + url: '#{url}', + data: {q: search.term}, + success: function(choices) { + showChoices(that._subtractArray(jQuery.parseJSON(choices), that.assignedTags())); + } + }); + }, + allowSpaces: true, + placeholderText: '#{l(:label_crm_add_tag)}', + caseSensitive: false, + removeConfirmation: true + });") + s.html_safe + end + + def tagsedit_for(field_id, available_tags='') + s = '' + unless @heads_for_tagsedit_included + s << javascript_include_tag(:"tag-it", :plugin => 'redmine_contacts') + s << stylesheet_link_tag(:"jquery.tagit.css", :plugin => 'redmine_contacts') + @heads_for_tagsedit_included = true + end + + s << javascript_tag("$('#{field_id}').tagit({ + availableTags: ['#{available_tags}'], + allowSpaces: true, + placeholderText: '#{l(:label_crm_add_tag)}', + caseSensitive: false, + removeConfirmation: true + });") + s.html_safe + end + + def note_type_icon(note) + note_type_tag = '' + case note.type_id + when 0 + note_type_tag = content_tag('span', '', :class => 'icon icon-email', :title => l(:label_crm_note_type_email)) + when 1 + note_type_tag = content_tag('span', '', :class => 'icon icon-call', :title => l(:label_crm_note_type_call)) + when 2 + note_type_tag = content_tag('span', '', :class => 'icon icon-meeting', :title => l(:label_crm_note_type_meeting)) + end + context = { :type_tag => note_type_tag, :type_id => note.type_id } + call_hook(:helper_notes_note_type_tag, context) + context[:type_tag].html_safe + end + def deal_tag(deal, options = {}) + return deal.name unless deal.visible? + deal_name = options[:no_contact] ? deal.name : deal.full_name + s = '' + s << avatar_to(deal, :size => options.delete(:size) || 16) unless options[:plain] + s << ' ' + link_to(deal_name, deal_path(deal)) + s << " (#{deal.price_to_s}) " unless deal.price.blank? || options[:no_price] + s << (options[:plain] ? deal.status.name : deal_status_tag(deal.status)) if deal.status + s.html_safe + end + + def deal_status_tag(deal_status) + status_tag = content_tag(:span, deal_status.name) + content_tag(:span, status_tag, :class => 'tag-label-color', :style => "background-color:#{deal_status.color_name};color:white;") + end + + def deals_for_select(project, options = {}) + scope = Deal.visible.where(options[:where]).limit(options[:limit] || 500) + scope = scope.by_project(project) if project + scope.order("#{Deal.table_name}.name") + .collect { |m| [m.name + (options[:short_label] || m.info.blank? ? '' : " - #{m.info}"), m.id.to_s] } + end + end +end + +ActionView::Base.send :include, RedmineContacts::Helper diff --git a/plugins/redmine_contacts/lib/redmine_contacts/helpers/crm_calendar_helper.rb b/plugins/redmine_contacts/lib/redmine_contacts/helpers/crm_calendar_helper.rb new file mode 100644 index 0000000..087938f --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/helpers/crm_calendar_helper.rb @@ -0,0 +1,91 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Helpers + + class CrmCalendar + include Redmine::I18n + attr_reader :startdt, :enddt + + def initialize(date, options = {}) + @date = date + @events = [] + @ending_events_by_days = {} + @starting_events_by_days = {} + @start_date_field = options[:start_date_field] || "start_date" + @due_date_field = options[:due_date_field] || "due_date" + set_language_if_valid options[:language] || current_language + period = options[:period] || :month + case period + when :month + @startdt = Date.civil(date.year, date.month, 1) + @enddt = (@startdt >> 1)-1 + # starts from the first day of the week + @startdt = @startdt - (@startdt.cwday - first_wday)%7 + # ends on the last day of the week + @enddt = @enddt + (last_wday - @enddt.cwday)%7 + when :week + @startdt = date - (date.cwday - first_wday)%7 + @enddt = date + (last_wday - date.cwday)%7 + else + raise 'Invalid period' + end + end + + # Sets calendar events + def events=(events) + @events = events + @ending_events_by_days = @events.group_by {|event| event.send(@start_date_field).to_date if event.send(@start_date_field)} + @starting_events_by_days = @events.group_by {|event| event.send(@due_date_field).to_date if event.send(@due_date_field)} + end + + # Returns events for the given day + def events_on(day) + ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq + end + + # Calendar current month + def month + @date.month + end + + # Return the first day of week + # 1 = Monday ... 7 = Sunday + def first_wday + case Setting.start_of_week.to_i + when 1 + @first_dow ||= (1 - 1)%7 + 1 + when 6 + @first_dow ||= (6 - 1)%7 + 1 + when 7 + @first_dow ||= (7 - 1)%7 + 1 + else + @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1 + end + end + + def last_wday + @last_dow ||= (first_wday + 5)%7 + 1 + end + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/helpers/money_helper.rb b/plugins/redmine_contacts/lib/redmine_contacts/helpers/money_helper.rb new file mode 100644 index 0000000..7bae3e3 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/helpers/money_helper.rb @@ -0,0 +1,91 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module MoneyHelper + + def object_price(obj, price_field = :price) + price_to_currency(obj.try(price_field), obj.currency, :symbol => true).to_s if obj.respond_to?(:currency) + end + + def prices_collection_by_currency(prices_collection, options={}) + return [] if prices_collection.blank? || prices_collection == 0 + prices = prices_collection + prices.reject!{|k, v| v.to_i == 0} if options[:hide_zeros] + prices.collect{|k, v| content_tag(:span, price_to_currency(v, k, :symbol => true), :style => "white-space: nowrap;")}.compact + end + + def deal_currency_icon(currency) + case currency.to_s.upcase + when 'EUR' + "icon-money-euro" + when 'GBP' + "icon-money-pound" + when 'JPY' + "icon-money-yen" + else + "icon-money-dollar" + end + end + + def collection_for_currencies_select(default_currency = ContactsSetting.default_currency) + major_currencies_collection(default_currency) + end + + def major_currencies_collection(default_currency) + currencies = [] + currencies << default_currency.to_s unless default_currency.blank? + currencies |= ContactsSetting.major_currencies + currencies.map do |c| + currency = RedmineCrm::Currency.find(c) + ["#{currency.iso_code} (#{currency.symbol})", currency.iso_code] if currency + end.compact.uniq + end + + def all_currencies + RedmineCrm::Currency.table.inject([]) do |array, (id, attributes)| + array ||= [] + array << ["#{attributes[:name]}" + (attributes[:symbol].blank? ? "" : " (#{attributes[:symbol]})"), attributes[:iso_code]] + array + end.sort{|x, y| x[0] <=> y[0]} + end + + def price_to_currency(price, currency, options={}) + return '' if price.blank? + options[:decimal_mark] = ContactsSetting.decimal_separator unless options[:decimal_mark] + options[:thousands_separator] = ContactsSetting.thousands_delimiter unless options[:thousands_separator] + # RedmineCrm::Currency.from_float(price.to_f, currency).format(options) rescue ActionController::Base.helpers.number_with_delimiter(price.to_f, :separator => ContactsSetting.decimal_separator, :delimiter => ContactsSetting.thousands_delimiter, :precision => 2) + if currency + if currency.is_a? String + currency = RedmineCrm::Currency.find(currency) + end + else + currency = RedmineCrm::Currency.find("USD") + end + ActionController::Base.helpers.number_to_currency(price.to_f, :unit => currency.code) + end + + end +end + +unless ActionView::Base.included_modules.include?(RedmineContacts::MoneyHelper) + ActionView::Base.send(:include, RedmineContacts::MoneyHelper) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/controllers_time_entry_reports_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/controllers_time_entry_reports_hook.rb new file mode 100644 index 0000000..582af37 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/controllers_time_entry_reports_hook.rb @@ -0,0 +1,34 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Hooks + class ControllersTimeEntryReportsHook < Redmine::Hook::ViewListener + def controller_timelog_available_criterias(context = {}) + context[:available_criterias]['contacts'] = { :sql => 'contacts_issues.contact_id', + :klass => Contact, + :label => :label_crm_contact } + end + + def controller_timelog_time_report_joins(context = {}) + context[:sql] << " LEFT JOIN contacts_issues ON contacts_issues.issue_id = #{Issue.table_name}.id" + end + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_custom_fields_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_custom_fields_hook.rb new file mode 100644 index 0000000..cbc2c17 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_custom_fields_hook.rb @@ -0,0 +1,27 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Hooks + class ViewsCustomFieldsHook < Redmine::Hook::ViewListener + render_on :view_custom_fields_form_deal_custom_field, :partial => "deals/custom_field_form" + render_on :view_custom_fields_form_contact_custom_field, :partial => "contacts/custom_field_form" + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_issues_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_issues_hook.rb new file mode 100644 index 0000000..3fa3d2b --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_issues_hook.rb @@ -0,0 +1,28 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Hooks + class ViewsIssuesHook < Redmine::Hook::ViewListener + render_on :view_issues_sidebar_planning_bottom, :partial => "contacts_issues/contacts", :locals => {:contact_issue => @issue} + render_on :view_issues_form_details_bottom, :partial => 'deals_issues/form' + render_on :view_issues_show_details_bottom, :partial => 'deals_issues/show' + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_layouts_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_layouts_hook.rb new file mode 100644 index 0000000..a040b9a --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_layouts_hook.rb @@ -0,0 +1,27 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Hooks + class ViewsLayoutsHook < Redmine::Hook::ViewListener + render_on :view_layouts_base_body_bottom, :partial => 'common/contacts_select2_data' + render_on :view_layouts_base_html_head, :partial => 'contacts_issues/additional_assets' + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_projects_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_projects_hook.rb new file mode 100644 index 0000000..014e19d --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_projects_hook.rb @@ -0,0 +1,26 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Hooks + class ViewsUsersHook < Redmine::Hook::ViewListener + render_on :view_projects_show_left, :partial => "projects/contacts" + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_users_hook.rb b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_users_hook.rb new file mode 100644 index 0000000..c851d46 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/hooks/views_users_hook.rb @@ -0,0 +1,28 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# include ContactsHelper + +module RedmineContacts + module Hooks + class ViewsUsersHook < Redmine::Hook::ViewListener + render_on :view_account_left_bottom, :partial => "users/contact", :locals => {:user => @user} + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/addresses_drop.rb b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/addresses_drop.rb new file mode 100644 index 0000000..0d0e10d --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/addresses_drop.rb @@ -0,0 +1,64 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class AddressesDrop < Liquid::Drop + + def initialize(addresses) + @addresses = addresses + end + + def before_method(id) + address = @addresses.where(:id => id).first || Address.new + AddressDrop.new address + end + + def all + @all ||= @addresses.map do |address| + AddressDrop.new address + end + end + + def visible + @visible ||= @addresses.visible.map do |address| + AddressDrop.new address + end + end + + def each(&block) + all.each(&block) + end + +end + + +class AddressDrop < Liquid::Drop + + delegate :id, :address_type, :street1, :street2, :city, :region, :postcode, :country_code, :country, :full_address, :post_address, :to => :@address + + def initialize(address) + @address = address + end + + private + + def helpers + Rails.application.routes.url_helpers + end + +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/contacts_drop.rb b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/contacts_drop.rb new file mode 100644 index 0000000..c89f74f --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/contacts_drop.rb @@ -0,0 +1,91 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class ContactsDrop < Liquid::Drop + + def initialize(contacts) + @contacts = contacts + end + + def before_method(id) + contact = @contacts.where(:id => id).first || Contact.new + ContactDrop.new contact + end + + def all + @all ||= @contacts.map do |contact| + ContactDrop.new contact + end + end + + def visible + @visible ||= @contacts.visible.map do |contact| + ContactDrop.new contact + end + end + + def each(&block) + all.each(&block) + end + +end + + +class ContactDrop < Liquid::Drop + + delegate :id, :name, :first_name, :last_name, :middle_name, :company, :phones, :emails, :primary_email, :website, :skype_name, :birthday, :age, :background, :job_title, :is_company, :tag_list, :post_address, :to => :@contact + + def initialize(contact) + @contact = contact + end + + def contact_company + ContactDrop.new @contact.contact_company if @contact.contact_company + end + + def company_contacts + @contact.company_contacts.map{|c| ContactDrop.new c } if @contact.company_contacts + end + + def avatar_diskfile + @contact.avatar.diskfile + end + + def avatar_url + helpers.url_for :controller => "attachments", :action => "contacts_thumbnail", :id => @contact.avatar, :size => '64', :only_path => true + end + + def notes + @contact.notes.map{|n| NoteDrop.new(n) } + end + + def address + AddressDrop.new(@contact.address) if @contact.address + end + def custom_field_values + @contact.custom_field_values + end + + private + + def helpers + Rails.application.routes.url_helpers + end + +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/deals_drop.rb b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/deals_drop.rb new file mode 100644 index 0000000..7e5be1b --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/deals_drop.rb @@ -0,0 +1,77 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class DealsDrop < Liquid::Drop + + def initialize(deals) + @deals = deals + end + + def before_method(id) + deal = @deals.where(:id => id).first || Deal.new + DealDrop.new deal + end + + def all + @all ||= @deals.map do |deal| + DealDrop.new deal + end + end + + def visible + @visible ||= @deals.visible.map do |deal| + DealDrop.new deal + end + end + + def each(&block) + all.each(&block) + end + +end + + +class DealDrop < Liquid::Drop + + delegate :id, :name, :created_on, :due_date, :price, :price_type, :currency, :background, :probability, :to => :@deal + + def initialize(deal) + @deal = deal + end + + def notes + @deal.notes.map{|n| NoteDrop.new(n) } + end + + def category + @deal.category.name if @deal.category + end + + def contact + ContactDrop.new @deal.contact if @deal.contact + end + + def status + @deal.status.name if @deal.status + end + + def custom_field_values + @deal.custom_field_values + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/notes_drop.rb b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/notes_drop.rb new file mode 100644 index 0000000..41f4636 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/liquid/drops/notes_drop.rb @@ -0,0 +1,61 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +class NotesDrop < Liquid::Drop + + def initialize(notes) + @notes = notes + end + + def before_method(id) + note = @notes.where(:id => id).first || Note.new + NoteDrop.new note + end + + def all + @all ||= @notes.map do |note| + NoteDrop.new note + end + end + + def visible + @visible ||= @notes.visible.map do |note| + NoteDrop.new note + end + end + + def each(&block) + all.each(&block) + end + +end + + +class NoteDrop < Liquid::Drop + + delegate :id, :subject, :content, :type_id, :to => :@note + + def initialize(note) + @note = note + end + def custom_field_values + @note.custom_field_values + end + +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/liquid/liquid.rb b/plugins/redmine_contacts/lib/redmine_contacts/liquid/liquid.rb new file mode 100644 index 0000000..9e06c5e --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/liquid/liquid.rb @@ -0,0 +1,97 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require "redmine_contacts/liquid/drops/contacts_drop" +require "redmine_contacts/liquid/drops/deals_drop" +require "redmine_contacts/liquid/drops/notes_drop" +require "redmine_contacts/liquid/drops/addresses_drop" + +module RedmineContacts + module Liquid + module Filters + include RedmineCrm::MoneyHelper + + def underscore(input) + input.to_s.gsub(' ', '_').gsub('/', '_').underscore + end + + def dasherize(input) + input.to_s.gsub(' ', '-').gsub('/', '-').dasherize + end + + def encode(input) + Rack::Utils.escape(input) + end + + # alias newline_to_br + def multi_line(input) + input.to_s.gsub("\n", '
        ').html_safe + end + + def concat(input, *args) + result = input.to_s + args.flatten.each { |a| result << a.to_s } + result + end + + # right justify and padd a string + def rjust(input, integer, padstr = '') + input.to_s.rjust(integer, padstr) + end + + # left justify and padd a string + def ljust(input, integer, padstr = '') + input.to_s.ljust(integer, padstr) + end + + def textile(input) + ::RedCloth3.new(input).to_html + end + + def currency(input, currency_code=nil) + price_to_currency(input, currency_code || container_currency, :converted => false) + end + + def custom_field(input, field_name) + if input.respond_to?(:custom_field_values) + input.custom_field_values.detect{|cfv| cfv.custom_field.name == field_name}.try(:value) + end + end + + def attachment(input, file_name) + if input.respond_to?(:attachments) + input.attachments.detect{|a| a.file_name == file_name}.try(:diskfile) + end + end + + private + def container + @container ||= @context.registers[:container] + end + + def container_currency + container.currency if container.respond_to?(:currency) + end + + end + + ::Liquid::Template.register_filter(RedmineContacts::Liquid::Filters) + + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/action_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/action_controller_patch.rb new file mode 100644 index 0000000..f4f7c65 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/action_controller_patch.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ActionControllerPatch + def self.included(base) + base.extend(ClassMethods) if Rails::VERSION::MAJOR < 4 + + base.class_eval do + end + end + + module ClassMethods + if Rails::VERSION::MAJOR < 4 + def before_action(*filters, &block) + before_filter(*filters, &block) + end + + def after_action(*filters, &block) + after_filter(*filters, &block) + end + + def skip_before_action(*filters) + skip_before_filter(*filters) + end + end + end + end + end +end + +unless ActionController::Base.included_modules.include?(RedmineContacts::Patches::ActionControllerPatch) + ActionController::Base.send(:include, RedmineContacts::Patches::ActionControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/application_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/application_controller_patch.rb new file mode 100644 index 0000000..13fc798 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/application_controller_patch.rb @@ -0,0 +1,44 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ApplicationControllerPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + alias_method :user_setup_without_contacts, :user_setup + alias_method :user_setup, :user_setup_with_contacts + end + end + + module InstanceMethods + def user_setup_with_contacts + user_setup_without_contacts + ContactsSetting.check_cache + end + end + end + end +end + +unless ApplicationController.included_modules.include?(RedmineContacts::Patches::ApplicationControllerPatch) + ApplicationController.send(:include, RedmineContacts::Patches::ApplicationControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/attachments_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/attachments_controller_patch.rb new file mode 100644 index 0000000..1cd548f --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/attachments_controller_patch.rb @@ -0,0 +1,104 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'attachments_controller' +require_dependency 'attachment' + +module RedmineContacts + module Patches + + module AttachmentsControllerPatch + def self.included(base) + base.send(:include, InstanceMethods) + end + + module InstanceMethods + + def contacts_thumbnail + find_attachment + size = params[:size].to_i + size = 64 unless size > 0 + if @attachment.readable? && @attachment.thumbnailable? + if Redmine::Thumbnail.convert_available? + target = File.join(@attachment.class.thumbnails_storage_path, "#{@attachment.id}_#{@attachment.digest}_#{size}.thumb") + thumbnail = RedmineContacts::Thumbnail.generate(@attachment.diskfile, target, size) + else + thumbnail = @attachment.diskfile + end + if stale?(:etag => @attachment.digest) + send_file thumbnail, :filename => (request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(@attachment.filename) : @attachment.filename), + :type => detect_content_type(@attachment), + :disposition => 'inline' + end + else + # No thumbnail for the attachment or thumbnail could not be created + render :nothing => true, :status => 404 + end + rescue => e + logger.error "An error occured while generating contact thumbnail for #{@attachment.disk_filename} to #{target}\nException was: #{e.message}" if logger + return nil + end + + private + + def find_attachment + @attachment = Attachment.find(params[:id]) + # Show 404 if the filename in the url is wrong + raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename + @project = @attachment.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + end + + end + + module AttachmentPatch + + module InstanceMethods + + def is_contacts_thumbnailable? + (self.is_pdf? && self.filesize < 600.kilobytes) || self.image? + end + + def is_pdf? + self.filename =~ /\.(pdf)$/i + end + + end + + def self.included(base) # :nodoc: + base.send :include, InstanceMethods + end + + end + + + end +end + + +unless Attachment.included_modules.include?(RedmineContacts::Patches::AttachmentPatch) + Attachment.send(:include, RedmineContacts::Patches::AttachmentPatch) +end + +unless AttachmentsController.included_modules.include?(RedmineContacts::Patches::AttachmentsControllerPatch) + AttachmentsController.send(:include, RedmineContacts::Patches::AttachmentsControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/auto_completes_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/auto_completes_controller_patch.rb new file mode 100644 index 0000000..2c6ed65 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/auto_completes_controller_patch.rb @@ -0,0 +1,95 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'auto_completes_controller' + +module RedmineContacts + module Patches + module AutoCompletesControllerPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + end + end + + module InstanceMethods + def deals + @deals = [] + q = (params[:q] || params[:term]).to_s.strip + scope = Deal.joins(:project).where({}) + scope = scope.limit(params[:limit] || 10) + scope = scope.by_project(@project) if @project + if q.match(/\A#?(\d+)\z/) + @deals << scope.visible.find_by_id($1.to_i) + end + scope = scope.live_search_with_contact(q) unless q.blank? + @deals += scope.visible.order("#{Deal.table_name}.name") + @deals.uniq! { |deal| deal.id } + render :layout => false, :partial => 'deals' + end + + def contact_tags + @name = params[:q].to_s + @tags = Contact.available_tags :name_like => @name, :limit => 10 + render :layout => false, :partial => 'crm_tag_list' + end + + def taggable_tags + klass = Object.const_get(params[:taggable_type].camelcase) + @name = params[:q].to_s + @tags = klass.all_tag_counts(:conditions => ["#{RedmineCrm::Tag.table_name}.name LIKE ?", "%#{@name}%"], :limit => 10) + render :layout => false, :partial => 'crm_tag_list' + end + + def contacts + @contacts = [] + q = (params[:q] || params[:term]).to_s.strip + scope = Contact.includes(:avatar).where({}) + scope = scope.limit(params[:limit] || 10) + scope = scope.companies if params[:is_company] + scope = scope.joins(:projects).where(Contact.visible_condition(User.current)) + scope = Rails.version >= '5.1' ? scope.distinct : scope.uniq + q.split(' ').collect { |search_string| scope = scope.live_search(search_string) } unless q.blank? + scope = scope.by_project(@project) if @project + @contacts = scope.to_a.sort! { |x, y| x.name <=> y.name } + render :layout => false, :partial => 'contacts' + end + + def companies + @companies = [] + q = (params[:q] || params[:term]).to_s.strip + if q.present? + scope = Contact.joins(:projects).where({}) + scope = scope.limit(params[:limit] || 10) + scope = scope.includes(:avatar) + scope = scope.by_project(@project) if @project + scope = scope.where('LOWER(first_name) LIKE LOWER(?)', "#{q}%") unless q.blank? + @companies = scope.visible.companies.order("#{Contact.table_name}.first_name") + end + render :layout => false, :partial => 'companies' + end + end + end + end +end + +unless AutoCompletesController.included_modules.include?(RedmineContacts::Patches::AutoCompletesControllerPatch) + AutoCompletesController.send(:include, RedmineContacts::Patches::AutoCompletesControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/2.3/query_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/2.3/query_patch.rb new file mode 100644 index 0000000..27e03e8 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/2.3/query_patch.rb @@ -0,0 +1,62 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module QueryPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + base.class_eval do + unloadable + class << self + VISIBILITY_PRIVATE = 0 + VISIBILITY_ROLES = 1 + VISIBILITY_PUBLIC = 2 + end + end + end + end + + module InstanceMethods + VISIBILITY_PRIVATE = 0 + VISIBILITY_ROLES = 1 + VISIBILITY_PUBLIC = 2 + + def is_private? + visibility == VISIBILITY_PRIVATE + end + + def is_public? + !is_private? + end + + def visibility=(value) + self.is_public = value == VISIBILITY_PUBLIC + end + + def visibility + self.is_public ? VISIBILITY_PUBLIC : VISIBILITY_PRIVATE + end + end + end +end + +unless Query.included_modules.include?(RedmineContacts::Patches::QueryPatch) + Query.send(:include, RedmineContacts::Patches::QueryPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_base_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_base_patch.rb new file mode 100644 index 0000000..0172af6 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_base_patch.rb @@ -0,0 +1,84 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ActiveRecordBasePatch + def self.included(base) + base.send(:include, InstanceMethods) + base.class_eval do + alias_method :has_many_without_contacts, :has_many + alias_method :has_many, :has_many_with_contacts + end + end + + module InstanceMethods + def has_many_with_contacts(name, param2 = nil, *param3, &extension) + return has_many_without_contacts(name, param2, *param3, &extension) if param3 && param3.is_a?(Array) && param3[0] && param3[0][:through] + if param2.nil? + options = {} + else + if param2.is_a?(Proc) + scope = param2 + options = param3.empty? ? {} : param3[0] + else + options = param2 + end + end + if ActiveRecord::VERSION::MAJOR >= 4 + scope, options = build_scope_and_options(options) if scope.nil? + has_many_without_contacts(name, scope, options, &extension) + else + has_many_without_contacts(name, options, &extension) + end + end + + def build_scope_and_options(options) + scope_opts, opts = parse_options(options) + + unless scope_opts.empty? + scope = lambda do + scope_opts.inject(self) { |result, hash| result.send *hash } + end + end + [defined?(scope) ? scope : nil, opts] + end + + def parse_options(opts) + scope_opts = {} + [:order, :having, :select, :group, :limit, :offset, :readonly].each do |o| + scope_opts[o] = opts.delete o if opts[o] + end + scope_opts[:where] = opts.delete :conditions if opts[:conditions] + scope_opts[:joins] = opts.delete :include if opts [:include] + scope_opts[:distinct] = opts.delete :uniq if opts[:uniq] + + [scope_opts, opts] + end + end + end + end +end + +if defined?(ActiveRecord::Base) + ActiveRecord::Base.extend RedmineContacts::Patches::ActiveRecordBasePatch::InstanceMethods + unless ActiveRecord::Associations::ClassMethods.included_modules.include?(RedmineContacts::Patches::ActiveRecordBasePatch) + ActiveRecord::Associations::ClassMethods.send(:include, RedmineContacts::Patches::ActiveRecordBasePatch) + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_sanitization_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_sanitization_patch.rb new file mode 100644 index 0000000..fdeef33 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/active_record_sanitization_patch.rb @@ -0,0 +1,36 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ActiveRecordSanitizationPatch + def self.included(base) + base.class_eval do + def quote_value(value, column = nil) + connection.quote(value, column) + end + end + end + end + end +end + +unless ActiveRecord::Sanitization::ClassMethods.included_modules.include?(RedmineContacts::Patches::ActiveRecordSanitizationPatch) + ActiveRecord::Sanitization::ClassMethods.send(:include, RedmineContacts::Patches::ActiveRecordSanitizationPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/application_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/application_helper_patch.rb new file mode 100644 index 0000000..2a1980e --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/application_helper_patch.rb @@ -0,0 +1,38 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ApplicationHelperPatch + def self.included(base) # :nodoc: + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + + def stocked_reorder_link(object, name = nil, url = {}, method = :post) + Redmine::VERSION.to_s > '3.3' ? reorder_handle(object, :param => name) : reorder_links(name, url, method) + end + end + end + end + end +end + +unless ApplicationHelper.included_modules.include?(RedmineContacts::Patches::ApplicationHelperPatch) + ApplicationHelper.send(:include, RedmineContacts::Patches::ApplicationHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/user_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/user_patch.rb new file mode 100644 index 0000000..d4ba243 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility/user_patch.rb @@ -0,0 +1,50 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module UserPatch + def self.included(base) + base.class_eval do + scope :having_mail, lambda {|arg| + addresses = Array.wrap(arg).map {|a| a.to_s.downcase} + if addresses.any? + joins(:email_addresses).where("LOWER(#{EmailAddress.table_name}.address) IN (?)", addresses).uniq + else + none + end + } + + def self.find_by_mail(mail) + if ActiveRecord::VERSION::MAJOR >= 4 + mail.is_a?(Array) ? mail : [mail] + having_mail(mail).first + else + where("LOWER(mail) = ?", mail.to_s.downcase).first + end + end + end + end + end + end +end + +unless User.included_modules.include?(RedmineContacts::Patches::UserPatch) + User.send(:include, RedmineContacts::Patches::UserPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility_patch.rb new file mode 100644 index 0000000..baf4a78 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/compatibility_patch.rb @@ -0,0 +1,26 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +if Redmine::VERSION.to_s < '2.4' + Dir[File.dirname(__FILE__) + '/compatibility/2.3/*.rb'].each { |f| require f } +end + +if ActiveRecord::VERSION::MAJOR > 3 + Dir[File.dirname(__FILE__) + '/compatibility/rails/*.rb'].each { |f| require f } +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/custom_fields_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/custom_fields_helper_patch.rb new file mode 100644 index 0000000..0fe6d9a --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/custom_fields_helper_patch.rb @@ -0,0 +1,59 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'custom_fields_helper' + +module RedmineContacts + module Patches + module CustomFieldsHelperPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + if Rails.version < '5.1' + alias_method_chain :custom_fields_tabs, :contacts_tab + else + alias_method :custom_fields_tabs_without_contacts, :custom_fields_tabs + alias_method :custom_fields_tabs, :custom_fields_tabs_with_contacts + end + end + end + + module InstanceMethods + def custom_fields_tabs_with_contacts_tab + new_tabs = [] + new_tabs << { :name => 'ContactCustomField', :partial => 'custom_fields/index', :label => :label_contact_plural } + new_tabs << { :name => 'DealCustomField', :partial => 'custom_fields/index', :label => :label_deal_plural } + new_tabs << { :name => 'NoteCustomField', :partial => 'custom_fields/index', :label => :label_crm_note_plural } + custom_fields_tabs_without_contacts_tab | new_tabs + end + end + end + end +end + +if Redmine::VERSION.to_s > '2.5' + CustomFieldsHelper::CUSTOM_FIELDS_TABS << { :name => 'ContactCustomField', :partial => 'custom_fields/index', :label => :label_contact_plural } + CustomFieldsHelper::CUSTOM_FIELDS_TABS << { :name => 'DealCustomField', :partial => 'custom_fields/index', :label => :label_deal_plural } + CustomFieldsHelper::CUSTOM_FIELDS_TABS << { :name => 'NoteCustomField', :partial => 'custom_fields/index', :label => :label_crm_note_plural } +else + unless CustomFieldsHelper.included_modules.include?(RedmineContacts::Patches::CustomFieldsHelperPatch) + CustomFieldsHelper.send(:include, RedmineContacts::Patches::CustomFieldsHelperPatch) + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_patch.rb new file mode 100644 index 0000000..8fc7a7f --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_patch.rb @@ -0,0 +1,54 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'issue' +require_dependency 'contact' + +module RedmineContacts + module Patches + module IssuePatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + has_and_belongs_to_many :contacts, :uniq => true + has_one :deals_issue + has_one :deal, :through => :deals_issue + accepts_nested_attributes_for :deals_issue, :reject_if => :reject_deal, :allow_destroy => true + + safe_attributes 'deals_issue_attributes', + :if => lambda { |issue, user| user.allowed_to?(:edit_deals, issue.project) } + end + end + + module InstanceMethods + def reject_deal(attributes) + exists = attributes['id'].present? + empty = attributes[:deal_id].blank? + attributes[:_destroy] = 1 if exists && empty + !exists && empty + end + end + end + end +end + +unless Issue.included_modules.include?(RedmineContacts::Patches::IssuePatch) + Issue.send(:include, RedmineContacts::Patches::IssuePatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_query_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_query_patch.rb new file mode 100644 index 0000000..c603898 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/issue_query_patch.rb @@ -0,0 +1,138 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'query' + +module RedmineContacts + module Patches + module IssueQueryPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.send(:include, RedmineContacts::Helper) + base.class_eval do + unloadable + + alias_method :available_columns_without_contacts, :available_columns + alias_method :available_columns, :available_columns_with_contacts + + alias_method :initialize_available_filters_without_contacts, :initialize_available_filters + alias_method :initialize_available_filters, :initialize_available_filters_with_contacts + end + end + + module InstanceMethods + def sql_for_contacts_field(_field, operator, value) + case operator + when '=', '*' + compare = 'IN' + when '!', '!*' + compare = 'NOT IN' + end + contacts_select = "SELECT contacts_issues.issue_id FROM contacts_issues + WHERE contacts_issues.contact_id IN (#{value.join(',')})" + issues_with_contacts = 'SELECT DISTINCT(issue_id) FROM contacts_issues' + + "(#{Issue.table_name}.id #{compare} (#{ %w(= !).include?(operator) ? contacts_select : issues_with_contacts }))" + end + + def sql_for_companies_field(_field, operator, value) + compare = operator == '=' ? 'IN' : 'NOT IN' + employes_select = "SELECT contacts_issues.issue_id FROM contacts_issues + WHERE contacts_issues.contact_id IN + ( SELECT c_1.id from #{Contact.table_name} + LEFT OUTER JOIN #{Contact.table_name} AS c_1 ON c_1.company = #{Contact.table_name}.first_name + WHERE #{Contact.table_name}.id IN (#{value.join(',')}) + )" + companies_select = "SELECT contacts_issues.issue_id FROM contacts_issues + WHERE contacts_issues.contact_id IN (#{value.join(',')})" + + "((#{Issue.table_name}.id #{compare} (#{employes_select})) + OR (#{Issue.table_name}.id #{compare} (#{companies_select})))" + end + + def sql_for_deal_field(_field, operator, value) + if operator == '!*' + compare = 'NOT IN' + operator = '*' + else + compare = 'IN' + end + + deals_select = "SELECT deals_issues.issue_id FROM deals_issues + WHERE #{sql_for_field('deal_id', operator, value, 'deals_issues', 'deal_id')}" + + "(#{Issue.table_name}.id #{compare} (#{deals_select}))" + end + + def available_columns_with_contacts + if @available_columns.blank? + @available_columns = available_columns_without_contacts + @available_columns << QueryColumn.new(:deal, :caption => :label_deal) if User.current.allowed_to?(:view_deals, project, :global => true) + @available_columns << QueryColumn.new(:contacts) if User.current.allowed_to?(:view_contacts, project, :global => true) + else + available_columns_without_contacts + end + @available_columns + end + + def initialize_available_filters_with_contacts + initialize_available_filters_without_contacts + + if !available_filters.key?('contacts') && User.current.allowed_to?(:view_contacts, project, :global => true) + selected_contacts = filters['contacts'].blank? ? [] : Contact.visible.where(:id => filters['contacts'][:values]).map { |c| [c.name, c.id.to_s] } + add_available_filter('contacts', :type => :list_optional, :field_format => 'contact', :name => l(:field_contacts), :values => selected_contacts) + end + + if !available_filters.key?('companies') && User.current.allowed_to?(:view_contacts, project, :global => true) + selected_companies = filters['companies'].blank? ? [] : Contact.visible.where(:id => filters['companies'][:values]).map { |c| [c.name, c.id.to_s] } + add_available_filter('companies', :type => :list, :field_format => 'company', :name => l(:field_companies), :values => selected_companies) + end + + if !available_filters.key?('deal') && User.current.allowed_to?(:view_deals, project, :global => true) + selected_deals = filters['deal'].blank? ? [] : Deal.visible.where(:id => filters['deal'][:values]).map { |deal| [deal.name, deal.id.to_s] } + add_available_filter('deal', :type => :list_optional, :field_format => 'deal', :name => l(:label_deal), :values => selected_deals) + end + end + + def contact_query_values(options = {}) + if options[:field] == 'deal' + deals_for_select(nil, :where => { :id => options[:values] }, :short_label => true) + else + contacts_for_select(nil, :where => { :id => options[:values] }, :short_label => true) + end + end + + def default_contact_query_values(options = {}) + records = + if options[:field] == 'deal' + Deal.visible.where(:id => options[:values]) + else + Contact.joins(:projects).where(Contact.visible_condition(User.current)).where(:id => options[:values]) + end + + records.to_a.sort! { |x, y| x.name <=> y.name }.collect { |m| [m.name, m.id.to_s] } + end + end + end + end +end + +unless IssueQuery.included_modules.include?(RedmineContacts::Patches::IssueQueryPatch) + IssueQuery.send(:include, RedmineContacts::Patches::IssueQueryPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_controller_patch.rb new file mode 100644 index 0000000..3c66abd --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_controller_patch.rb @@ -0,0 +1,47 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module IssuesControllerPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method :build_new_issue_from_params_without_contacts, :build_new_issue_from_params + alias_method :build_new_issue_from_params, :build_new_issue_from_params_with_contacts + end + end + + module InstanceMethods + def build_new_issue_from_params_with_contacts + build_new_issue_from_params_without_contacts + return if @issue.blank? || params[:deal_id].blank? + deal = Deal.visible.where(:id => params[:deal_id]).first + @issue.deals_issue = DealsIssue.new(:issue => @issue, :deal => deal) if deal + end + end + end + end +end + +unless IssuesController.included_modules.include?(RedmineContacts::Patches::IssuesControllerPatch) + IssuesController.send(:include, RedmineContacts::Patches::IssuesControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_helper_patch.rb new file mode 100644 index 0000000..698fbe1 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/issues_helper_patch.rb @@ -0,0 +1,41 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module IssuesHelperPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + base.class_eval do + unloadable + end + end + + module InstanceMethods + def render_custom_fields_rows(issue) + render_half_width_custom_fields_rows(issue) + end + end + end + end +end + +unless IssuesHelper.included_modules.include?(RedmineContacts::Patches::IssuesHelperPatch) + IssuesHelper.send(:include, RedmineContacts::Patches::IssuesHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/mailer_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/mailer_patch.rb new file mode 100644 index 0000000..ffdb6ea --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/mailer_patch.rb @@ -0,0 +1,115 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module MailerPatch + module ClassMethods + end + + module InstanceMethods + def crm_note_add(note) + redmine_headers 'Project' => note.source.project.identifier, + 'X-Notable-Id' => note.source.id, + 'X-Note-Id' => note.id + @author = note.author + message_id note + recipients = note.source.recipients + cc = (note.source.respond_to?(:all_watcher_recepients) ? note.source.all_watcher_recepients : note.source.watcher_recipients) - recipients + @note = note + @note_url = url_for(:controller => 'notes', :action => 'show', :id => note.id) + mail :to => recipients, + :cc => cc, + :subject => "[#{note.source.project.name}] - #{l(:label_crm_note_for)} #{note.source.name}" + end + + def crm_contact_add(contact) + redmine_headers 'Project' => contact.project.identifier, + 'X-Contact-Id' => contact.id + @author = contact.author + message_id contact + recipients = contact.recipients + cc = contact.watcher_recipients - recipients + @contact = contact + @contact_url = url_for(:controller => 'contacts', :action => 'show', :id => contact.id) + mail :to => recipients, + :cc => cc, + :subject => "[#{contact.project.name} - #{l(:label_contact)} ##{contact.id}] #{contact.name}" + end + def crm_deal_add(deal) + redmine_headers 'Project' => deal.project.identifier, + 'X-Deal-Id' => deal.id + @author = deal.author + message_id deal + recipients = deal.recipients + cc = deal.watcher_recipients - recipients + @deal = deal + @deal_url = url_for(:controller => 'deals', :action => 'show', :id => deal.id) + mail :to => recipients, + :cc => cc, + :subject => "[#{deal.project.name} - #{l(:label_deal)} ##{deal.id}] #{deal.full_name}" + end + + def crm_deal_updated(deal_process) + @deal = deal_process.deal + redmine_headers 'Project' => @deal.project.identifier, + 'X-Deal-Id' => @deal.id + @author = deal_process.author + recipients = deal_process.recipients + cc = @deal.watcher_recipients - recipients + @status_was = deal_process.from + @status = deal_process.to + @deal_url = url_for(:controller => 'deals', :action => 'show', :id => @deal.id) + mail :to => recipients, + :cc => cc, + :subject => "[#{@deal.project.name} - #{l(:label_deal)} ##{@deal.id}] #{@deal.full_name}" + end + + def crm_issue_connected(issue, contact) + redmine_headers 'X-Project' => contact.project.identifier, + 'X-Issue-Id' => issue.id, + 'X-Contact-Id' => contact.id + message_id contact + recipients contact.watcher_recipients + subject "[#{contact.projects.first.name}] - #{l(:label_issue_for)} #{contact.name}" + + body :contact => contact, + :issue => issue, + :contact_url => url_for(:controller => contact.class.name.pluralize.downcase, :action => 'show', :project_id => contact.project, :id => contact.id), + :issue_url => url_for(:controller => "issues", :action => "show", :id => issue) + render_multipart('issue_connected', body) + end + end + + def self.included(receiver) + receiver.extend ClassMethods + receiver.send :include, InstanceMethods + receiver.class_eval do + unloadable + # TODO: Удалено из-за неÑовмеÑтимоÑти, может быть коÑÑк Ñ ÑˆÐ°Ð±Ð»Ð¾Ð½Ð°Ð¼Ð¸ Ð´Ð»Ñ Ð¼Ð°Ð¹Ð»ÐµÑ€Ð° + # self.instance_variable_get("@inheritable_attributes")[:view_paths] << RAILS_ROOT + "/vendor/plugins/redmine_contacts/app/views" + end + end + end + end +end + +unless Mailer.included_modules.include?(RedmineContacts::Patches::MailerPatch) + Mailer.send(:include, RedmineContacts::Patches::MailerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/notifiable_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/notifiable_patch.rb new file mode 100644 index 0000000..10170d7 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/notifiable_patch.rb @@ -0,0 +1,52 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module NotifiablePatch + def self.included(base) + base.extend(ClassMethods) + base.class_eval do + unloadable + class << self + alias_method :all_without_contacts, :all + alias_method :all, :all_with_contacts + end + end + end + + module ClassMethods + # include ContactsHelper + + def all_with_contacts + notifications = all_without_contacts + notifications << Redmine::Notifiable.new('crm_contact_added') + notifications << Redmine::Notifiable.new('crm_deal_added') + notifications << Redmine::Notifiable.new('crm_deal_updated') + notifications << Redmine::Notifiable.new('crm_note_added') + notifications + end + end + end + end +end + +unless Redmine::Notifiable.included_modules.include?(RedmineContacts::Patches::NotifiablePatch) + Redmine::Notifiable.send(:include, RedmineContacts::Patches::NotifiablePatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/project_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/project_patch.rb new file mode 100644 index 0000000..c99be63 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/project_patch.rb @@ -0,0 +1,47 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module ProjectPatch + def self.included(base) # :nodoc: + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + + has_many :deals, :dependent => :delete_all + if ActiveRecord::VERSION::MAJOR >= 4 + has_and_belongs_to_many :contacts, lambda { order("#{Contact.table_name}.last_name, #{Contact.table_name}.first_name") } + + has_many :deal_categories, lambda { order("#{DealCategory.table_name}.name") }, :dependent => :delete_all + has_and_belongs_to_many :deal_statuses, lambda { order("#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position") } + else + has_and_belongs_to_many :contacts, :order => "#{Contact.table_name}.last_name, #{Contact.table_name}.first_name" + + has_many :deal_categories, :order => "#{DealCategory.table_name}.name", :dependent => :delete_all + has_and_belongs_to_many :deal_statuses, :order => "#{DealStatus.table_name}.status_type, #{DealStatus.table_name}.position", :uniq => true + end + end + end + end + end +end + +unless Project.included_modules.include?(RedmineContacts::Patches::ProjectPatch) + Project.send(:include, RedmineContacts::Patches::ProjectPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/projects_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/projects_helper_patch.rb new file mode 100644 index 0000000..23ed511 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/projects_helper_patch.rb @@ -0,0 +1,58 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'queries_helper' + +module RedmineContacts + module Patches + module ProjectsHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method :project_settings_tabs_without_contacts, :project_settings_tabs + alias_method :project_settings_tabs, :project_settings_tabs_with_contacts + end + end + + module InstanceMethods + # include ContactsHelper + + def project_settings_tabs_with_contacts + tabs = project_settings_tabs_without_contacts + + tabs.push(:name => 'contacts', + :action => :manage_contacts, + :partial => 'projects/contacts_settings', + :label => :label_contact_plural) if User.current.allowed_to?(:manage_contacts, @project) + tabs.push(:name => 'deals', + :action => :manage_deals, + :partial => 'projects/deals_settings', + :label => :label_deal_plural) if User.current.allowed_to?(:manage_deals, @project) + tabs + end + end + end + end +end + +unless ProjectsHelper.included_modules.include?(RedmineContacts::Patches::ProjectsHelperPatch) + ProjectsHelper.send(:include, RedmineContacts::Patches::ProjectsHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/queries_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/queries_helper_patch.rb new file mode 100644 index 0000000..3b0f4c6 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/queries_helper_patch.rb @@ -0,0 +1,76 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'queries_helper' + +module RedmineContacts + module Patches + module QueriesHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method :column_value_without_contacts, :column_value + alias_method :column_value, :column_value_with_contacts + end + end + + module InstanceMethods + def column_value_with_contacts(column, list_object, value) + if column.name == :id && list_object.is_a?(Contact) + link_to(value, contact_path(value)) + elsif column.name == :name && list_object.is_a?(Contact) + contact_tag(list_object) + elsif column.name == :name && list_object.is_a?(Deal) + link_to(list_object.name, deal_path(list_object)) + elsif column.name == :price && list_object.is_a?(Deal) + list_object.price_to_s + elsif column.name == :expected_revenue && list_object.is_a?(Deal) + list_object.expected_revenue_to_s + elsif column.name == :probability && !value.blank? && list_object.is_a?(Deal) + "#{value.to_i}%" + elsif value.is_a?(Deal) + deal_tag(value, :no_contact => true, :plain => true) + elsif value.is_a?(Contact) + contact_tag(value) + elsif column.name == :contacts + contacts_span = [] + [value].flatten.each do |contact| + contacts_span << contact_tag(contact) + end + contacts_span.join(', ').html_safe + elsif column.name == :tags && list_object.is_a?(Contact) + contact_tags = [] + [value].flatten.each do |tag| + contact_tags << tag.name + end + contact_tags.join(', ') + else + column_value_without_contacts(column, list_object, value) + end + end + end + end + end +end + +unless QueriesHelper.included_modules.include?(RedmineContacts::Patches::QueriesHelperPatch) + QueriesHelper.send(:include, RedmineContacts::Patches::QueriesHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/query_filter_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/query_filter_patch.rb new file mode 100644 index 0000000..c0e1832 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/query_filter_patch.rb @@ -0,0 +1,44 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'query' + +module RedmineContacts + module Patches + module QueryFilterPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.class_eval do + unloadable + end + end + + module InstanceMethods + def []=(key, value) + return unless key == :values + @value = @options[:values] = value + end + end + end + end +end + +unless QueryFilter.included_modules.include?(RedmineContacts::Patches::QueryFilterPatch) + QueryFilter.send(:include, RedmineContacts::Patches::QueryFilterPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/query_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/query_patch.rb new file mode 100644 index 0000000..b8adf0d --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/query_patch.rb @@ -0,0 +1,68 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'query' + +module RedmineContacts + module Patches + module QueryPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.class_eval do + unloadable + + alias_method :add_filter_without_contacts, :add_filter + alias_method :add_filter, :add_filter_with_contacts + + alias_method :available_filters_as_json_without_contacts, :available_filters_as_json + alias_method :available_filters_as_json, :available_filters_as_json_with_contacts + end + end + + module InstanceMethods + def add_filter_with_contacts(field, operator, values = nil) + add_filter_without_contacts(field, operator, values) + + if available_filters[field] && %w(company contact deal).include?(available_filters[field][:field_format]) + filter_options = available_filters[field] + # Method :contact_query_values should be defined in query class for model + if respond_to?(:contact_query_values) + filter_options[:values] = contact_query_values(:field => field, :values => values) + end + return if filter_options[:values].present? + filter_options[:values] = default_contact_query_values(:field => field, :values => values) + end + true + end + + def available_filters_as_json_with_contacts + json_data = available_filters_as_json_without_contacts + Hash[json_data.map do |f_name, f_data| + f_data['field_format'] = available_filters[f_name][:field_format] + [f_name, f_data] + end] + end + end + end + end +end + +unless Query.included_modules.include?(RedmineContacts::Patches::QueryPatch) + Query.send(:include, RedmineContacts::Patches::QueryPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/setting_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/setting_patch.rb new file mode 100644 index 0000000..901f649 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/setting_patch.rb @@ -0,0 +1,73 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module SettingPatch + def self.included(base) + base.extend(ClassMethods) + # base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + # Setting.available_settings["disable_taxes"] = {'default' => 0} + # @@available_settings["disable_taxes"] = {} + + end + end + + module ClassMethods + + # Setting.available_settings["disable_taxes"] = {} + + # def disable_taxes? + # self[:disable_taxes].to_i > 0 + # end + + # def disable_taxes=(value) + # self[:disable_taxes] = value + # end + + %w(disable_taxes default_tax tax_type default_currency money_thousands_delimiter money_decimal_separator).each do |name| + src = <<-END_SRC + Setting.available_settings["#{name}"] = "" + + def #{name} + self[:#{name}] + end + + def #{name}? + self[:#{name}].to_i > 0 + end + + def #{name}=(value) + self[:#{name}] = value + end + END_SRC + class_eval src, __FILE__, __LINE__ + end + + end + end + end +end + +unless Setting.included_modules.include?(RedmineContacts::Patches::SettingPatch) + Setting.send(:include, RedmineContacts::Patches::SettingPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/settings_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/settings_helper_patch.rb new file mode 100644 index 0000000..afe2cec --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/settings_helper_patch.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module SettingsHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + + alias_method :administration_settings_tabs_without_contacts, :administration_settings_tabs + alias_method :administration_settings_tabs, :administration_settings_tabs_with_contacts + end + end + + module InstanceMethods + # include ContactsHelper + + def administration_settings_tabs_with_contacts + tabs = administration_settings_tabs_without_contacts + + tabs.push(:name => 'money', + :partial => 'settings/contacts/money', + :label => :label_crm_money_settings) + end + end + end + end +end + +unless SettingsHelper.included_modules.include?(RedmineContacts::Patches::SettingsHelperPatch) + SettingsHelper.send(:include, RedmineContacts::Patches::SettingsHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/time_entry_query_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/time_entry_query_patch.rb new file mode 100644 index 0000000..31fbd0c --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/time_entry_query_patch.rb @@ -0,0 +1,45 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'query' + +module RedmineContacts + module Patches + module TimeEntryQueryPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.send(:include, RedmineContacts::Helper) + + base.class_eval do + unloadable + end + end + + module InstanceMethods + def contact_query_values(options = {}) + contacts_for_select(nil, :where => { :id => options[:values] }, :short_label => true) + end + end + end + end +end + +unless TimeEntryQuery.included_modules.include?(RedmineContacts::Patches::TimeEntryQueryPatch) + TimeEntryQuery.send(:include, RedmineContacts::Patches::TimeEntryQueryPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/time_report_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/time_report_patch.rb new file mode 100644 index 0000000..9f0df16 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/time_report_patch.rb @@ -0,0 +1,54 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module TimeReportPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method :load_available_criteria_without_contacts, :load_available_criteria + alias_method :load_available_criteria, :load_available_criteria_with_contacts + end + end + + module InstanceMethods + def load_available_criteria_with_contacts + @available_criteria = load_available_criteria_without_contacts + @available_criteria['deal'] = { :sql => 'd_deals_issues.deal_id', + :kclass => Deal, + :joins => 'LEFT OUTER JOIN deals_issues d_deals_issues ON d_deals_issues.issue_id = issues.id', + :label => :label_deal } if User.current.allowed_to?(:view_deals, @project, :global => true) + @available_criteria['deal_contact'] = { :sql => 'd_deals.contact_id', + :kclass => Contact, + :joins => 'LEFT OUTER JOIN deals_issues c_deals_issues ON c_deals_issues.issue_id = issues.id + LEFT OUTER JOIN deals d_deals ON c_deals_issues.deal_id = d_deals.id', + :label => :label_crm_deal_contact } if User.current.allowed_to?(:view_deals, @project, :global => true) + @available_criteria + end + end + end + end +end + +unless Redmine::Helpers::TimeReport.included_modules.include?(RedmineContacts::Patches::TimeReportPatch) + Redmine::Helpers::TimeReport.send(:include, RedmineContacts::Patches::TimeReportPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/timelog_helper_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/timelog_helper_patch.rb new file mode 100644 index 0000000..33c7b21 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/timelog_helper_patch.rb @@ -0,0 +1,50 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module Patches + module TimelogHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method :format_criteria_value_without_contacts, :format_criteria_value + alias_method :format_criteria_value, :format_criteria_value_with_contacts + end + end + + module InstanceMethods + def format_criteria_value_with_contacts(criteria_options, value) + if !value.blank? && criteria_options[:kclass] == Contact && obj = Contact.find_by_id(value.to_i) + obj.visible? ? obj.name : "#{l(:label_contact)} - ##{obj.id}" + elsif !value.blank? && criteria_options[:kclass] == Deal && obj = Deal.find_by_id(value.to_i) + obj.visible? ? "#{obj.full_name} (#{obj.info})" : "#{l(:label_deal)} - ##{obj.id}" + else + format_criteria_value_without_contacts(criteria_options, value) + end + end + end + end + end +end + +unless TimelogHelper.included_modules.include?(RedmineContacts::Patches::TimelogHelperPatch) + TimelogHelper.send(:include, RedmineContacts::Patches::TimelogHelperPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/patches/users_controller_patch.rb b/plugins/redmine_contacts/lib/redmine_contacts/patches/users_controller_patch.rb new file mode 100644 index 0000000..57b00b6 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/patches/users_controller_patch.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require_dependency 'users_controller' +require_dependency 'user' + +module RedmineContacts + module Patches + module UsersControllerPatch + def self.included(base) # :nodoc: + base.class_eval do + end + base.send(:include, InstanceMethods) + end + + module InstanceMethods + def new_from_contact + contact = Contact.visible.find(params[:contact_id]) + @user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option) + @user.firstname = contact.first_name + @user.lastname = contact.last_name + @user.mail = contact.emails.first + @auth_sources = AuthSource.all + respond_to do |format| + format.html { render :action => 'new' } + end + end + end + end + end +end + +unless UsersController.included_modules.include?(RedmineContacts::Patches::UsersControllerPatch) + UsersController.send(:include, RedmineContacts::Patches::UsersControllerPatch) +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb b/plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb new file mode 100644 index 0000000..8ae102e --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/utils/check_mail.rb @@ -0,0 +1,120 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require 'net/imap' +require 'net/pop' +require 'openssl' +require 'timeout' + +module RedmineContacts + module Mailer + class << self + + def check_imap(mailer, imap_options={}, options={}) + host = imap_options[:host] || '127.0.0.1' + port = imap_options[:port] || '143' + ssl = !imap_options[:ssl].nil? + folder = imap_options[:folder] || 'INBOX' + + Timeout::timeout(15) do + @imap = Net::IMAP.new(host, port, ssl) + @imap.login(imap_options[:username], imap_options[:password]) unless imap_options[:username].nil? + end + + @imap.select(folder) + msg_count = 0 + + @imap.uid_search(['NOT', 'SEEN']).each do |uid| + msg = @imap.uid_fetch(uid,'RFC822')[0].attr['RFC822'] + logger.info "ContactsMailHandler: Receiving message #{uid}" if logger && logger.info? + msg_count += 1 + + if mailer.receive(msg, options) + logger.info "ContactsMailHandler: Message #{uid} successfully received" if logger && logger.info? + if imap_options[:move_on_success] && imap_options[:move_on_success] != folder + @imap.uid_copy(uid, imap_options[:move_on_success]) + end + @imap.uid_store(uid, "+FLAGS", [:Seen, :Deleted]) + else + logger.info "ContactsMailHandler: Message #{uid} can not be processed" if logger && logger.info? + @imap.uid_store(uid, "+FLAGS", [:Seen]) + if imap_options[:move_on_failure] + @imap.uid_copy(uid, imap_options[:move_on_failure]) + @imap.uid_store(uid, "+FLAGS", [:Deleted]) + end + end + end + @imap.expunge + msg_count + ensure + if defined?(@imap) && @imap && !@imap.disconnected? + @imap.disconnect + end + end + + def check_pop3(mailer, pop_options={}, options={}) + + host = pop_options[:host] || '127.0.0.1' + port = pop_options[:port] || '110' + apop = (pop_options[:apop].to_s == '1') + delete_unprocessed = (pop_options[:delete_unprocessed].to_s == '1') + + pop = Net::POP3.APOP(apop).new(host,port) + pop.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if pop_options[:ssl] + logger.info "ContactsMailHandler: Connecting to #{host}..." if logger && logger.info? + msg_count = 0 + pop.start(pop_options[:username], pop_options[:password]) do |pop_session| + if pop_session.mails.empty? + logger.info "ContactsMailHandler: No email to process" if logger && logger.info? + else + logger.info "ContactsMailHandler: #{pop_session.mails.size} email(s) to process..." if logger && logger.info? + pop_session.each_mail do |msg| + msg_count += 1 + message = msg.pop(String.new) + uid = (message =~ /^Message-ID: (.*)/ ? $1 : '').strip + if mailer.receive(message, options) + msg.delete + logger.info "--> ContactsMailHandler: Message #{uid} processed and deleted from the server" if logger && logger.info? + else + if delete_unprocessed + msg.delete + logger.info "--> ContactsMailHandler: Message #{uid} NOT processed and deleted from the server" if logger && logger.info? + else + logger.info "--> ContactsMailHandler: Message #{uid} NOT processed and left on the server" if logger && logger.info? + end + end + end + end + end + msg_count + ensure + if defined?(pop) && pop && pop.started? + pop.finish + end + end + + private + + def logger + ::Rails.logger + end + + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/utils/csv_utils.rb b/plugins/redmine_contacts/lib/redmine_contacts/utils/csv_utils.rb new file mode 100644 index 0000000..3c58915 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/utils/csv_utils.rb @@ -0,0 +1,49 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module CSVUtils + include Redmine::I18n + + class << self + + def csv_custom_value(custom_value) + return "" unless custom_value + value = custom_value.value + case custom_value.custom_field.field_format + when 'date' + begin; format_date(value.to_date); rescue; value end + when 'bool' + l(value == "1" ? :general_text_Yes : :general_text_No) + when 'float' + sprintf("%.2f", value).gsub('.', l(:general_csv_decimal_separator)) + else + if value.is_a?(Array) + value.map(&:to_s).join(', ') + else + value.to_s + end + end + rescue + return "" + end + + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/utils/date_utils.rb b/plugins/redmine_contacts/lib/redmine_contacts/utils/date_utils.rb new file mode 100644 index 0000000..dd80538 --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/utils/date_utils.rb @@ -0,0 +1,67 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module DateUtils + class << self + def retrieve_date_range(period) + from, to = nil, nil + case period + when 'today' + from = to = Date.today + when 'yesterday' + from = to = Date.today - 1 + when 'current_week' + from = Date.today - (Date.today.cwday - 1)%7 + to = from + 6 + when 'last_week' + from = Date.today - 7 - (Date.today.cwday - 1)%7 + to = from + 6 + when 'last_2_weeks' + from = Date.today - 14 - (Date.today.cwday - 1)%7 + to = from + 13 + when '7_days' + from = Date.today - 7 + to = Date.today + when 'last_7_days' + from = Date.today - 14 + to = from + 7 + when 'current_month' + from = Date.civil(Date.today.year, Date.today.month, 1) + to = (from >> 1) - 1 + when 'last_month' + from = Date.civil(Date.today.year, Date.today.month, 1) << 1 + to = (from >> 1) - 1 + when '30_days' + from = Date.today - 30 + to = Date.today + when 'current_year' + from = Date.civil(Date.today.year, 1, 1) + to = Date.civil(Date.today.year, 12, 31) + when 'last_year' + from = Date.civil(1.year.ago.year, 1, 1) + to = Date.civil(1.year.ago.year, 12, 31) + end + + from, to = from, to + 1 if (from && to) + [from, to] + end + end + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/utils/thumbnail.rb b/plugins/redmine_contacts/lib/redmine_contacts/utils/thumbnail.rb new file mode 100644 index 0000000..90c19fb --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/utils/thumbnail.rb @@ -0,0 +1,51 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require 'fileutils' + +module RedmineContacts + module Thumbnail + extend Redmine::Utils::Shell + include Redmine::Thumbnail + + CONVERT_BIN = (Redmine::Configuration['imagemagick_convert_command'] || 'convert').freeze + + # Generates a thumbnail for the source image to target + def self.generate(source, target, size) + return nil unless Redmine::Thumbnail.convert_available? + unless File.exists?(target) + directory = File.dirname(target) + unless File.exists?(directory) + FileUtils.mkdir_p directory + end + size_option = "#{size}x#{size}^" + sharpen_option = "0.7x6" + crop_option = "#{size}x#{size}" + cmd = "#{shell_quote CONVERT_BIN} #{shell_quote source} -resize #{shell_quote size_option} -sharpen #{shell_quote sharpen_option} -gravity center -extent #{shell_quote crop_option} #{shell_quote target}" + unless system(cmd) + Rails.logger.error("Creating thumbnail failed (#{$?}):\nCommand: #{cmd}") + return nil + end + end + target + end + + + end +end diff --git a/plugins/redmine_contacts/lib/redmine_contacts/wiki_macros/contacts_wiki_macros.rb b/plugins/redmine_contacts/lib/redmine_contacts/wiki_macros/contacts_wiki_macros.rb new file mode 100644 index 0000000..7ecfd6d --- /dev/null +++ b/plugins/redmine_contacts/lib/redmine_contacts/wiki_macros/contacts_wiki_macros.rb @@ -0,0 +1,87 @@ +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +module RedmineContacts + module WikiMacros + Redmine::WikiFormatting::Macros.register do + + desc "Contact Description Macro" + macro :contact_plain do |obj, args| + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size != 1 + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + first_name, last_name = args.first.split + conditions = {:first_name => first_name} + conditions[:last_name] = last_name if last_name + contact = Contact.visible.find(:first, :conditions => conditions) + else + contact = Contact.visible.find_by_id(args.first) + end + link_to_source(contact) if contact + end + + desc "Contact avatar" + macro :contact_avatar do |obj, args| + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size != 1 + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + first_name, last_name = args.first.split + conditions = {:first_name => first_name} + conditions[:last_name] = last_name if last_name + contact = Contact.visible.find(:first, :conditions => conditions) + else + contact = Contact.visible.find_by_id(args.first) + end + link_to avatar_to(contact, :size => "32"), contact_path(contact), :id => "avatar", :title => contact.name if contact + end + + desc "Contact with avatar" + macro :contact do |obj, args| + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size != 1 + if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/) + first_name, last_name = args.first.split + conditions = {:first_name => first_name} + conditions[:last_name] = last_name if last_name + contact = Contact.visible.find(:first, :conditions => conditions) + else + contact = Contact.visible.find_by_id(args.first) + end + contact_tag(contact) if contact + end + + desc "Contact/Deal note" + macro :contact_note do |obj, args| + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size != 1 + note = Note.find_by_id(args.first) + textilizable(note, :content).html_safe if note && note.source.visible? + end + desc "Deal" + macro :deal do |obj, args| + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size != 1 + deal = Deal.visible.find(args.first) + deal_tag(deal) + end + + end + + end +end diff --git a/plugins/redmine_contacts/lib/tasks/clear_tags_table.rake b/plugins/redmine_contacts/lib/tasks/clear_tags_table.rake new file mode 100644 index 0000000..366f76a --- /dev/null +++ b/plugins/redmine_contacts/lib/tasks/clear_tags_table.rake @@ -0,0 +1,16 @@ +namespace :redmine do + namespace :contacts do + + desc <<-END_DESC +Clear tags table. + + rake redmine:contacts:clear_tags_table RAILS_ENV="production" +END_DESC + + task :clear_tags_table => :environment do + ActiveRecord::Migration.remove_column(:tags, :color) if RedmineCrm::Tag.column_names.include?("color") + ActiveRecord::Migration.remove_column(:tags, :created_at) if RedmineCrm::Tag.column_names.include?("created_at") + ActiveRecord::Migration.remove_column(:tags, :updated_at) if RedmineCrm::Tag.column_names.include?("updated_at") + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts/lib/tasks/contacts.rake b/plugins/redmine_contacts/lib/tasks/contacts.rake new file mode 100644 index 0000000..4137668 --- /dev/null +++ b/plugins/redmine_contacts/lib/tasks/contacts.rake @@ -0,0 +1,69 @@ +namespace :redmine do + namespace :contacts do + + desc <<-END_DESC +Drop settings. + + rake redmine:contacts:drop_settings RAILS_ENV="production" plugin="plugin_redmine_contacts" + +Plugins: + plugin_redmine_contacts: Redmine CRM plugin + plugin_redmine_contacts_helpdesk: Redmine Helpdesk plugin + plugin_redmine_contacts_invoices: Redmine Invoices plugin + +END_DESC + + task :drop_settings => :environment do + plugin_name = ENV['plugin'] + Setting[plugin_name.to_sym] = {} if plugin_name + end + + desc <<-END_DESC +Set plugin settings. + + rake redmine:contacts:settings RAILS_ENV="production" plugin="plugin_redmine_contacts" setting="list_partial_style" value="list" + rake redmine:contacts:settings RAILS_ENV="production" project="helpdesk" setting="contacts_show_deals_tab" value="1" + +Plugins: + plugin_redmine_contacts: Redmine CRM plugin + plugin_redmine_contacts_helpdesk: Redmine Helpdesk plugin + plugin_redmine_contacts_invoices: Redmine Invoices plugin + +END_DESC + + task :settings => :environment do + plugin_name = ENV['plugin'] + setting = ENV['setting'] + value = ENV['value'] + project = ENV['project'] + + if (plugin_name.blank? && project.blank?) || setting.blank? || value.blank? + puts "RedmineCRM: Params plugin, setting and value should be present" + return + end + + if project + ContactsSetting[setting, Project.find(project).id] = value + else + plugin_settings = Setting[plugin_name.to_sym] + plugin_settings[setting] = value + Setting[plugin_name.to_sym] = plugin_settings + end + + end + + desc <<-END_DESC +Load CRM plugin default data. +END_DESC + + task :load_default_data => :environment do + DealStatus.create(:name => l(:label_deal_status_pending), :status_type => DealStatus::OPEN_STATUS, :is_default => true, :color => "AAAAAA".hex) + DealStatus.create(:name => l(:label_deal_status_won), :status_type => DealStatus::WON_STATUS, :is_default => false, :color => "008000".hex) + DealStatus.create(:name => l(:label_deal_status_lost), :status_type => DealStatus::LOST_STATUS, :is_default => false, :color => "FF0000".hex) + end + + + + + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts/lib/tasks/contacts_email.rake b/plugins/redmine_contacts/lib/tasks/contacts_email.rake new file mode 100644 index 0000000..8b21c3c --- /dev/null +++ b/plugins/redmine_contacts/lib/tasks/contacts_email.rake @@ -0,0 +1,154 @@ +namespace :redmine do + namespace :email do + namespace :contacts do + + desc <<-END_DESC +Read an email from standard input. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + rake redmine:email:read RAILS_ENV="production" < raw_email + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + rake redmine:email:contacts:read RAILS_ENV="production" \\ + project=foo \\ + tracker=bug \\ + allow_override=tracker,priority < raw_email +END_DESC + + task :read => :environment do + options = { :contact => {} } + # %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + # options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + # options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + # options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + + ContactsMailer.receive(STDIN.read, options) + end + + desc <<-END_DESC +Read emails from an IMAP server. + +General options: + unknown_user=ACTION how to handle emails from an unknown user + ACTION can be one of the following values: + ignore: email is ignored (default) + accept: accept as anonymous user + create: create a user account + no_permission_check=1 disable permission checking when receiving + the email + +Available IMAP options: + host=HOST IMAP server host (default: 127.0.0.1) + port=PORT IMAP server port (default: 143) + ssl=SSL Use SSL? (default: false) + username=USERNAME IMAP account + password=PASSWORD IMAP password + folder=FOLDER IMAP folder to read (default: INBOX) + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Processed emails control options: + move_on_success=MAILBOX move emails that were successfully received + to MAILBOX instead of deleting them + move_on_failure=MAILBOX move emails that were ignored to MAILBOX + +Examples: + # No project specified. Emails MUST contain the 'Project' keyword: + + rake redmine:email:contacts:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx + + + # Fixed project and default tracker specified, but emails can override + # both tracker and priority attributes: + + rake redmine:email:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ + project=foo \\ + tracker=bug +END_DESC + + task :receive_imap => :environment do + imap_options = {:host => ENV['host'], + :port => ENV['port'], + :ssl => ENV['ssl'], + :username => ENV['username'], + :password => ENV['password'], + :folder => ENV['folder'], + :move_on_success => ENV['move_on_success'], + :move_on_failure => ENV['move_on_failure']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + + RedmineContacts::Mailer.check_imap(ContactsMailer, imap_options, options) + end + + desc <<-END_DESC +Read emails from an POP3 server. + +Available POP3 options: + host=HOST POP3 server host (default: 127.0.0.1) + port=PORT POP3 server port (default: 110) + username=USERNAME POP3 account + password=PASSWORD POP3 password + apop=1 use APOP authentication (default: false) + delete_unprocessed=1 delete messages that could not be processed + successfully from the server (default + behaviour is to leave them on the server) + +See redmine:email:contacts:receive_imap for more options and examples. +END_DESC + + task :receive_pop3 => :environment do + pop_options = {:host => ENV['host'], + :port => ENV['port'], + :apop => ENV['apop'], + :username => ENV['username'], + :password => ENV['password'], + :delete_unprocessed => ENV['delete_unprocessed']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:unknown_user] = ENV['unknown_user'] if ENV['unknown_user'] + options[:no_permission_check] = ENV['no_permission_check'] if ENV['no_permission_check'] + + RedmineContacts::Mailer.check_pop3(ContactsMailer, pop_options, options) + end + + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/addresses.yml b/plugins/redmine_contacts/test/fixtures/addresses.yml new file mode 100644 index 0000000..b4075e1 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/addresses.yml @@ -0,0 +1,18 @@ +address_1: + id: 1 + city: New York + country_code: US + region: NY + street1: "1443A 5th Ave" + postcode: 10035 + addressable_type: Contact + addressable_id: 2 + +address_2: + id: 2 + city: Moscow + country_code: RU + street1: "Bolshaya Yakimanka, 2" + postcode: 103234 + addressable_type: Contact + addressable_id: 3 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts.yml b/plugins/redmine_contacts/test/fixtures/contacts.yml new file mode 100644 index 0000000..1b9623b --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts.yml @@ -0,0 +1,47 @@ +contact_one: + id: 1 + first_name: Ivan + last_name: Ivanov + middle_name: Ivanovich + company: Domoway + email: ivan@mail.com + website: http://ivanov.com + +contact_two: + id: 2 + first_name: Marat + last_name: Aminov + middle_name: Ivanovich + job_title: CEO + company: Domoway + email: "marat@mail.ru, marat@mail.com" + skype_name: marat_aminov + background: Marat is a famous writer an reader + +contact_three: + id: 3 + first_name: Domoway + job_title: web project + is_company: true + email: "domoway@mail.ru" + background: Realy cool project + website: domoway.ru + +contact_four: + id: 4 + first_name: John + last_name: Smith + job_title: CEO + is_company: false + email: "jsmith@somenet.foo" + background: "Contact with user realation\nUser id = 2" + website: domoway.ru + +contact_five: + id: 5 + first_name: 'My company' + job_title: web project + is_company: true + email: "company@mail.ru" + background: Realy cool project + website: company.ru diff --git a/plugins/redmine_contacts/test/fixtures/contacts_issues.yml b/plugins/redmine_contacts/test/fixtures/contacts_issues.yml new file mode 100644 index 0000000..8973e74 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_issues.yml @@ -0,0 +1,10 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + issue_id: 1 + contact_id: 1 +two: + issue_id: 2 + contact_id: 2 +three: + issue_id: 1 + contact_id: 2 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_html.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_html.eml new file mode 100644 index 0000000..ca7c0a3 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_html.eml @@ -0,0 +1,128 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Subject: New note from forwarded html email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="Apple-Mail=_A369DB54-0A71-4F82-915A-340994835550" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + + +--Apple-Mail=_A369DB54-0A71-4F82-915A-340994835550 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + + + +Begin forwarded message: + +> From: Marat Aminov +> Subject: Lorem ipsum dolor sit amet, consectetuer +> Date: Wed, 5 Oct 2011 00:16:18 +> To: admin@somenet.foo +>=20 +> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas = +imperdiet=20 +> turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus=20= + +> blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent = +taciti=20 +> sociosqu ad litora torquent per conubia nostra, per inceptos = +himenaeos. In=20 +> in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in = +dolor. Cras=20 +> sagittis odio eu lacus.=20 +>=20 +> Aliquam sem tortor, consequat sit amet, vestibulum id, iaculis at, = +lectus.=20 +> Fusce tortor libero, congue ut, euismod nec, luctus=20 +>=20 +> eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, = +tristique=20 +> sed, mauris. Pellentesque habitant morbi tristique senectus et netus = +et=20 +> malesuada fames ac turpis egestas. Quisque sit amet libero. In hac = +habitasse=20 +> platea dictumst. +>=20 +> Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque=20= + +> sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed = +lorem.=20 +> Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum = +et,=20 +> dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat = +sed,=20 +> massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo=20= + +> pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + + +--Apple-Mail=_A369DB54-0A71-4F82-915A-340994835550 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; + charset=utf-8 + +

        Begin forwarded message:

        From: Marat Aminov <marat@mail.com>
        Subject: = +Lorem ipsum dolor sit amet, = +consectetuer
        Date: Wed, 5 Oct 2011 00:16:18
        Lorem ipsum dolor = +sit amet, consectetuer adipiscing elit. Maecenas = +imperdiet 
        turpis et odio. Integer eget pede vel dolor = +euismod varius. Phasellus 
        blandit eleifend augue. = +Nulla facilisi. Duis id diam. Class aptent = +taciti 
        sociosqu ad litora torquent per conubia nostra, = +per inceptos himenaeos. In 
        in urna sed tellus aliquet = +lobortis. Morbi scelerisque tortor in dolor. = +Cras 
        sagittis odio eu = +lacus. 

        • Aliquam sem tortor, consequat sit amet, = +vestibulum id, iaculis at, lectus. 
        • Fusce tortor = +libero, congue ut, euismod nec, = +luctus 

        eget, eros. Pellentesque = +tortor enim, feugiat in, dignissim eget, tristique 
        sed, = +mauris. Pellentesque habitant morbi tristique senectus et netus = +et 
        malesuada fames ac turpis egestas. Quisque sit amet = +libero. In hac habitasse 
        platea = +dictumst.

        Nulla et nunc. Duis pede. Donec et = +ipsum. Nam ut dui tincidunt neque 
        sollicitudin iaculis. = +Duis vitae dolor. Vestibulum eget massa. Sed = +lorem. 
        Nullam volutpat cursus erat. Cras felis dolor, = +lacinia quis, rutrum et, 
        dictum et, ligula. Sed erat = +nibh, gravida in, accumsan non, placerat sed, 
        massa. Sed = +sodales, ante fermentum ultricies sollicitudin, massa = +leo 
        pulvinar dui, a gravida orci mi eget odio. Nunc a = +lacus.

        = + +--Apple-Mail=_A369DB54-0A71-4F82-915A-340994835550-- \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_plain.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_plain.eml new file mode 100644 index 0000000..891adcb --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/fwd_new_note_plain.eml @@ -0,0 +1,51 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Subject: New note from forwarded email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + + +---- Original message ---- +> From: "Marat Aminov" marat@mail.ru +> To: "Ivan Ivanov" contacts@somenet.foo +> Date: Sun, 26 Jun 2011 12:28:07 +0200 +> Subject: Test subject + +> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +> turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +> blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +> sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +> in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +> sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +> id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +> eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +> sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +> malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +> platea dictumst. +> +> Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +> sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +> Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +> dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +> massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +> pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. +> +> Project: onlinestore +> Tracker: Feature Request +> category: stock management +> priority: URGENT +> \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deal_note_by_id.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deal_note_by_id.eml new file mode 100644 index 0000000..4936804 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deal_note_by_id.eml @@ -0,0 +1,39 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Bcc: +Subject: New note from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deny_note.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deny_note.eml new file mode 100644 index 0000000..f4994ac --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_deny_note.eml @@ -0,0 +1,44 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Bcc: +Subject: New note from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature Request +category: stock management +priority: URGENT \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note.eml new file mode 100644 index 0000000..466616a --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note.eml @@ -0,0 +1,44 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Bcc: +Subject: New note from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature Request +category: stock management +priority: URGENT \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_by_id.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_by_id.eml new file mode 100644 index 0000000..d36bdde --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_by_id.eml @@ -0,0 +1,43 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Subject: New note from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature Request +category: stock management +priority: URGENT \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_with_cc.eml b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_with_cc.eml new file mode 100644 index 0000000..f1d48af --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_mailer/new_note_with_cc.eml @@ -0,0 +1,44 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "redMine Admin" +To: +Cc: +Subject: New note from email by id in cc +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Feature Request +category: stock management +priority: URGENT \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/contacts_projects.yml b/plugins/redmine_contacts/test/fixtures/contacts_projects.yml new file mode 100644 index 0000000..f98a910 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_projects.yml @@ -0,0 +1,19 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +contact_project_one: + project_id: 1 + contact_id: 1 +contact_project_two: + project_id: 1 + contact_id: 2 +contact_project_three: + project_id: 1 + contact_id: 3 +contact_project_0004: + project_id: 2 + contact_id: 1 +contact_project_0005: + project_id: 1 + contact_id: 4 +contact_project_0006: + project_id: 1 + contact_id: 5 diff --git a/plugins/redmine_contacts/test/fixtures/contacts_settings.yml b/plugins/redmine_contacts/test/fixtures/contacts_settings.yml new file mode 100644 index 0000000..9f96242 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/contacts_settings.yml @@ -0,0 +1,13 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +contacts_setting_001: + name: "redmine_contacts_string_setting" + value: "String value" + project_id: 1 +contacts_setting_002: + name: "redmine_contacts_boolean_setting" + value: 1 + project_id: 1 +contacts_setting_003: + name: "redmine_contacts_float_setting" + value: "10.3" + project_id: 1 diff --git a/plugins/redmine_contacts/test/fixtures/deal_categories.yml b/plugins/redmine_contacts/test/fixtures/deal_categories.yml new file mode 100644 index 0000000..d966b07 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deal_categories.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 + name: Design + project_id: 1 +two: + id: 2 + name: Developing + project_id: 1 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/deal_processes.yml b/plugins/redmine_contacts/test/fixtures/deal_processes.yml new file mode 100644 index 0000000..e978710 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deal_processes.yml @@ -0,0 +1,27 @@ +deal_process_001: + deal_id: 1 + old_value: 1 + value: 2 + author_id: 1 + created_at: "<%= (Time.now - 4.days).to_s(:db) %>" + +deal_process_002: + deal_id: 2 + old_value: 2 + value: 1 + author_id: 2 + created_at: "<%= (Time.now - 2.days).to_s(:db) %>" + +deal_process_003: + deal_id: 1 + old_value: 2 + value: 3 + author_id: 1 + created_at: "<%= (Time.now - 1.day).to_s(:db) %>" + +deal_process_004: + deal_id: 2 + old_value: 1 + value: 3 + author_id: 2 + created_at: "<%= Time.now.to_s(:db) %>" diff --git a/plugins/redmine_contacts/test/fixtures/deal_statuses.yml b/plugins/redmine_contacts/test/fixtures/deal_statuses.yml new file mode 100644 index 0000000..9e88ca8 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deal_statuses.yml @@ -0,0 +1,34 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +new: + id: 1 + name: Pending + color: 11184810 + is_default: true + status_type: 0 + position: 1 +won: + id: 2 + name: Won + color: 32768 + is_default: false + status_type: 1 +lost: + id: 3 + name: Lost + color: 16711680 + is_default: false + status_type: 2 +intermediate1: + id: 4 + name: Intermediate 1 + color: 0 + is_default: false + status_type: 0 + position: 2 +intermediate2: + id: 5 + name: Intermediate 2 + color: 0 + is_default: false + status_type: 0 + position: 3 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/deal_statuses_projects.yml b/plugins/redmine_contacts/test/fixtures/deal_statuses_projects.yml new file mode 100644 index 0000000..e88a134 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deal_statuses_projects.yml @@ -0,0 +1,24 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +deal_status_project_001: + project_id: 1 + deal_status_id: 1 + +deal_status_project_002: + project_id: 1 + deal_status_id: 2 + +deal_status_project_003: + project_id: 1 + deal_status_id: 3 + +deal_status_project_004: + project_id: 2 + deal_status_id: 1 + +deal_status_project_005: + project_id: 2 + deal_status_id: 2 + +deal_status_project_006: + project_id: 2 + deal_status_id: 3 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/deals.yml b/plugins/redmine_contacts/test/fixtures/deals.yml new file mode 100644 index 0000000..9c0e21a --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deals.yml @@ -0,0 +1,59 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +deal_001: + id: 1 + name: First deal with contacts + contact_id: 1 + category_id: 1 + project_id: 1 + background: Test deal + author_id: 1 + status_id: 1 + assigned_to_id: 1 + due_date: "<%= Time.now.to_s(:db) %>" + price: 3000 + +deal_002: + id: 2 + name: Second deal with contacts + contact_id: 3 + project_id: 2 + background: Test deal two + author_id: 2 + status_id: 3 + due_date: "<%= 3.days.from_now.to_s(:db) %>" + price: 10000 + currency: "USD" + +deal_003: + id: 3 + name: Delevelop redmine plugin + contact_id: 3 + project_id: 2 + background: Cross project deal + author_id: 2 + status_id: 2 + due_date: "<%= 1.days.from_now.to_s(:db) %>" + price: 5000 + currency: "USD" + +deal_004: + id: 4 + name: Deal without contact + project_id: 2 + background: Deal without contact + author_id: 2 + status_id: 1 + due_date: "<%= 2.days.from_now.to_s(:db) %>" + price: 25500 + currency: "RUB" + +deal_005: + id: 5 + name: Closed deal + contact_id: 3 + project_id: 2 + background: Closed deal + author_id: 2 + status_id: 2 + price: 25500 + currency: "RUB" diff --git a/plugins/redmine_contacts/test/fixtures/deals_issues.yml b/plugins/redmine_contacts/test/fixtures/deals_issues.yml new file mode 100644 index 0000000..1b6f8ad --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/deals_issues.yml @@ -0,0 +1,10 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +di_001: + issue_id: 1 + deal_id: 1 +di_002: + issue_id: 2 + deal_id: 2 +di_003: + issue_id: 3 + deal_id: 2 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/files/contacts_cf.csv b/plugins/redmine_contacts/test/fixtures/files/contacts_cf.csv new file mode 100644 index 0000000..c34db3c --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/files/contacts_cf.csv @@ -0,0 +1,2 @@ +#,Is company,First Name,Middle Name,Last Name,Job title,Company,Phone,Email,Address,Skype,Website,Birthday,Tags,Background,License,Purchase date,Responsible,Postcode,City,Country code,LIST_FIELD +25,0,Monica,,Smith,Realtor,"LLC ""Selection""",+1 650-253-0000,ivan@mail.com,,ivan.ivanov,,,"partner, realtor, smart, conference, do not call, PhD",,12345,2012-12-12,rhill,123456,Moscow,RU,"1, 3" diff --git a/plugins/redmine_contacts/test/fixtures/files/correct.csv b/plugins/redmine_contacts/test/fixtures/files/correct.csv new file mode 100644 index 0000000..8938934 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/files/correct.csv @@ -0,0 +1,5 @@ +#;Is company;First Name;Middle Name;Last Name;Job title;Company;Phone;Email;Address;City;Country Code;Skype;Website;Birthday;Tags;Background;License;Purchase date +46;0;Steven;Anthony;Ballmer;CEO;Microsoft;+1 (425) 706-8448;steveb@microsoft.com;;Seattle;US;;;24.03.1956;first, negative, partner;Steven Anthony 'Steve' Ballmer (born March 24, 1956)[4] is an American businessman and the chief executive of Microsoft, having held that post since January 2000.[4] As of 2011, his personal wealth is estimated at US$14.5 billion, ranking number 46 on the Forbes list of billionaires.[2];; +12;0;Timothy;D.;Cook;CEO;Apple Inc.;+1 233 943 32 23;tim@apple.com;Cupertino , CA;Cupertino;GB;;;01.11.1960;conference;Independent Director , Nike, Inc. Beaverton , OR Sector: CONSUMER GOODS / Textile - Apparel Footwear & Accessories 50 Years Old Timothy D. Cook, Chief Operating Officer, joined the Company in March 1998. Mr. Cook also served as Executive Vice President, Worldwide Sales and Operations from 2002 to 2005. In 2004, his responsibilities were expanded to include the Company?s Macintosh hardware engineering. From 2000 to 2002, Mr. Cook served as Senior Vice President, Worldwide Operations, Sales, Service and Support. From 1998 to 2000, Mr. Cook served as Senior Vice President, Worldwide Operations. Prior to joining the Company, Mr. Cook was Vice President, Corporate Materials for Compaq Computer Corporation (Compaq). Prior to his work at Compaq, Mr. Cook was Chief Operating Officer of the Reseller Division at Intelligent Electronics. Mr. Cook also spent 12 years with International Business Machines Corporation (IBM), most recently as Director of North American Fulfillment. As CEO, Mr. Jobs will remain involved in major strategic decisions during this leave of absence, and Chief Operating Officer Tim Cook will be responsible for Apple's day to day operations.;; +44;0;Билл;Генри;ГейтÑ;Chairman;Microsoft;+1 206-709-3100, +1 425-882-8080, +1 (408) 726-4390;billg@microsoft.com;Bill & Melinda Gates Foundation P.O. Box 23350 Seattle, WA 98102;;;;http://www.thegatesnotes.com/;28.10.1955;conference, IT;William Henry 'Bill' Gates III (born October 28, 1955)[3] is an American business magnate, investor, philanthropist, author, and former CEO and current chairman of Microsoft, the software company he founded with Paul Allen. He is consistently ranked among the world's wealthiest people[4] and was the wealthiest overall from 1995 to 2009, excluding 2008, when he was ranked third.[5] During his career at Microsoft, Gates held the positions of CEO and chief software architect, and remains the largest individual shareholder, with more than 8 percent of the common stock.[6] He has also authored or co-authored several books.;; +13;0;Steven;Paul;Jobs;Chargé de communication;Apple Inc.;+1 344 563 93 33;jobs@apple.com;;;RU;;;24.02.1955;vip, IT;Steven Paul 'Steve' Jobs;; diff --git a/plugins/redmine_contacts/test/fixtures/files/deals_correct.csv b/plugins/redmine_contacts/test/fixtures/files/deals_correct.csv new file mode 100644 index 0000000..8c1ab9b --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/files/deals_correct.csv @@ -0,0 +1,2 @@ +#;Name;Background;Currency;Sum;Author;Assignee;Status;Contact;Category;Created;Updated,LIST_FIELD +8;Сделка века;КемÑка волоÑть;JPY;100500;Redmine Admin;rhill;Won;Иван Грозный;Design;17.12.2012;17.12.2012;1, 3 diff --git a/plugins/redmine_contacts/test/fixtures/files/image.jpg b/plugins/redmine_contacts/test/fixtures/files/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b2444a92ebd90d70de2aa1c34ec5b2ee302c04ef GIT binary patch literal 12197 zcmYki1yCH#^FO?BhZ9_m!#%jW6C47;gNC5NU4jO8_XG>>?(Qyu0KuIAhr7d@=l8Ap z*Sov5+toeWJ-bsq)AQ+mU3lF9uoPtEWdJxh0D${<0QL7>z`P$&f(1r0p|Gcz+G1qTm1BM%)DGXoq7Dk=s# z1{4z$%1uK~!}d?g{9hAhHU?%^?*E8?1R#t^vs!hLQgOLJL3SF+`7$k0+YZ*=e(_diiqHVe>VRAM&RH< z0Qf%}tk?iNJb(a)fb#zu{4)Z=q2!QI#r^!V=8TF{&Cx%;XL?hT3;enSpo8H4bsmHb zhyfS9Gr?+0ZR+(-9FsX3ufWP9qoEkR{qrl}QKjjzf)=nF1SeuQlj{U@U-0i4j(Vh9 zw_jp~m`pjW;HQ8a*2)91`i{H#AFUio@RPw6tB%8RLj5}ur)xhBSRz_!O5m)oYnJL?%lN9Q zCm35920qs2TRL6v>C3ns17FDLU%EV=ig_4eJR^jx8<+Mi&)XN3?>qNyV@CtkqTzc- z-TB&mH7%P~UV(A3_wWU%JRx-J&aVLbGta8lv-Zp9hcEbLzl5#ym~|q?;Sh2u3nfnj z$(D2;8Rzq4wUyqfc~(93UHvg%FMS0j#iVVjl&CspAl2sRwff3Aa6%--85!w3{FRw% z3VjCZklA4xqWJM=(r1ZveCCTgZ-w31fejOde>%3Ww0ZQdKiv=Q7e1&Iv~J{-Brl}0 zL=6vLynKRAmJaC7Y0oBQiUt?kOp;b8uz9vn2mg%nW+boO}i3Z2e*G@uwArggLrr?V1{I zt!+heKhH%0bsX$pk;gR&f2wMXRg3(##6W$x4fWpffreTWmoY7l<)rU7MjNR63pyRP z2gk}1i8Yt5Z*W6Q1vny$owxHLMN=|5FI>vb^9zp+liskKJ0WY1ezBR4e~e%d@*xNp z$4fRNX_a60kw~_Ce29UF*1!L2Ii^pWoAVFDAz%+U7U zxY-v?7^kw3et&frgl!Y!^ZY!Rxg~M=moX?6e*R8TI*hdxE?Rm7>>k#{ag>eP)o(NI zTC&G-yo!Lc*Q2+!)&Z3FhkVjp<{Wk2_RE`M zd3J4eIih2-ei@-XAyrWpa}(Vo^s#Qysyr^82F7%u{)rWnB&Edcq;8Vo-&1kq zW^6-e`#E@&A%zB#M&BBTDR^b`Wor`(S-od`ys|^)&?|U~o$0bdSszedPTN(hT-vHB z_HkDtiX7ZO2n8daP89uk1r+=O54>U>vFnqWOFv^lXh2|A%qXQj&zU9IE$UjizRQ?D zpa+7nU9T@IoRdhnF&p6$1BhT7cBUDFO(EbXs#Jom6AJS-Z-}W1tQh*1L%#aKz+HZ}XY2qqWL6D-K%=+Y>`aR`J94H)ie z=3D;ZG=+5cV|^WuALWjDVp9u-_PY7a-Kei%BG}xd=hgPV5$8ph!cHHTbv6G}vaifK z(WZ5M_KC$C*(33oM0QvTddA2BlF{L(Xi-%QR)0=c+kdI1CrMf2wg`M^8@Ux)IG>UajI?qjH`8UWqrYm z#hDLakW|c;!vbz)ME<%D&Jyd}7}JojFUh+2Q!na#W|^sKApn`%lrevWi!#yV)lYf&O1wIRcd$|626;@dzbUytPGAfvaxxa9 zN~YtMx@XCrM}v(+G2mK4A*KS5neSYBMmb0 z&1Y^OTLam5zQ|*u7T>fdJ8!c(E$n;81oiv6IHkkc{4)3=wy_TviDg8^H%Cpzu{;*v znqPvO>W8pX%eLJ101{Ng(i$$A_=x2 z8&NWu0Yl~{K)*`VZ(012`u-GX#CxI!Mvck@#9Qfe_zl5PR>#HqvN4*@MgODq&JJ1+ zjNXPuwHgXNBsXIJz^UO_)V`7rN6CwZe-zl+iJL`_6 z4L`ZB8Pp0CKFd^GB@cY~SqgNyFvk-+!e8KYb?h14z773 zcsc@;Ruik=But-b$#(yILC9rEy9(udbLAF$VCY%ET>a<%_dd5H`NQnoMk_?0*{ro- zZV5^gCxvWIT!J)(C%ccOuGwfprL7edA9zTfM^B!~G@Z$QU1`2?@+Y3AADi5C{h_i#pc@d_k7o9gLTXL>d3-J z2(3)s(wTbihggK|c^KN*$XX`;@sO320zptA0~^u#a2~%cDhf5B90QqM;*9Eh%gimr z^2jtg9xg))lJ&{*G^b;3o(2&u5UaKA0?AKwgE1~LqOik9>WA7Kfp2`Pgu3HsW`Ek6 z+p=fFT5@|Y^sqY4sD?^PB5(?{YS7icNJTKMzGi22vI|OGH&CYQj!iIGzlfjmW^uK z60Dwuy3t6a$BYv2o2XT4X`)EvI3gqyc-|JKO>WbDC);0UoT9U$3G(BOD2m^M}0?rSdfjB)IM1^s$VskOMTMPX2^2%6{h2K zrsrQ-=5jP7BsmZw-rHF&(yNZnSV;&{BV^EXau^(#E`OH!aXHgWt4Z>TPd{~pxo|ZX zM$v~gDmSQ14frtho9*HwxG|glg+)zLO(sHxpgx%A-aX>R2v)5c@G2PA&I5BD~ zY3@WlO&f#ce*!uxy{>9+TryW4biCc3EK-Mw^yU1%qU{cjvSp`9Xci1Y{tZsgOl~Xw z)A?rQgL1wR{WP=26k%%SH6$3C|+ZrKOn#`B!Z z3L7Tsjj0k{)D%}D#U*r0YNaDOQ;8{ekIEkejnjS_fwd2~^w?ZQS!;Ork3l1A78-?j zg>^+c1Kq!`BI2<#>$nuui;Nqj^13Ff=KcEY{FV6cX&-LI-jrgxn2qRV^JdvUH}G3e zqEm7%42k;H8MOV+XLU)$fJuLRmwLoMwT&f|q%plfU^NBoRKrQ$N-)x1Lazs}vcx%G z*~m+v`X;(~%Hg`A5bikr19-C*W4r%7D0(vuA3=^1{M=xsbx4fl&fiZqFU^{4qSq>9 z5$S0ALv>YriW90*?US#m0^OGVk~P_%ZP0jfqsq4ICO2du2S{OGL{ksSr%!rQFh2>M&SiwyriBY!g=}YGHd@8@w=^QVU456z_{>YXCiMZ$y4;P!oL>!;DzRc( zIc*Xw;~O_lVqbH+aJCNJmf>p<$HZO?{Eyz2NuS5LttCjp{YV1Vx9uRoM+K5Q3_BZR1F`QN6t-z1 zciK-fJN$u9J0^*hMWRFQ&738%RJt zLsrJ2&<*z$$UHDj>1-xrS%QDkbBzJjdl$_J^kE<6#86(5 z{Z{^Y73=IVmo>iP(>4B}ILsHVT@oZ@H7gJXv9a%3Rmu6DYJF6mr<$g7OyVAW`&e4$ zQCT@X2N@aq;?v+4*`rk6`%F9dbq%ewC zK>XTjY!RyKmO+A!GAwF3mG!J>8p6q*mv)_s&vJH=M@4f9KH?>LD*F;$M)(=RPZjeD zaCaHJ0&hxLmRsXZjc{^K#CwSLT5gT0LcdZi1s;mULv44+T>iMQXa;YxAoAiJ=?CuU ze8KdtBQ;Iu^hWtB;Kj>5gmCcjVeg!Uu7U!z0e5nFR-pF)>%Bo4z?6l>+C6HE{|N2W zd-!ADm0M&>z2F@GIN|C~sPe;4m&rtARL8dmX?nJZI1m%;d#`z_mno#2KEzQ{D|+#k z(FZiogR~8&m=ju zNnpJGW|n|lW=CinRTJD!SY|$bRv77{Pj(k0-MhpUF`U@ew&GMn*}7-OkIG|-^_cdS z4or~mUO;9+UVEC#qbdsB$$j9-FUMU;H|Dax(`1VNGH#JUZ#G6wp}ZH5hkiAzNw3Ml@!QE%)l`y)EDat#US)nL}=v=*% zPmh`@t}%DyL0a1<)=2i|7v$kq8*XqSe}x^o%oIVj{l{c4Gk@H5gye5F9p3t*pXHU$ zmAdJKrG!c>5L=@Tk?eJSow1XHmq$IxmHPr`8wNHOu2~hDxygEW0d){tOJ=QKR&!_l zFDPYMx5P4zL(yky1!6>J57Z$PJ|S>@6w^z#J+=UhJpwrPBdlEr!)ZwjJk^}hjnR^mP2Z#qUJNEdnZJXKZf z%vJ9~{T-$#-dS-}E(q`o*gySYb#YFP=?{>POzGw%q;A-3aJqh^1U~2o)*<_6lp)f$ zVH0m98&PS%i_{ADKev6*>0mp^w7n<3i#$|c{6_dR=A8t(Y8xQWxieRc_?QDtG2fDU z=U3)E|H zRN7yTle%0UTte13W7!$3(ZS?gX25B>4?gl6H$v%I?v3@!J8p$(H&uN5HaPP(ejLPT zG`?Y#?~!y6^V4cNB?B$kCVw>kqVeA970`}WTp=>kUq+B3Pg1%`8%{aLTkYgst;X4+ zWTHrJjOBpJ`KbM75kkj=>gC^!sX+0*(S6pWr{hF(5;NmW`mvyu8)rxF@8!vIwH*mtZqQbacGHXI-9SKO)$?br7GbuX zIxTqpw{<={_PegIbnyP*iyMxhYEmkg?2@g7%+dVlJY{7mF!U)kt!DJI4hF14RlX+Ia3~+?E6E_`nqP_v~o9=j1qd{Ej;kJAN|W6Z1pz(2u)(ifl8$| z#>V9_Q=$74hkSv7yZ@ z3Xnf5ESl9bc!S#~LC#=b3$SmqJHfp)s6^lvr#6D%k!(D>xBEQtv#_J~WtwG_7|cGo zy#nr;AI?(IWI~yx1{2viIEbAJ9%5c3h=Y&(E7#D-m7LM;RS83&eS}Q_W+B%iy#EWS zo5Hnyu{gDSoU%BTK*VlmY;)g-#$B)81ddLVTRaySW{&Hotd*Va@XqRh61s=>r{H8z zc|AZUB!fsutakGtc&Mc33=Tw9!fw7Ko$X!jGLs#t-TTO#5oAgL0TH`LV73Ii=P*W; zZn@$h%5+;BTKKJ9%ib3}oZi{aE$0&j!U8L~;H9t}jK$Xh|L{!?eZEjv zidZ@Q54*L6|E@=yvVcX?`RJ5~5#*z52KQ~z5lhMF@+NFL4p{Yv}QCooYU-Z@j_AkxAWf}h2Mdes%uSXF!POW+Gyl@ zgd=7-z9*}*1ZCL$+Qahy#kq_c6q?D!)Qs9Y8XiZZ}a}toj;Uh;Hj9*nyM5b1Hc!d#`|>MQd@4M4TM8 zF}qLD2hUO&T@e+jXE-w>u4+2ubRbIpN@&X4#F;0&|LnQ&S5;BcP0T8cVy`vmb}{#3 z+LAoSE{~)oC5o)O8SUGS__njjX+t{n2ZBj&Eo-pD@5y#nXDqO0&V9`0~K zGaIkKh<9M*DZ;9ZuJGN_kdnRz0x?ndu(D@wmEsUqEx?W6i3 z1NAeqrfvAqJ^FK(dnRJS0^{07${#NOn{3Kwx3Yp8vY>Kvjr5a*zNs;80%N6&@78%9 zkza-?v`iT#G;P7@VEL25xq^lhvkn^+k5pnVlyl*_d(G(O;1KK6-4cwlVrlj_Q^(n_=y7S~BEyQs7*n3pU@2CF5ftN^s&@M3U)Bs-- z;q%Y7S#+es=EMFNrfM-Bc^Qpk?RW~xfraDbkObma1D8A4k>6%T$@L7z0}ozd*wNdg zb*+kdO9S(b{R9SECYFEUkc6-!Fvhml9iFB(bE>yytoO-g6n=BFxIC7|7bus^$~);w znOX%#nUbIq?S~r6!M5BmYIe99O%@f;<|WYki~XbZM^2)L|E1`nr(>kMvBOvxl@7r6 zxRY|Ev$|t;6e>LCLMC}9NP;@=Sm|~B<3|d4Ik=Scw>*N?R=-eD9Jun+bLL%~6nJFu zuTht~tvfHl8qdIbJ;H%7frdJU|CN zNkhcH;IwR0O#G2#&g4l`L?>SXRP}%Z0vYBmSqgd51a65LiDi}M`6jfX)y|j=3SRCN zYR&F`8zWdZ6Qj~I1>!YB%fQk!UPM%~$T%5Sfc5)=T=$+VQ$K~GkC+8r6;n$B_osWh zB=(A~rHZ=pSML-PxcFy>>`!hnPO+M#(6lN5(^^>X9+EoIipPegMuFiB%~q8R)y?pQ z5O&S;?T3*f=~c!hDKn{RbYzw#rw{b0`=A{w9SsG2=cn#LTY=oAXf*JqBdv(9z#V?FNtAQhE<|!F ze|h-4>=g*t|26H{ojh0sg z!fq#{J|GXgr_oQE^9tySL1QE=rpypG9@vAnQwjw3x&$XG{v~0i3O&X0p<{<1^84MA z*mvCcS5S2egyc(&>3-MSH1)RSkI^Q0O%zPE;muVAjpSQ3w$^Z%kK3nZ9N+xOs6gw0 z22eflYPgG2vm}nS{$yt6DIkcUquyDpJDaP&iB!sEXf`Yrz1!7f zHBF$m&D+MZue0OlZjzotS72E>6M09tF zMDgH*cDy{@s^^MI{>g#ahhjj{0<~JFB9CJ^{jqqAoP?=>JF>E-z!=01vf5K}xOA^g z<^Ix?pL}cyOW=_~iEYZ{I!DVUHO3A*%%)VST*JGmt%|F^6iffn&0R5~$zc<}2oa2v z8$46#Oxu*VBilqR2+UE>$IXun?jBq~TC%JR+&8vew@#p10BmVi#GNFTAvn%y|H=je z7Jrk?uZA^s)d=O_LR%;(fU7-4}B!KUeS>;6c&*urHFK8>1cKtH% zA;;^ZnJK2|hm|kpYE^nfgsiR^LhMq|HwEmuQjz)frwQPDxY4A>zOq2}k2`kA@EqfA zv0Ws)Wx`xB&^@!zQC%U@fKS<-Ns30xS4mhy;GTaQl~n`OhJQFk0PB@cpFTe=>Kk-L z2CaB15Ikkh0Vf~in-?sV*${w_rD*ykN`{xMxz+i=^>LP7ZFpTM=DBId=lRMijrEuH z{Hc>H1Q%JRSE%>~e`15^lX92FcEfH{Fug;`u8labd*Su3x+QO+!3Li*Xmq=2YVhk< zwK4mRF~5U;y&Nq3EsjO5=5>4x#a80l{)8^6VbY447>$9y4fe``<|kU_t3AP|bDeqZ zWcv9Lgt&ucC8P#Hbe#UP{eg`V%dG*n=NlG^i3MzLKY(CU9Rz+ zHOOte_9p)`7&qq|0I+7JaStn{6K5Z2Y~Lk&{D!AN$V6f&4euwGNbtdKFoM;YDpw1ft?Ox`IFD zwVK&pt4%E^Mu>y`OB&vY)Nr~l34L$-P0E}4p%k`@L&b^7Yxgcmo4zUAR;);+F3~Zj zP6e^fHNlNw2OSnI<(`oZosyw-WfsdVx7!IZI$X?__EJx5Msn2N5>#7u7_$KtBdj2U z-HAkn>x4^uk)kykVXjupQTcEIh3;{(b5Fa&~K>h89_vB%E{uzJ>1!{Zvb>?$NrFn~eflGVE;ijId zZK0e$NS{oHgQPL}$Q zoM%@(0zQS8!7(~FQr68Z@QVE=!|RiD0^3KQxUxc{=s`o(>-A< zmXl@s+>oF5!Z8j7^;z7lpYw`2GnU60pnb}uQewpRyWkDJ)2-7LhE zV=xZ0*#63zWoL6X(iZ8ZrfMHT_mw&QhY@U*3}g*t7yH3hOPDd(dR*R~w^R|%-nzY@ zR~*w@;%B8F1$rT$d3=NNe&v=O#D#TkYoylKxd3pkd(b*Vedz zDq2<{Gua1cxiQcSChvwqGcQvjHwb2Zr8609iWE-;z2KWTNjed|8uJ^mW8r=;X^!qI zRTwKi!GaZ{oz-Ac3wtTO_80MEi>xuyT!7G{t_*ywB#@J4}#1oV5lkWUZo}b?~^Cw(uFENUwCGv&w_wLB0x5JE6o z_;r>N>({9+0%w{I-3`uj9Wv1B**7E0jAWYQqXbZm#-d$nRKABgyFypI&L zs4$)u^z14imbWd!-E0u~)j<+%Tgh8FCTkiGQqEQR9E9P#zWN=%#3|j% zQID(VC(avE@vcqdS11#_0+`6q1*8KG3n33s%f(Q%*>4f(x782EC^7TPq!$c-D|~|k z3&rfW1`J)w$Ylh3wfDnzmaV6}H7oeupfDzAz`(0wO;O~%NN$Ei%e^foGIDLrf(lW{LS_|IRy`OVIPbJ}k^c@q;lwl5z14|2F z(v|YON5-9sYpeNF@8h6RRqY#3sm%&OF4{Ju3k-Z-BXn2`#n-)uuVpt19O^hiQL!F9 zWt%Eo>!(tV)X6p&1q#VdG$m>UCOA3x1r{NjS0&cvkQ4Y#6)`ch_b4{jjpO4SH8Uvi zQbX1VbHqkOzcne z8xfgfDUl@Ps%D_$*A!+R=lvW2&obHdoE>FV`duJ6Qs~n8w+Ycyz z*Ozs}Lbu%CCKsiS7I1Gf}4nHxupZ4$-{aY~ScomqnYeMW8_O^UNtDNZRV)pT9alG2 z>&ec}>5UVlfcdIM!-3Usz8m2fi-45OLL+cd^kK(CEvxYr$nGmKmJvih&qg{asP;Dv zHQ@}9&3N}8^r+oyve{HLW_4HbxSeBmiOJ(*)sG$)Ota27>JG;hrAvPW zuhr+@092$#OL@VBDuvuku;0+aHMX)0HXUvmguDOI4e^ zkQYi&>8$E?L|iTlPo@ZCcU+Mf3|7mk|H-Du$DbXn)ZLp*BNZ9r9?srBX@TfNy&*o; z%lm7zEWd`P+HH(fq=E}~@wq$7GnqCJp+`27Z=}g@lebn;FA>3X#mNN=?h`dq(w?^# zt-}7h6|au^^Ptbwv;vCG$&BMcMghX-nnhU2j2h*iBSlDE`wN z%m0!<&@50!5lmKbImnV4U~PYgj%Xx|f;WUVWAiQg?Tm!rAJuOE&aEO^%Izfg7X~9& zJ7jht7V28Wj8~w*`<5r$IghoNd42`1g;9L)1;%+bCIMAL$&3+3bjWM&613 zD*;^VO`@0U^7!00@8kzAMjc6*mIVzIvbHFm8=Nnx53_sg5sj4+=?7zEhhr=KkV{3G zr6))f$MO-?j50!^x4UmO4smD2re1+d{SuitF2bgMsyIm^Fu7vdl&hs5EM&+Cc?q%& z_$eean1vGAg?voV!}}Cu${KHLtZ3&hKJ;=GAsD;Tew9==P3*{;p_;pp=(k@APs zwzJGdh1!6832<6?+C>`224lNyFQ3cXZc7T}EJ-Sq<{6Hfy&v?TqSPcgCp7#_wCAmZ;wW& zXWosz0w^N4Vg}CJ|M9yHOVk=am|LjeboKqGRi8xe1?o5T0Mn>QCgu`4FL=T4dIc;F zi#>imasQhxu4ha17" +note_002: + id: 2 + subject: Note 2 subject + content: Note 2 _content_ with wiki *syntax* + source_id: 1 + source_type: Contact + author_id: 2 + created_on: "<%= (Time.now - 2.days).to_s(:db) %>" +note_003: + id: 3 + subject: Note 3 subject + content: Note 3 _content_ with wiki *syntax* + source_id: 2 + source_type: Contact + author_id: 1 + created_on: "<%= (Time.now - 1.days).to_s(:db) %>" +note_004: + id: 4 + subject: Note 4 subject + content: Note 4 _content_ with wiki *syntax* + source_id: 3 + source_type: Contact + author_id: 2 + created_on: "<%= (Time.now + 4.days).to_s(:db) %>" +note_005: + id: 5 + subject: Note 5 subject + content: Note 5 _content_ with wiki *syntax* + source_id: 1 + source_type: Deal + author_id: 2 + created_on: "<%= (Time.now + 4.days).to_s(:db) %>" diff --git a/plugins/redmine_contacts/test/fixtures/queries.yml b/plugins/redmine_contacts/test/fixtures/queries.yml new file mode 100644 index 0000000..bea0dff --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/queries.yml @@ -0,0 +1,82 @@ +contact_queries_002: + id: 2 + project_id: 1 + <% if Redmine::VERSION.to_s < '2.4' %> + is_public: false + <% else %> + visibility: 2 + <% end %> + type: ContactQuery + name: Private contacts query for cookbook + filters: | + --- + first_name: + :values: + - "Ivan" + :operator: "=" + last_name: + :values: + - "Ivanov" + :operator: "=" + + user_id: 3 +contact_queries_003: + id: 3 + project_id: + <% if Redmine::VERSION.to_s < '2.4' %> + is_public: false + <% else %> + visibility: 2 + <% end %> + type: ContactQuery + name: Private query for all projects + filters: | + --- + first_name: + :values: + - "Ivan" + :operator: "=" + + user_id: 3 +contact_queries_004: + id: 4 + project_id: + <% if Redmine::VERSION.to_s < '2.4' %> + is_public: true + <% else %> + visibility: 0 + <% end %> + type: ContactQuery + name: Public query for all projects + filters: | + --- + first_name: + :values: + - "Ivan" + :operator: "=" + + user_id: 2 +contact_queries_005: + id: 5 + project_id: + <% if Redmine::VERSION.to_s < '2.4' %> + is_public: true + <% else %> + visibility: 0 + <% end %> + type: ContactQuery + name: Open contacts by tags + filters: | + --- + tags: + :values: + - "main" + :operator: "=" + + user_id: 1 + sort_criteria: | + --- + - - first_name + - desc + - - last_name + - asc \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/recently_vieweds.yml b/plugins/redmine_contacts/test/fixtures/recently_vieweds.yml new file mode 100644 index 0000000..c6d6d40 --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/recently_vieweds.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 + viewed_id: 1 + viewer_id: 1 +two: + id: 2 + viewed_id: 1 + viewer_id: 1 \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/taggings.yml b/plugins/redmine_contacts/test/fixtures/taggings.yml new file mode 100644 index 0000000..418a3be --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/taggings.yml @@ -0,0 +1,30 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +tagging_001: + tag_id: 1 + taggable_id: 1 + taggable_type: Contact + # context: tags + +tagging_002: + tag_id: 2 + taggable_id: 2 + taggable_type: Contact + # context: tags + +tagging_003: + tag_id: 1 + taggable_id: 3 + taggable_type: Contact + # context: tags + +tagging_004: + tag_id: 2 + taggable_id: 4 + taggable_type: Contact + # context: tags + +tagging_005: + tag_id: 2 + taggable_id: 3 + taggable_type: Contact + # context: tags \ No newline at end of file diff --git a/plugins/redmine_contacts/test/fixtures/tags.yml b/plugins/redmine_contacts/test/fixtures/tags.yml new file mode 100644 index 0000000..41633fc --- /dev/null +++ b/plugins/redmine_contacts/test/fixtures/tags.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +tag_one: + id: 1 + name: main +tag_two: + id: 2 + name: test \ No newline at end of file diff --git a/plugins/redmine_contacts/test/functional/auto_completes_controller_test.rb b/plugins/redmine_contacts/test/functional/auto_completes_controller_test.rb new file mode 100644 index 0000000..487718d --- /dev/null +++ b/plugins/redmine_contacts/test/functional/auto_completes_controller_test.rb @@ -0,0 +1,148 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class AutoCompletesControllerTest < ActionController::TestCase + fixtures :projects, :issues, :issue_statuses, + :enumerations, :users, :issue_categories, + :trackers, + :projects_trackers, + :roles, + :member_roles, + :members, + :enabled_modules, + :workflows, + :journals, :journal_details + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + @request.session[:user_id] = 1 + end + + def test_contacts_should_not_be_case_sensitive + compatible_request :get, :contacts, :project_id => 'ecookbook', :q => 'ma' + assert_response :success + assert response.body.match /Marat/ + end + + def test_contacts_should_accept_term_param + compatible_request :get, :contacts, :project_id => 'ecookbook', :term => 'ma' + assert_response :success + assert response.body.match /Marat/ + end + + def test_companies_should_not_be_case_sensitive + compatible_request :get, :companies, :project_id => 'ecookbook', :q => 'domo' + assert_response :success + assert response.body.match /Domoway/ + end + + def test_companies_witth_spaces_should_be_found + compatible_request :get, :companies, :project_id => 'ecookbook', :q => 'my c' + assert_response :success + assert response.body.match /My company/ + end + + def test_contacts_should_return_json + compatible_request :get, :contacts, :project_id => 'ecookbook', :q => 'marat' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + contact = json.last + assert_kind_of Hash, contact + assert_equal 2, contact['id'] + assert_equal 2, contact['value'] + assert_equal 'Marat Aminov', contact['name'] + end + + def test_companies_should_return_json + compatible_request :get, :companies, :project_id => 'ecookbook', :q => 'domo' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + contact = json.first + assert_kind_of Hash, contact + assert_equal 3, contact['id'] + assert_equal 'Domoway', contact['value'] + assert_equal 'Domoway', contact['label'] + end + + def test_contact_tags_should_return_json + compatible_request :get, :contact_tags, :q => 'ma' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + tag = json.last + assert_match 'main', tag + end + + def test_taggable_tags_should_return_json + compatible_request :get, :taggable_tags, :q => 'ma', :taggable_type => 'contact' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + tag = json.last + assert_match 'main', tag + end + def test_deals_should_return_json + compatible_request :get, :deals, :q => 'redmine' + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_kind_of Array, json + deal = json.last + assert_kind_of Hash, deal + assert_equal 3, deal['id'] + assert_equal 3, deal['value'] + assert_match 'Delevelop redmine plugin', deal['label'] + end + + def test_deals_should_fiend_by_contact_details + check_by_contact_details('', 5, 2, 'Second deal with contacts') # Search string is empty + check_by_contact_details('Ivanov', 1, 1, 'First deal with contacts') # Contact last name is Ivanov + check_by_contact_details('jsmith@somenet.foo', 0) # Contact email is jsmith@somenet.foo + check_by_contact_details('Domoway', 4, 2) # Contact first name is Domoway + end + + private + + def check_by_contact_details(search_string, expected_number, deal_id = 0, deal_label = '') + compatible_request :get, :deals, :q => search_string + assert_response :success + json = ActiveSupport::JSON.decode(response.body) + assert_equal expected_number, json.length + + return if expected_number == 0 + deal = json.last + %w(id value).each { |field| assert_equal deal_id, deal[field] } + %w(label text).each { |field| assert_match deal_label, deal[field] } + end +end diff --git a/plugins/redmine_contacts/test/functional/contact_imports_controller_test.rb b/plugins/redmine_contacts/test/functional/contact_imports_controller_test.rb new file mode 100644 index 0000000..9df2488 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contact_imports_controller_test.rb @@ -0,0 +1,158 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + + +require File.expand_path('../../test_helper', __FILE__) + +class ContactImportsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @csv_file = Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'contacts_cf.csv', 'text/csv') + end + + test 'should open contact import form' do + @request.session[:user_id] = 1 + compatible_request :get, :new, :project_id => 1 + assert_response :success + if Redmine::VERSION.to_s >= '3.2' + assert_select 'form input#file' + else + assert_select 'form.new_contact_import' + end + end + + test 'should create new import object' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + compatible_request :get, :create, :project_id => 1, :file => @csv_file + assert_response :redirect + assert_equal Import.last.class, ContactKernelImport + assert_equal Import.last.user, User.find(1) + assert_equal Import.last.project, 1 + assert_equal Import.last.settings, { 'project' => 1, + 'separator' => Rails.version >= '5.1' ? ';' : ',', + 'wrapper' => "\"", + 'encoding' => 'ISO-8859-1', + 'date_format' => '%m/%d/%Y' } + end + end + + test 'should open settings page' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + import = ContactKernelImport.new + import.user = User.find(1) + import.project = Project.find(1) + import.file = @csv_file + import.save! + compatible_request :get, :settings, :id => import.filename, :project_id => 1 + assert_response :success + assert_select 'form#import-form' + end + end + + test 'should show mapping page' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + import = ContactKernelImport.new + import.user = User.find(1) + import.project = Project.find(1) + import.file = @csv_file + import.save! + compatible_request :get, :mapping, :id => import.filename, :project_id => 1 + assert_response :success + assert_select "select[name='import_settings[mapping][is_company]']" + assert_select 'select[name="import_settings[mapping][first_name]"]' + assert_select 'table.sample-data tr' + assert_select 'table.sample-data tr td', 'Monica' + assert_select 'table.sample-data tr td', 'ivan@mail.com' + end + end + + test 'should successfully import from CSV with new import' do + if Redmine::VERSION.to_s >= '3.2' + cf = ContactCustomField.create!(:name => 'LIST_FIELD', :field_format => 'list', :multiple => true, :possible_values => %w(1 2 3)) + @request.session[:user_id] = 1 + import = ContactKernelImport.new + import.user = User.find(1) + import.project = Project.find(1) + import.file = @csv_file + import.save! + compatible_request :post, :mapping, :id => import.filename, :project_id => 1, + :import_settings => { :mapping => { :first_name => 2, :email => 8, "cf_#{cf.id}" => 21 } } + assert_response :redirect + compatible_request :post, :run, :id => import.filename, :project_id => 1, :format => :js + assert_equal Contact.last.first_name, 'Monica' + assert_equal Contact.last.email, 'ivan@mail.com' + assert_equal Contact.last.custom_field_value(cf).sort, ['1', '3'] + end + end + + test 'should successfully import from CSV' do + if Redmine::VERSION.to_s < '3.2' + @request.session[:user_id] = 1 + assert_difference('Contact.count', 4, 'Should add 4 contacts to the database') do + compatible_request :post, :create, { + :project_id => 1, + :contact_import => { + :file => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'correct.csv', 'text/comma-separated-values'), + :quotes_type => '"' + } + } + assert_redirected_to project_contacts_path(:project_id => 1) + end + end + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_controller_test.rb new file mode 100644 index 0000000..3871ca5 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_controller_test.rb @@ -0,0 +1,753 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class ContactsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries, + :addresses]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_get_index + @request.session[:user_id] = 1 + assert_not_nil Contact.find(1) + compatible_request :get, :index + assert_response :success + assert_not_nil contacts_in_list + assert_select 'a', :html => /Domoway/ + assert_select 'a', :html => /Marat/ + assert_select 'h3', :html => /Tags/ + assert_select 'h3', :html => /Recently viewed/ + assert_select 'div#tags span#single_tags span.tag-label-color a', 'test' + assert_select 'div#tags span#single_tags span.tag-label-color a', 'main' + end + + test 'should get index in project' do + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + compatible_request :get, :index, :project_id => 1 + assert_response :success + assert_not_nil contacts_in_list + assert_select 'a', :html => /Domoway/ + assert_select 'a', :html => /Marat/ + assert_select 'h3', :html => /Tags/ + assert_select 'h3', :html => /Recently viewed/ + end + test 'should get index with filters and sorting' do + field = ContactCustomField.create!(:name => 'Test custom field', :is_filter => true, :field_format => 'string') + contact = Contact.find(1) + contact.custom_field_values = { field.id => "This is custom значение" } + contact.save + + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + compatible_request :get, :index, :sort => 'assigned_to,cf_1,last_name,first_name', + :v => { 'first_name' => ['Ivan'] }, + :f => ['first_name', ''], + :op => { 'first_name' => '~' } + assert_response :success + assert_not_nil contacts_in_list + + assert_select 'div#content div#contact_list table.contacts td.name h1', 'Ivan Ivanov' + end + + def test_get_index_with_all_fields + @request.session[:user_id] = 1 + compatible_request :get, + :index, + :set_filter => 1, + :project_id => 1, + :c => ContactQuery.available_columns.map(&:name), + :contacts_list_style => 'list' + assert_response :success + assert_select 'tr#contact-1 td.id a[href=?]', '/contacts/1' + assert_select 'tr#contact-1 td.tags', 'main' + end + + def test_index_with_short_filters + @request.session[:user_id] = 1 + to_test = { + 'tags' => { + 'main|test' => { :op => '=', :values => ['main', 'test'] }, + '=main' => { :op => '=', :values => ['main'] }, + '!test' => { :op => '!', :values => ['test'] } }, + 'country' => { + '*' => { :op => '*', :values => [''] }, + '!*' => { :op => '!*', :values => [''] }, + 'US|RU' => { :op => '=', :values => ['US', 'RU'] } }, + 'first_name' => { + 'Marat' => { :op => '=', :values => ['Marat'] }, + '~Mara' => { :op => '~', :values => ['Mara'] }, + '!~Mara' => { :op => '!~', :values => ['Mara'] } }, + 'created_on' => { + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + ' { :op => ' ['2'] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] } }, + 'last_note' => { + '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] }, + ' { :op => ' ['2'] }, + '>t-2' => { :op => '>t-', :values => ['2'] }, + 't-2' => { :op => 't-', :values => ['2'] } }, + 'has_deals' => { + 'c' => { :op => '=', :values => ['c'] }, + '!c' => { :op => '!', :values => ['c'] } }, + 'has_open_issues' => { + '=4' => { :op => '=', :values => ['4'] }, + '!*' => { :op => '!*', :values => [''] }, + '*' => { :op => '*', :values => [''] } } + } + + default_filter = { 'status_id' => { :operator => 'o', :values => [''] } } + + to_test.each do |field, expression_and_expected| + expression_and_expected.each do |filter_expression, expected| + compatible_request :get, :index, :set_filter => 1, field => filter_expression + + assert_response :success + assert_not_nil contacts_in_list + end + end + end + + def test_filter_by_ids_equal + @request.session[:user_id] = 1 + ids = [1, 2] + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '=' }, 'v' => { 'ids' => [ids.join(',')] } + assert_response :success + assert_equal ids.sort, contacts_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_any + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '*' } + assert_response :success + assert_equal Project.find(1).contacts.map(&:id).sort, contacts_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_more_than + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '>=' }, 'v' => { 'ids' => [3] } + assert_response :success + assert_equal [3, 4, 5], contacts_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_less_than + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '<=' }, 'v' => { 'ids' => [2] } + assert_response :success + assert_equal [1, 2], contacts_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_between + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '><' }, 'v' => { 'ids' => ['1', '3'] } + assert_response :success + assert_equal [1, 2, 3], contacts_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_index_sort_by_custom_field + @request.session[:user_id] = 1 + cf = ContactCustomField.create!(:name => 'Contact test cf', :is_for_all => true, :field_format => 'string') + CustomValue.create!(:custom_field => cf, :customized => Contact.find(1), :value => 'test_1') + CustomValue.create!(:custom_field => cf, :customized => Contact.find(2), :value => 'test_2') + CustomValue.create!(:custom_field => cf, :customized => Contact.find(3), :value => 'test_3') + + compatible_request :get, :index, :set_filter => 1, :sort => "cf_#{cf.id},id" + assert_response :success + + assert_equal [1, 2, 3], contacts_in_list.select { |contact| contact.custom_field_value(cf).present? }.map(&:id).sort + end + + def test_should_not_absolute_links + @request.session[:user_id] = 1 + + compatible_request :get, :index + assert_response :success + assert_no_match %r{localhost}, @response.body + end + + def test_should_get_index_deny_user_in_project + @request.session[:user_id] = 5 + + compatible_request :get, :index, :project_id => 1 + assert_response :redirect + end + + def test_should_get_index_with_filters + @request.session[:user_id] = 1 + compatible_request :get, :index, :is_company => ActiveRecord::Base.connection.quoted_true.gsub(/'/, '') + assert_response :success + assert_select 'div#content div#contact_list table.contacts td.name h1 a', 'Domoway' + end + def test_should_get_index_as_csv + field = ContactCustomField.create!(:name => 'Test custom field', :is_filter => true, :field_format => 'string') + contact = Contact.find(1) + contact.custom_field_values = { field.id => "This is custom значение" } + contact.save + + @request.session[:user_id] = 1 + compatible_request :get, :index, :format => 'csv' + assert_response :success + assert_not_nil contacts_in_list + assert_equal 'text/csv; header=present', @response.content_type + assert_match /Domoway/, @response.body + end + + def test_should_get_index_as_VCF + @request.session[:user_id] = 1 + compatible_request :get, :index, :format => 'vcf' + assert_response :success + assert_not_nil contacts_in_list + assert_equal 'text/x-vcard', @response.content_type + assert @response.body.starts_with?('BEGIN:VCARD') + assert_match /^N:;Domoway/, @response.body + end + + def test_should_get_contacts_notes_as_csv + @request.session[:user_id] = 1 + compatible_request :get, :contacts_notes, :format => 'csv' + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + assert @response.body.starts_with?('#,') + end + + def test_get_show + @request.session[:user_id] = 2 + Setting.default_language = 'en' + + compatible_request :get, :show, :id => 3, :project_id => 1 + assert_response :success + + assert_not_nil contacts_in_list + assert_select 'h1', :html => /Domoway/ + assert_select 'div#tags_data span.tag-label-color a', 'main' + assert_select 'div#tags_data span.tag-label-color a', 'test' + assert_select 'div#tab-placeholder-contacts' + assert_select 'div#comments div#notes table.note_data td.name h4', 4 + assert_select 'h3', 'Recently viewed' + end + + def test_get_show_with_long_note + long_note = 'A' * 1500 + Contact.find(3).notes.create(:content => long_note, :author_id => 1) + @request.session[:user_id] = 2 + Setting.default_language = 'en' + + compatible_request :get, :show, :id => 3, :project_id => 1 + assert_response :success + assert_select '.note a', '(read more)' + end + def test_get_show_tab_deals + @request.session[:user_id] = 2 + Setting.default_language = 'en' + + compatible_request :get, :show, :id => 3, :project_id => 1, :tab => 'deals' + assert_response :success + assert_not_nil contacts_in_list + assert_select 'h1', :html => /Domoway/ + assert_select 'div#deals a', 'Delevelop redmine plugin' + assert_select 'div#deals a', 'Second deal with contacts' + end + + def test_get_show_without_deals + @request.session[:user_id] = 4 + Setting.default_language = 'en' + + compatible_request :get, :show, :id => 3, :project_id => 1, :tab => 'deals' + assert_response :success + assert_not_nil contacts_in_list + + assert_select 'div#deals a', { :count => 0, :text => /Delevelop redmine plugin/ } + assert_select 'div#deals a', { :count => 0, :text => /Second deal with contacts/ } + end + + def test_get_new + @request.session[:user_id] = 2 + compatible_request :get, :new, :project_id => 1 + assert_response :success + assert_select 'input#contact_first_name' + end + + def test_get_new_without_permission + @request.session[:user_id] = 4 + compatible_request :get, :new, :project_id => 1 + assert_response :forbidden + end + + def test_post_create + @request.session[:user_id] = 1 + assert_difference 'Contact.count' do + compatible_request :post, :create, :project_id => 1, :contact => { :company => 'OOO "GKR"', + :is_company => 0, + :job_title => 'CFO', + :assigned_to_id => 3, + :tag_list => 'test,new', + :last_name => 'New', + :middle_name => 'Ivanovich', + :first_name => 'Created' } + end + + assert_redirected_to :controller => 'contacts', :action => 'show', :id => Contact.last.id, :project_id => Contact.last.project + + contact = Contact.where(:first_name => 'Created', :last_name => 'New', :middle_name => 'Ivanovich').first + assert_not_nil contact + assert_equal 'CFO', contact.job_title + assert_equal ['new', 'test'], contact.tag_list.sort + assert_equal 3, contact.assigned_to_id + end + def test_post_create_with_custom_fields + field = ContactCustomField.create!(:name => 'Test', :is_filter => true, :field_format => 'string') + @request.session[:user_id] = 1 + assert_difference 'Contact.count' do + compatible_request :post, :create, :project_id => 1, :contact => { :company => 'OOO "GKR"', + :is_company => 0, + :job_title => 'CFO', + :assigned_to_id => 3, + :tag_list => 'test,new', + :last_name => 'New', + :middle_name => 'Ivanovich', + :first_name => 'Created', + :custom_field_values => { "#{field.id}" => 'contact one' } } + end + assert_redirected_to :controller => 'contacts', :action => 'show', :id => Contact.last.id, :project_id => Contact.last.project + + contact = Contact.where(:first_name => 'Created', :last_name => 'New', :middle_name => 'Ivanovich').first + assert_equal 'contact one', contact.custom_field_values.last.value + end + + def test_post_create_without_permission + @request.session[:user_id] = 4 + compatible_request :post, :create, :project_id => 1, :contact => { :company => 'OOO "GKR"', + :is_company => 0, + :job_title => 'CFO', + :assigned_to_id => 3, + :tag_list => 'test,new', + :last_name => 'New', + :middle_name => 'Ivanovich', + :first_name => 'Created' } + assert_response :forbidden + end + + def test_get_edit + @request.session[:user_id] = 1 + compatible_request :get, :edit, :id => 1 + assert_response :success + assert_select 'h2', /Editing Contact Information/ + end + + def test_get_edit_with_duplicates + contact = Contact.find(3) + contact_clone = contact.dup + contact_clone.project = contact.project + contact_clone.save! + + @request.session[:user_id] = 2 + Setting.default_language = 'en' + + compatible_request :get, :edit, :id => 3 + assert_response :success + assert_select 'div#duplicates', 1 + assert_select 'div#duplicates h3', /Possible duplicates/ + ensure + contact_clone.delete + end + + def test_put_update + @request.session[:user_id] = 1 + + contact = Contact.find(1) + new_firstname = 'Fist name modified by ContactsControllerTest#test_put_update' + + compatible_request :put, :update, :id => 1, :project_id => 1, :contact => { :first_name => new_firstname } + assert_redirected_to :action => 'show', :id => '1', :project_id => 1 + contact.reload + assert_equal new_firstname, contact.first_name + end + + def test_post_destroy + @request.session[:user_id] = 1 + compatible_request :post, :destroy, :id => 1, :project_id => 'ecookbook' + assert_redirected_to :action => 'index', :project_id => 'ecookbook' + assert_equal 0, Contact.where(:id => [1]).count + end + + def test_post_bulk_destroy + @request.session[:user_id] = 1 + + compatible_request :post, :bulk_destroy, :ids => [1, 2, 3] + assert_redirected_to :controller => 'contacts', :action => 'index' + + assert_equal 0, Contact.where(:id => [1, 2, 3]).count + end + + def test_post_bulk_destroy_without_permission + @request.session[:user_id] = 4 + assert_raises ActiveRecord::RecordNotFound do + compatible_request :post, :bulk_destroy, :ids => [1, 2] + end + end + def test_bulk_edit_mails + @request.session[:user_id] = 1 + compatible_request :post, :edit_mails, :ids => [1, 2] + assert_response :success + assert_not_nil contacts_in_list + end + + def test_bulk_edit_mails_by_deny_user + @request.session[:user_id] = 4 + compatible_request :post, :edit_mails, :ids => [1, 2] + assert_response 403 + end + + def test_bulk_send_mails_by_deny_user + @request.session[:user_id] = 4 + compatible_request :post, :send_mails, :ids => [1, 2], :message => 'test message', :subject => 'test subject' + assert_response 403 + end + + def test_bulk_send_mails + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 1 + compatible_request :post, :send_mails, :ids => [2], :from => 'test@mail.from', :bcc => 'test@mail.bcc', :"message-content" => "Hello %%NAME%%\ntest message", :subject => 'test subject' + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + assert_match /Hello Marat/, mail.text_part.body.to_s + assert_equal 'test subject', mail.subject + assert_equal 'test@mail.from', mail.from.first + assert_equal 'test@mail.bcc', mail.bcc.first + note = Note.last + assert_equal 'test subject', note.subject + assert_equal note.type_id, Note.note_types[:email] + assert_equal "Hello Marat\ntest message", note.content + end + + def test_post_bulk_edit + @request.session[:user_id] = 1 + compatible_request :post, :bulk_edit, :ids => [1, 2] + assert_response :success + assert_not_nil contacts_in_list + end + + def test_post_bulk_edit_without_permission + @request.session[:user_id] = 4 + assert_raises ActiveRecord::RecordNotFound do + compatible_request :post, :bulk_edit, :ids => [1, 2] + end + end + + def test_put_bulk_update + @request.session[:user_id] = 1 + + compatible_request :put, :bulk_update, :ids => [1, 2], + :add_tag_list => 'bulk, edit, tags', + :delete_tag_list => 'main', + :add_projects_list => ['1', '2', '3'], + :delete_projects_list => ['3', '4', '5'], + :note => { :content => 'Bulk note content' }, + :contact => { :company => 'Bulk company', :job_title => '' } + + assert_redirected_to :controller => 'contacts', :action => 'index', :project_id => nil + contacts = Contact.find([1, 2]) + contacts.each do |contact| + assert_equal 'Bulk company', contact.company + tag_list = contact.tag_list # Need for 4 rails + assert tag_list.include?('bulk') + assert tag_list.include?('edit') + assert tag_list.include?('tags') + assert !tag_list.include?('main') + assert contact.project_ids.include?(1) && contact.project_ids.include?(2) + + assert_equal 'Bulk note content', contact.notes.find_by_content('Bulk note content').content + end + end + + def test_put_bulk_update_without_permission + @request.session[:user_id] = 4 + + compatible_request :put, :bulk_update, :ids => [1, 2], + :add_tag_list => 'bulk, edit, tags', + :delete_tag_list => 'main', + :note => { :content => 'Bulk note content' }, + :contact => { :company => 'Bulk company', :job_title => '' } + assert_response 403 + end + + def test_get_contacts_notes + @request.session[:user_id] = 2 + + compatible_request :get, :contacts_notes + assert_response :success + assert_select 'h2', /All notes/ + assert_select 'div#contacts_notes table.note_data div.note.content.preview', /Note 1/ + end + + def test_get_context_menu + @request.session[:user_id] = 1 + compatible_xhr_request :get, :context_menu, :back_url => '/projects/contacts-plugin/contacts', :project_id => 'ecookbook', :ids => ['1', '2'] + assert_response :success + end + + def test_post_index_with_search + @request.session[:user_id] = 1 + compatible_xhr_request :post, :index, :search => 'Domoway' + assert_response :success + assert_match 'contacts?search=Domoway', response.body + assert_select 'a', :html => /Domoway/ + end + + def test_post_index_with_search_in_project + @request.session[:user_id] = 1 + compatible_xhr_request :post, :index, :search => 'Domoway', :project_id => 'ecookbook' + assert_response :success + assert_match 'contacts?search=Domoway', response.body + assert_select 'a', :html => /Domoway/ + end + + def test_post_contacts_notes_with_search + @request.session[:user_id] = 1 + compatible_xhr_request :post, :contacts_notes, :search_note => 'Note 1' + assert_response :success + assert_match 'note_data', response.body + assert_select 'table.note_data div.note.content.preview', /Note 1/ + assert_select 'table.note_data div.note.content.preview', { :count => 0, :text => /Note 2/ } + end + + def test_post_contacts_notes_with_search_in_project + @request.session[:user_id] = 1 + compatible_xhr_request :post, :contacts_notes, :search_note => 'Note 2', :project_id => 'ecookbook' + assert_response :success + assert_match 'note_data', response.body + assert_select 'table.note_data div.note.content.preview', /Note 2/ + end + def test_should_have_import_csv_link_if_authorized_to + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1 + assert_response :success + assert_select 'a#import_from_csv' + end + + def test_should_not_have_import_csv_link_if_unauthorized + @request.session[:user_id] = 4 + compatible_request :get, :index, :project_id => 1 + assert_response :success + assert_select 'a#import_from_csv', false, 'Should not see CSV import link' + end + + def test_index_should_omit_page_param_in_csv_export_link + @request.session[:user_id] = 1 + compatible_request :get, :index, :page => 2 + assert_response :success + assert_select 'a.csv[href=?]', '/contacts.csv' + assert_select 'form#csv-export-form[action=?]', '/contacts.csv' + end + + def test_index_should_include_query_params_in_csv_export_form + @request.session[:user_id] = 1 + compatible_request :get, + :index, + {:project_id => 1, + :set_filter => 1, + :has_deals => 1, + :c => ['name', 'job_title'], + :sort => 'name'} + + assert_select '#csv-export-form[action=?]', '/projects/ecookbook/contacts.csv' + assert_select '#csv-export-form[method=?]', 'get' + + assert_select '#csv-export-form' do + assert_select 'input[name=?][value=?]', 'set_filter', '1' + + assert_select 'input[name=?][value=?]', 'f[]', 'has_deals' + assert_select 'input[name=?][value=?]', 'op[has_deals]', '=' + assert_select 'input[name=?][value=?]', 'v[has_deals][]', '1' + + assert_select 'input[name=?][value=?]', 'c[]', 'name' + assert_select 'input[name=?][value=?]', 'c[]', 'job_title' + + assert_select 'input[name=?][value=?]', 'sort', 'name' + end + end if Redmine::VERSION::STRING > '3.2.1' + + def test_index_csv_without_filters + @request.session[:user_id] = 1 + compatible_request :get, + :index, + {:format => 'csv', + :set_filter => 1, + :f => ['']} + assert_response :success + # -1 for headers + lines = @response.body.chomp.lines.count - 1 + assert_equal Contact.count, lines + end if Redmine::VERSION::STRING > '3.3' + + def test_index_csv_with_some_filters + @request.session[:user_id] = 1 + filter = {:job_title => 'CEO'} + params = {:format => 'csv', :set_filter => 1}.merge(filter) + + compatible_request :get, :index, params + assert_response :success + # -1 for headers + lines = @response.body.chomp.lines.count - 1 + assert_equal Contact.where(filter).count, lines + end if Redmine::VERSION::STRING > '3.3' + + def test_index_csv_with_few_columns + @request.session[:user_id] = 1 + columns = ['id', 'name', 'company', 'job_title'] + compatible_request :get, + :index, + :format => 'csv', + :c => columns + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + assert response.body.starts_with?("#,") + + actual_columns = response.body.chomp.lines.first.split(',').count + assert_equal columns.count, actual_columns + end + + def test_index_csv_with_all_available_columns + @request.session[:user_id] = 1 + all_columns = if Redmine::VERSION::STRING < '3.2' + {:columns => 'all'} + elsif Redmine::VERSION::STRING < '3.4' + {:csv => {:columns => 'all'}} + else + {:c => ['all_inline']} + end + params = {:format => 'csv'}.merge(all_columns) + + compatible_request :get, :index, params + assert_response :success + assert_equal 'text/csv; header=present', @response.content_type + assert response.body.starts_with?("#,") + + available_columns = ContactQuery.new.available_columns.count + actual_columns = response.body.chomp.lines.first.split(',').count + assert_equal available_columns, actual_columns + end + + def test_index_with_contacts_as_cards_exports_all_columns + @request.session[:user_id] = 1 + compatible_request :get, :index, :contacts_list_style => 'list_cards' + assert_response :success + assert_select 'a[href^="/contacts.csv"][onclick^=?]', 'showModal', false + end + + def test_index_with_contacts_as_list_allows_to_choose_columns + @request.session[:user_id] = 1 + compatible_request :get, :index, :contacts_list_style => 'list' + assert_response :success + assert_select 'a[href^="/contacts.csv"][onclick^=?]', 'showModal' + end + + def test_index_properly_exports_tags_as_text_in_csv + @request.session[:user_id] = 1 + + contact = Contact.find(1) + contact.tags = [RedmineCrm::Tag.new(:name => 'foo')] + contact.save + + compatible_request :get, + :index, + :format => 'csv', + :c => ['tags'] + assert_response :success + assert_include "foo\n", @response.body.chomp.lines + end + + def test_render_tab_partial_on_load_tab + @request.session[:user_id] = 4 + compatible_xhr_request :get, :load_tab, :id => 3, :tab_name => 'notes', :partial => 'notes', :format => :js + assert_response :success + assert_match 'note_data', response.body + end + + def test_post_create_with_avatar + image = Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/files/image.jpg' + attach = Attachment.create!(:file => Rack::Test::UploadedFile.new(image, 'image/jpeg'), :author => User.find(1)) + + @request.session[:user_id] = 1 + assert_difference 'Contact.count' do + compatible_request :post, :create, :project_id => 1, + :attachments => { '0' => { 'filename' => 'image.jpg', 'description' => 'avatar', 'token' => attach.token } }, + :contact => { :last_name => 'Testov', + :middle_name => 'Test', + :first_name => 'Testovich' } + end + + assert_redirected_to :controller => 'contacts', :action => 'show', :id => Contact.last.id, :project_id => Contact.last.project + assert_equal 'Contact', Attachment.last.container_type + assert_equal Contact.last.id, Attachment.last.container_id + + assert_equal 'image.jpg', Attachment.last.diskfile[/image\.jpg/] + end + + def test_last_notes_for_contact + contact = Contact.find(1) + note = contact.notes.create(:content => 'note for contact', :author_id => 1) + @request.session[:user_id] = 1 + compatible_request :get, :index + assert_response :success + assert_select '.note.content', :text => note.content + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_duplicates_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_duplicates_controller_test.rb new file mode 100644 index 0000000..1378b2b --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_duplicates_controller_test.rb @@ -0,0 +1,103 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) +# require 'contacts_duplicates_controller' + +class ContactsDuplicatesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_get_index_duplicates + contact = Contact.find(3) + contact_clone = contact.dup + contact_clone.project = contact.project + contact_clone.save! + + @request.session[:user_id] = 2 + Setting.default_language = 'en' + + compatible_request :get, :index, :project_id => contact.project, :contact_id => 3 + assert_response :success + assert_select 'ul#contact_duplicates li', 1 + assert_select 'ul#contact_duplicates li a', contact.name + ensure + contact_clone.delete + end + + def test_get_merge_duplicates + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + compatible_request :get, :merge, :project_id => 1, :contact_id => 1, :duplicate_id => 2 + assert_redirected_to :controller => 'contacts', :action => 'show', :id => 2, :project_id => 'ecookbook' + + contact = Contact.find(2) + assert_equal contact.emails, ['marat@mail.ru', 'marat@mail.com', 'ivan@mail.com'] + end + + def test_xhr_get_duplicates + @request.session[:user_id] = 1 + compatible_xhr_request :get, :duplicates, :project_id => 'ecookbook', :contact => { :first_name => 'marat' } + assert_match /Marat Aminov/, @response.body + end + + def test_xhr_get_search + @request.session[:user_id] = 1 + compatible_xhr_request :get, :search, :project_id => 'ecookbook', :contact_id => 2, :q => 'iva' + assert_match /Ivan Ivanov/, @response.body + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_issues_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_issues_controller_test.rb new file mode 100644 index 0000000..3d70b89 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_issues_controller_test.rb @@ -0,0 +1,138 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsIssuesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_create_issue + @request.session[:user_id] = 1 + @request.env['HTTP_REFERER'] = '/contacts/1' + parameters = { :issue => { :subject => 'Test subject', :assigned_to_id => '1', :due_date => Date.today.to_s, :description => 'Test descripiton', :tracker_id => '1' } } + assert_difference('Issue.count') do + assert_difference('ContactsIssue.count') do + compatible_request :post, :create_issue, { :project_id => 1, :id => 1 }.merge!(parameters) + end + end + assert_response 302 + end + + def test_delete + @request.session[:user_id] = 1 + ContactsIssue.create(:contact_id => 1, :issue_id => 1) + assert_difference('ContactsIssue.count', -1) do + compatible_xhr_request :delete, :delete, :project_id => 1, :id => 1, :issue_id => 1 + end + assert_response :success + end + + def test_close + @request.session[:user_id] = 1 + assert_not_nil Issue.find(1) + compatible_xhr_request :post, :close, :issue_id => 1 + assert_response :success + end + + def test_autocomplete_for_contact + @request.session[:user_id] = 1 + compatible_xhr_request :get, :autocomplete_for_contact, :q => 'domo', :issue_id => '1', :project_id => 'ecookbook', :cross_project_contacts => '1' + assert_response :success + assert_select 'input', :count => 1 + if ActiveRecord::VERSION::MAJOR >= 4 + assert_select "input[name='contacts_issue[contact_ids][]'][value='3']" + else + assert_select 'input[name=?][value=3]', 'contacts_issue[contact_ids][]' + end + end + + def test_autocomplete_for_contact_cross_contacts + @request.session[:user_id] = 2 + + compatible_xhr_request :get, :autocomplete_for_contact, :q => 'a', :issue_id => '4', :project_id => 'onlinestore', :cross_project_contacts => '0' + assert_response :success + assert_select 'span.contact', :count => 1 + assert_select 'span.contact', /Ivan Ivanov/ + + compatible_xhr_request :get, :autocomplete_for_contact, :q => 'a', :issue_id => '4', :project_id => 'onlinestore', :cross_project_contacts => '1' + assert_response :success + assert_select 'span.contact', :count => 4 + assert_select 'span.contact', /Domoway/ + assert_select 'span.contact', /Ivan Ivanov/ + assert_select 'span.contact', /Marat Aminov/ + assert_select 'span.contact', /My company/ + end + + def test_new + @request.session[:user_id] = 1 + compatible_xhr_request :get, :new, :issue_id => '1' + assert_response :success + assert_match /ajax-modal/, response.body + end + + def test_create_multiple + @request.session[:user_id] = 1 + assert_difference('ContactsIssue.count', 2) do + compatible_xhr_request :post, :create, :issue_id => '2', :contacts_issue => {:contact_ids => ['3', '4']} + assert_response :success + assert_match /contacts/, response.body + assert_match /ajax-modal/, response.body + end + assert Issue.find(2).contact_ids.include?(3) + assert Issue.find(2).contact_ids.include?(4) + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_mailer_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_mailer_controller_test.rb new file mode 100644 index 0000000..6a69b95 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_mailer_controller_test.rb @@ -0,0 +1,75 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsMailerControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/contacts_mailer' + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_should_create_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + compatible_request :post, :index, :key => 'secret', :email => IO.read(File.join(FIXTURES_PATH, 'fwd_new_note_plain.eml')) + assert_response 201 + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_projects_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_projects_controller_test.rb new file mode 100644 index 0000000..0a40d06 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_projects_controller_test.rb @@ -0,0 +1,94 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsProjectsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :versions, + :trackers, + :projects_trackers, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :time_entries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_delete_destroy + @request.session[:user_id] = 1 + contact = Contact.find(1) + assert_equal 2, contact.projects.size + compatible_xhr_request :delete, :destroy, :project_id => 1, :id => 2, :contact_id => 1 + assert_response :success + assert_include 'contact_projects', response.body + + contact.reload + assert_equal [1], contact.project_ids + end + + def test_delete_destroy_last_project + @request.session[:user_id] = 1 + contact = Contact.find(1) + assert RedmineContacts::TestCase.is_arrays_equal(contact.project_ids, [1, 2]) + compatible_xhr_request :delete, :destroy, :project_id => 1, :id => 2, :contact_id => 1 + assert_response :success + compatible_xhr_request :delete, :destroy, :project_id => 1, :id => 1, :contact_id => 1 + assert_response 403 + + contact.reload + assert_equal [1], contact.project_ids + end + + def test_post_new + @request.session[:user_id] = 1 + + compatible_xhr_request :post, :new, :project_id => 'ecookbook', :id => 2, :contact_id => 2 + assert_response :success + assert_include 'contact_projects', response.body + contact = Contact.find(2) + assert RedmineContacts::TestCase.is_arrays_equal(contact.project_ids, [1, 2]) + end + + def test_post_create_without_permissions + @request.session[:user_id] = 1 + + compatible_xhr_request :post, :create, :project_id => 'project6', :id => 2, :contact_id => 2 + assert_response 403 + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_settings_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_settings_controller_test.rb new file mode 100644 index 0000000..51c364e --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_settings_controller_test.rb @@ -0,0 +1,60 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsSettingsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :versions, + :trackers, + :projects_trackers, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :time_entries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_should_save_setting + @request.session[:user_id] = 1 + compatible_request :post, :save, :project_id => 1, :contacts_settings => { :setting1 => 1, :setting2 => 'Hello' }, :tab => 'contacts' + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'contacts', :id => 'ecookbook' + assert_equal '1', ContactsSetting[:setting1, 1] + assert_equal 'Hello', ContactsSetting[:setting2, 1] + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_tags_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_tags_controller_test.rb new file mode 100644 index 0000000..58d708f --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_tags_controller_test.rb @@ -0,0 +1,112 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsTagsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @request.env['HTTP_REFERER'] = '/' + end + + def test_should_get_edit + @request.session[:user_id] = 1 + compatible_request :get, :edit, :id => 1 + assert_response :success + assigned_tag = css_select('#tag_name').map { |tag| tag['value'] }.join + assert_not_nil assigned_tag + assert_equal RedmineCrm::Tag.find(1).name, assigned_tag + end + + def test_should_put_update + @request.session[:user_id] = 1 + tag1 = RedmineCrm::Tag.find(1) + new_name = 'updated main' + compatible_request :put, :update, :id => 1, :tag => { :name => new_name, :color_name => '#000000' } + assert_redirected_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => 'tags' + tag1.reload + assert_equal new_name, tag1.name + end + + def test_should_delete_destroy + @request.session[:user_id] = 1 + assert_difference 'RedmineCrm::Tag.count', -1 do + compatible_request :post, :destroy, :id => 1 + assert_response 302 + end + end + + def test_should_get_merge + @request.session[:user_id] = 1 + tag1 = RedmineCrm::Tag.find(1) + tag2 = RedmineCrm::Tag.find(2) + compatible_request :get, :merge, :ids => [tag1.id, tag2.id] + assert_response :success + merged_tags = css_select('.tag_list a').map { |tag| tag.to_s.to_s[/.*>(.+?)<\/a>/, 1] } + assert_equal 2, merged_tags.size + end + + def test_should_post_merge + @request.session[:user_id] = 1 + tag1 = RedmineCrm::Tag.find(1) + tag2 = RedmineCrm::Tag.find(2) + assert_difference 'RedmineCrm::Tag.count', -1 do + compatible_request :post, :merge, :ids => [tag1.id, tag2.id], :tag => { :name => 'main' } + assert_redirected_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => 'tags' + end + assert_equal 0, Contact.tagged_with('test').count + assert_equal 4, Contact.tagged_with('main').count # added one more tagging for tag2 + end +end diff --git a/plugins/redmine_contacts/test/functional/contacts_vcf_controller_test.rb b/plugins/redmine_contacts/test/functional/contacts_vcf_controller_test.rb new file mode 100644 index 0000000..c3b940e --- /dev/null +++ b/plugins/redmine_contacts/test/functional/contacts_vcf_controller_test.rb @@ -0,0 +1,107 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class ContactsVcfControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + end + + def test_load_from_vcard + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + compatible_request :post, :load, { + :project_id => 1, + :contact_vcf => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'kirill_bezrukov.vcf', 'text/x-vcard') + } + + assert_redirected_to new_project_contact_path(:project_id => 'ecookbook', :contact => { + 'background' => 'ТекÑтовое опиÑание на РуÑÑком', + 'birthday' => '1981-05-12', + 'email' => 'kirill@gmail.com', + 'first_name' => 'Кирилл', + 'job_title' => '', + 'address_attributes' => { 'city' => 'МоÑква', 'postcode' => '', 'region' => '', 'street1' => 'Миклухи МаклаÑ, 9 к 2, ÐºÐ¾Ñ€Ð¿ÑƒÑ Ð‘' }, + 'last_name' => 'Безруков', + 'middle_name' => '', + 'phone' => '+1 (234) 234-11-33' + }) + end + + def test_load_from_vcard_with_umlauts + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + compatible_request :post, :load, { + :project_id => 1, + :contact_vcf => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'umlaut_card.vcf', 'text/x-vcard') + } + + assert_redirected_to new_project_contact_path(:project_id => 'ecookbook', :contact => { + 'address_attributes' => { 'city' => 'Düsseldorf', 'postcode' => '11111 ', 'region' => 'Nordrhein-Westfalen', 'street1' => 'Bleichstraße' }, + 'background' => 'Test note', + 'birthday' => '1980-12-01', + 'company' => 'Geschäftszweig', + 'email' => 't.test@test.com', + 'first_name' => 'Test', + 'job_title' => 'Tester', + 'last_name' => 'Testovish', + 'middle_name' => '', + 'phone' => '+11-111-111111-11111' + }) + end +end diff --git a/plugins/redmine_contacts/test/functional/crm_queries_controller_test.rb b/plugins/redmine_contacts/test/functional/crm_queries_controller_test.rb new file mode 100644 index 0000000..351948c --- /dev/null +++ b/plugins/redmine_contacts/test/functional/crm_queries_controller_test.rb @@ -0,0 +1,138 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class CrmQueriesControllerTest < ActionController::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :enumerations, :issues + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @request.session[:user_id] = 1 + end + + def test_get_new_project_query + compatible_request :get, :new, :project_id => 1, :object_type => 'contact' + assert_response :success + att = { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => nil, + :disabled => nil } + assert_select 'input', :attributes => att + end + + def test_get_new_global_query + compatible_request :get, :new, :object_type => 'contact' + assert_response :success + att = { :type => 'checkbox', + :name => 'query_is_for_all', + :checked => 'checked', + :disabled => nil } + assert_select 'input', :attributes => att + end + + def test_post_create_project_public_query + if Redmine::VERSION.to_s < '2.4' + query_params = { 'name' => 'test_new_project_public_contacts_query', 'is_public' => '1' } + else + query_params = { 'name' => 'test_new_project_public_contacts_query', 'visibility' => '2' } + end + + compatible_request :post, :create, + :project_id => 'ecookbook', + :object_type => 'contact', + :default_columns => '1', + :f => ['first_name', 'last_name'], + :op => { 'first_name' => '=', 'last_name' => '=' }, + :v => { 'first_name' => ['Ivan'], 'last_name' => ['Ivanov'] }, + :query => query_params + + q = ContactQuery.find_by_name('test_new_project_public_contacts_query') + assert_redirected_to :controller => 'contacts', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_post_create_project_private_query + + if Redmine::VERSION.to_s < '2.4' + query_params = { 'name' => 'test_new_project_public_contacts_query', 'is_public' => '0' } + else + query_params = { 'name' => 'test_new_project_public_contacts_query', 'visibility' => '0' } + end + + compatible_request :post, :create, + :project_id => 'ecookbook', + :object_type => 'contact', + :default_columns => '1', + :f => ['first_name', 'last_name'], + :op => { 'first_name' => '=', 'last_name' => '=' }, + :v => { 'first_name' => ['Ivan'], 'last_name' => ['Ivanov'] }, + :query => query_params + + q = ContactQuery.find_by_name('test_new_project_public_contacts_query') + assert_redirected_to :controller => 'contacts', :action => 'index', :project_id => 'ecookbook', :query_id => q + assert !q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_put_update_global_public_query + + if Redmine::VERSION.to_s < '2.4' + query_params = { 'name' => 'test_edit_global_public_query', 'is_public' => '1' } + else + query_params = { 'name' => 'test_edit_global_public_query', 'visibility' => '2' } + end + + compatible_request :put, :update, + :id => 2, + :object_type => 'contact', + :default_columns => '1', + :fields => ['first_name', 'last_name'], + :operators => { 'first_name' => '=', 'last_name' => '=' }, + :values => { 'first_name' => ['Ivan'], 'last_name' => ['Ivanov'] }, + :query => query_params + + assert_redirected_to :controller => 'contacts', :action => 'index', :project_id => 'ecookbook', :query_id => 2 + q = ContactQuery.find_by_name('test_edit_global_public_query') + assert q.is_public? + assert q.has_default_columns? + assert q.valid? + end + + def test_delete_destroy + compatible_request :delete, :destroy, :id => 2, :object_type => 'contact' + assert_redirected_to :controller => 'contacts', :action => 'index', :project_id => 'ecookbook', :set_filter => 1, :query_id => nil + assert_nil Query.find_by_id(2) + end +end diff --git a/plugins/redmine_contacts/test/functional/deal_categories_controller_test.rb b/plugins/redmine_contacts/test/functional/deal_categories_controller_test.rb new file mode 100644 index 0000000..06da535 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/deal_categories_controller_test.rb @@ -0,0 +1,120 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class DealCategoriesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :deal_statuses, + :deal_categories, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @request.session[:user_id] = 1 + end + + def test_get_new + @request.session[:user_id] = 1 + compatible_request :get, :new, :project_id => 1 + assert_response :success + end + + def test_get_edit + @request.session[:user_id] = 1 + compatible_request :get, :edit, :id => 1 + assert_response :success + category_name = css_select('#category_name').map { |tag| tag['value'] }.join + assert_not_nil category_name + assert_equal DealCategory.find(1).name, category_name + end + + def test_put_update + @request.session[:user_id] = 1 + category1 = DealCategory.find(1) + new_name = 'updated main' + compatible_request :put, :update, :id => 1, :category => { :name => new_name } + assert_redirected_to '/projects/ecookbook/settings/deals' + category1.reload + assert_equal new_name, category1.name + end + + def test_destroy_category_not_in_use + compatible_request :delete, :destroy, :id => 2 + assert_redirected_to '/projects/ecookbook/settings/deals' + assert_nil DealCategory.find_by_id(2) + end + + def test_destroy_category_in_use + compatible_request :delete, :destroy, :id => 1 + assert_response :success + assert_not_nil DealCategory.find_by_id(1) + end + + def test_destroy_category_in_use_with_reassignment + deal = Deal.where(:category_id => 1).first + compatible_request :delete, :destroy, :id => 1, :todo => 'reassign', :reassign_to_id => 2 + assert_redirected_to '/projects/ecookbook/settings/deals' + assert_nil DealCategory.find_by_id(1) + # check that the issue was reassign + assert_equal 2, deal.reload.category_id + end + + def test_destroy_category_in_use_without_reassignment + deal = Deal.where(:category_id => 1).first + compatible_request :delete, :destroy, :id => 1, :todo => 'nullify' + assert_redirected_to '/projects/ecookbook/settings/deals' + assert_nil DealCategory.find_by_id(1) + # check that the issue category was nullified + assert_nil deal.reload.category_id + end +end diff --git a/plugins/redmine_contacts/test/functional/deal_imports_controller_test.rb b/plugins/redmine_contacts/test/functional/deal_imports_controller_test.rb new file mode 100644 index 0000000..0816f0e --- /dev/null +++ b/plugins/redmine_contacts/test/functional/deal_imports_controller_test.rb @@ -0,0 +1,151 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class DealImportsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :deal_statuses, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + @controller = DealImportsController.new + User.current = nil + @csv_file = Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'deals_correct.csv', 'text/csv') + end + + test 'should open contact import form' do + @request.session[:user_id] = 1 + compatible_request :get, :new, :project_id => 1 + assert_response :success + if Redmine::VERSION.to_s >= '3.2' + assert_select 'form input#file' + else + assert_select 'form.new_deal_import' + end + end + + test 'should create new import object' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + compatible_request :get, :create, :project_id => 1, :file => @csv_file + assert_response :redirect + assert_equal Import.last.class, DealKernelImport + assert_equal Import.last.user, User.find(1) + assert_equal Import.last.project, 1 + assert_equal Import.last.settings, { 'project' => 1, + 'separator' => ';', + 'wrapper' => "\"", + 'encoding' => 'ISO-8859-1', + 'date_format' => '%m/%d/%Y' } + end + end + + test 'should open settings page' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + import = DealKernelImport.new + import.user = User.find(1) + import.project = Project.find(1) + import.file = @csv_file + import.save! + compatible_request :get, :settings, :id => import.filename, :project_id => 1 + assert_response :success + assert_select 'form#import-form' + end + end + + test 'should show mapping page' do + if Redmine::VERSION.to_s >= '3.2' + @request.session[:user_id] = 1 + import = DealKernelImport.new + import.user = User.find(1) + import.settings = { 'project' => 1, + 'separator' => ';', + 'wrapper' => "\"", + 'encoding' => 'UTF-8', + 'date_format' => '%m/%d/%Y' } + import.file = @csv_file + import.save! + compatible_request :get, :mapping, :id => import.filename, :project_id => 1 + assert_response :success + assert_select "select[name='import_settings[mapping][name]']" + assert_select 'select[name="import_settings[mapping][currency]"]' + assert_select 'table.sample-data tr' + assert_select 'table.sample-data tr td', 'Сделка века' + assert_select 'table.sample-data tr td', 'КемÑка волоÑть' + end + end + + test 'should successfully import from CSV with new import' do + if Redmine::VERSION.to_s >= '3.2' + cf = DealCustomField.create!(:name => 'LIST_FIELD', :field_format => 'list', :multiple => true, :possible_values => %w(1 2 3)) + @request.session[:user_id] = 1 + import = DealKernelImport.new + import.user = User.find(1) + import.settings = { 'project' => 1, + 'separator' => ';', + 'wrapper' => "\"", + 'encoding' => 'UTF-8', + 'date_format' => '%m/%d/%Y' } + import.file = @csv_file + import.save! + compatible_request :post, :mapping, :id => import.filename, :project_id => 1, :import_settings => { :mapping => { :name => 1, :background => 2, "cf_#{cf.id}" => 12 } } + assert_response :redirect + compatible_request :post, :run, :id => import.filename, :project_id => 1, :format => :js + assert_equal Deal.last.name, 'Сделка века' + assert_equal Deal.last.background, 'КемÑка волоÑть' + assert_equal Deal.last.custom_field_value(cf).sort, ['1', '3'] + end + end +end diff --git a/plugins/redmine_contacts/test/functional/deal_statuses_controller_test.rb b/plugins/redmine_contacts/test/functional/deal_statuses_controller_test.rb new file mode 100644 index 0000000..2f17197 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/deal_statuses_controller_test.rb @@ -0,0 +1,122 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class DealStatusesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :deal_statuses, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + @controller = DealStatusesController.new + User.current = nil + end + + def test_index_by_anonymous_should_redirect_to_login_form + @request.session[:user_id] = nil + compatible_request :get, :index + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fdeal_statuses' + end + + def test_should_get_new + @request.session[:user_id] = 1 + compatible_request :get, :new + assert_response :success + assert_select 'h2', %r{New} + end + + def test_should_get_edit + @request.session[:user_id] = 1 + compatible_request :get, :edit, :id => 1 + assert_response :success + assert_select 'h2', %r{#{DealStatus.find(1).name}} + end + + def test_should_post_update + @request.session[:user_id] = 1 + status1 = DealStatus.find(1) + new_name = 'updated main' + compatible_request :put, :update, :id => 1, :deal_status => { :name => new_name, :color_name => '#000000' } + assert_redirected_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => 'deal_statuses' + status1.reload + assert_equal new_name, status1.name + end + + def test_assing_to_project + @request.session[:user_id] = 1 + compatible_request :put, :assing_to_project, :deal_statuses => ['1', '2'], :project_id => 'ecookbook' + assert_redirected_to :controller => 'projects', :action => 'settings', :tab => 'deals', :id => 'ecookbook' + end + + def test_destroy + @request.session[:user_id] = 1 + Deal.where('status_id = 1').delete_all + + assert_difference 'DealStatus.count', -1 do + compatible_request :delete, :destroy, :id => '1' + end + assert_redirected_to :controller => 'settings', :action => 'plugin', :id => 'redmine_contacts', :tab => 'deal_statuses' + assert_nil DealStatus.find_by_id(1) + end + + def test_destroy_should_block_if_status_in_use + @request.session[:user_id] = 1 + assert_not_nil Deal.find_by_status_id(1) + + assert_no_difference 'DealStatus.count' do + compatible_request :delete, :destroy, :id => '1' + end + assert_redirected_to :controller => 'settings', :action => 'plugin', :id => "redmine_contacts", :tab => "deal_statuses" + assert_not_nil DealStatus.find_by_id(1) + end +end diff --git a/plugins/redmine_contacts/test/functional/deals_controller_test.rb b/plugins/redmine_contacts/test/functional/deals_controller_test.rb new file mode 100644 index 0000000..0c5fb1c --- /dev/null +++ b/plugins/redmine_contacts/test/functional/deals_controller_test.rb @@ -0,0 +1,506 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) +include RedmineContacts::TestHelper + +class DealsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :deals, + :deal_statuses, + :deal_statuses_projects, + :notes]) + if RedmineContacts.products_plugin_installed? + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_products).directory + '/test/fixtures/', [:product_categories, + :products, + :order_statuses, + :orders, + :product_lines]) + end + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_get_index + @request.session[:user_id] = 1 + + compatible_request :get, :index + assert_response :success + assert_not_nil deals_in_list + assert_select 'a', /First deal with contacts/ + end + + def test_get_index_list + @request.session[:user_id] = 1 + + compatible_request :get, :index, :deals_list_style => 'list' + assert_response :success + assert_select 'table.list.deals' + assert_not_nil deals_in_list + assert_select 'a', /First deal with contacts/ + end + + def test_get_index_board + @request.session[:user_id] = 1 + + compatible_request :get, :index, :deals_list_style => 'list_board' + assert_response :success + assert_select 'table.list.deal-board' + assert_not_nil deals_in_list + assert_select 'a', /First deal with contacts/ + end + + def test_get_index_pipeline + @request.session[:user_id] = 1 + + compatible_request :get, :index, :deals_list_style => 'list_pipeline' + assert_response :success + assert_select 'table.list.sales-funnel' + assert_not_nil deals_in_list + assert_select 'tr.deal_status_type-0 span', /Pending/ + end + + def test_get_index_calendar + @request.session[:user_id] = 1 + + compatible_request :get, :index, :deals_list_style => 'crm_calendars/crm_calendar' + assert_response :success + assert_select 'table.cal' + assert_not_nil deals_in_list + assert_select 'td.even div.deal a', /First deal with contacts/ + end + + def test_get_index_board_with_sorting + @request.session[:user_id] = 1 + + compatible_request :get, :index, :deals_list_style => 'list_board', :sort => 'due_date' + assert_response :success + assert_select 'table.list.deal-board' + assert_not_nil deals_in_list + assert_select 'a', /First deal with contacts/ + end + + def test_get_index_with_closed + @request.session[:user_id] = 1 + + compatible_request :get, :index + assert_response :success + assert_select 'h2', 'Deals' + assert_select 'a', /First deal with contacts/ + assert_select 'table.contacts.index h1.deal_name a', { :count => 0, :text => /Closed deal/ } + end + + def test_get_closed_index_with_pages + @request.session[:user_id] = 1 + + compatible_request :get, :index, :f => [''] + assert_response :success + assert_select 'h2', 'Deals' + assert_select 'table.contacts.index h1.deal_name a', /Closed deal/ + end + + def test_get_index_with_filters + @request.session[:user_id] = 1 + + compatible_request :get, :index, :f => ['status_id', ''], :op => { 'status_id' => '=' }, :v => { 'status_id' => ['3'] } + assert_equal 1, deals_in_list.count + assert_select 'table.contacts.index h1.deal_name a', /Second deal with contacts/ + assert_select 'table.contacts.index h1.deal_name a', { :count => 0, :text => 'Deal without contact' } + end + + def test_get_index_with_project + @request.session[:user_id] = 1 + + compatible_request :get, :index, :project_id => 1 + assert_response :success + assert_select 'h2', 'Deals' + assert_not_nil deals_in_list + assert_select 'a', :html => /First deal with contacts/ + assert_select 'Second deal with contacts', false + assert_select 'h3', :html => /Recently viewed/ + end + + def test_filter_by_ids + @request.session[:user_id] = 1 + ids = [3, 2] + compatible_request :get, :index, :project_id => 2, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '=' }, 'v' => { 'ids' => [ids.join(',')] } + assert_response :success + assert_equal ids.sort, deals_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_any + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 1, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '*' } + assert_response :success + assert_equal Project.find(1).deals.map(&:id).sort, deals_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_filter_by_ids_more_than + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 2, :set_filter => 1, 'f' => ['ids', ''], 'op' => { 'ids' => '>=' }, 'v' => { 'ids' => [3] } + assert_response :success + assert_equal [3, 4, 5], deals_in_list.map(&:id).sort + end if Redmine::VERSION.to_s >= '3.3' + + def test_get_index_without_statuses + project = Project.find_by_identifier('onlinestore') + + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 'onlinestore' + assert_response :success + assert_select 'h2', 'Deals' + assert_equal 1, deals_in_list.count + + assert_select 'table.deals_statistics' + assert_select'a', :html => /Deal without contact/ + assert_select'span.tag-label-color a', :text => 'Pending(1)' + + project.deal_statuses.delete_all + + @request.session[:user_id] = 1 + compatible_request :get, :index, :project_id => 'onlinestore' + assert_response :success + assert_select 'h2', 'Deals' + assert_equal 1, deals_in_list.count + + assert_select 'table.deals_statistics', { :count => 0 } + assert_select 'a', :html => /Deal without contact/ + end + + def test_post_create + @request.session[:user_id] = 1 + assert_difference 'Deal.count' do + compatible_request :post, :create, :project_id => 1, + :deal => { :price => 5500, + :name => 'New created deal 1', + :background => 'Background of new created deal', + :contact_id => 2, + :assigned_to_id => 3, + :category_id => 1, + :probability => 30, + :currency => 'RUB' } + end + assert_redirected_to :controller => 'deals', :action => 'show', :id => Deal.last.id + + deal = Deal.find_by_name('New created deal 1') + assert_not_nil deal + assert_equal 1, deal.category_id + assert_equal 2, deal.contact_id + assert_equal 3, deal.assigned_to_id + assert_equal 30, deal.probability + assert_equal 'RUB', deal.currency + end + + def test_post_create_with_formatted_price + with_contacts_settings('thousands_delimiter' => '.', 'decimal_separator' => ',') do + @request.session[:user_id] = 1 + assert_difference 'Deal.count' do + compatible_request :post, :create, :project_id => 1, + :deal => { :price => '1.234,56', + :name => 'New created deal 2', + :background => 'Background of new created deal', + :contact_id => 2, + :assigned_to_id => 3, + :category_id => 1, + :probability => 30, + :currency => 'RUB' } + end + assert_redirected_to :controller => 'deals', :action => 'show', :id => Deal.last.id + + deal = Deal.find_by_name('New created deal 2') + assert_not_nil deal + assert_equal 1234.56, deal.price + end + end + + def test_get_show + @request.session[:user_id] = 1 + deal = Deal.find(1) + compatible_request :get, :show, :id => deal.id + assert_response :success + assert_select 'h2', 'Deal #1' + assert_select 'table.subject_header td.name h1', %r{#{deal.name}} + end + + def test_get_show_with_custom_field + NoteCustomField.create!(:name => 'TestCustomField', :default_value => 'test text', :field_format => 'string') + @request.session[:user_id] = 1 + compatible_request :get, :show, :id => 1 + assert_response :success + assert_select 'h2', 'Deal #1' + assert_match 'TestCustomField', @response.body + assert_match 'test text', @response.body + end + + def test_get_show_with_statuses + project = Project.find(1) + project.deal_statuses.delete_all + project.deal_statuses << DealStatus.find(1) + project.deal_statuses << DealStatus.find(2) + project.save + + assert_equal ['Intermediate 1', 'Intermediate 2', 'Lost', 'Pending', 'Won'].sort, DealStatus.all.map(&:name).sort + assert_equal ['Pending', 'Won'].sort, project.deal_statuses.map(&:name).sort + @request.session[:user_id] = 1 + compatible_request :get, :show, :id => 1 + assert_response :success + assert_select 'h2', 'Deal #1' + assert_select '#deal_status_id', /Pending/ + assert_select '#deal_status_id', /Won/ + assert_select '#deal_status_id', { :count => 0, :text => /Lost/ } + end + + def test_get_new + @request.session[:user_id] = 1 + + project = Project.find(1) + project.deal_statuses << DealStatus.default + project.save + + compatible_request :get, :new, :project_id => 1 + assert_response :success + assert_equal DealStatus.default, Deal.new.status + assert_equal ContactsSetting.default_currency, Deal.new.currency + end + + def test_index_should_not_contatin_add_deal_link + EnabledModule.where(:name => 'deals').delete_all + + @request.session[:user_id] = 1 + compatible_request :get, :index + assert_response :success + assert_select '[href="/deals/new"]', { :count => 0 } + end + + def test_get_edit + @request.session[:user_id] = 1 + compatible_request :get, :edit, :id => 1 + assert_response :success + assert_select 'h2', 'Edit deal information' + assert_equal Deal.find(1).name, css_select('input#deal_name').map { |tag| tag['value'] }.join + end + + def test_put_update + @request.session[:user_id] = 1 + Setting.plugin_redmine_contacts['thousands_delimiter'] = ',' + Setting.plugin_redmine_contacts['decimal_separator'] = '.' + + deal = Deal.find(3) + new_name = 'Name modified by DealControllerTest#test_put_update' + + compatible_request :put, :update, :id => 3, :deal => { :name => new_name, :currency => 'GBP', :price => 23000 } + assert_redirected_to :action => 'show', :id => '3' + deal.reload + assert_equal 23000, deal.price + + compatible_request :get, :show, :id => 3 + assert_response :success + assert_select 'td.subject_info', /23\,000\.0/ + assert_equal new_name, deal.name + end + + def test_should_bulk_edit_deals + @request.session[:user_id] = 1 + compatible_request :post, :bulk_edit, :ids => [1, 2, 4] + assert_response :success + assert_select 'h2', 'Edit all selected deals' + assert_not_nil deals_in_list + end + + def test_should_not_bulk_edit_deals_by_deny_user + @request.session[:user_id] = 4 + compatible_request :post, :bulk_edit, :ids => [1, 2, 4] + assert_response 403 + end + + def test_should_put_bulk_update + @request.session[:user_id] = 1 + + compatible_request :put, :bulk_update, :ids => [1, 2, 4], + :deal => { :assigned_to_id => 2, + :category_id => 2, + :currency => 'GBP' }, + :note => { :content => 'Bulk deals edit note content' } + + assert_redirected_to :controller => 'deals', :action => 'index', :project_id => nil + + deals = Deal.find(1, 2, 4) + + assert_equal [2], deals.collect(&:assigned_to_id).uniq + assert_equal [2], deals.collect(&:category_id).uniq + assert_equal ['GBP'], deals.collect(&:currency).uniq + + assert_equal 3, Note.where(:content => 'Bulk deals edit note content').count + end + + def test_should_delete_bulk_destroy + @request.session[:user_id] = 1 + compatible_request :delete, :bulk_destroy, :ids => [1, 2, 4] + assert_redirected_to :controller => 'deals', :action => 'index' + end + + def test_post_index_live_search + @request.session[:user_id] = 1 + compatible_xhr_request :post, :index, :search => 'First' + assert_response :success + assert_select 'table.deals.index' + assert_select 'a', :html => /First deal with contacts/ + end + + def test_should_post_index_live_search_in_project + @request.session[:user_id] = 1 + compatible_xhr_request :post, :index, :search => 'First', :project_id => 'ecookbook' + assert_response :success + assert_select 'table.deals.index' + assert_select 'a', :content => /First deal with contacts/ + end + + def test_should_get_index_as_csv + field = DealCustomField.create!(:name => 'Test custom field', :is_filter => true, :field_format => 'string') + deal = Deal.find(1) + deal.custom_field_values = { field.id => "This is custom значение" } + deal.save + @request.session[:user_id] = 1 + compatible_request :get, :index, :format => 'csv' + assert_response :success + assert_not_nil deals_in_list + assert_equal 'text/csv; header=present', @response.content_type + assert_match 'Test custom field', @response.body + assert_match 'This is custom значение', @response.body + end + + def test_put_update_recalc_count_in_status + @request.session[:user_id] = 1 + + project = Project.find 1 + deal = project.deals.first + old_status = deal.status.id + new_status_id = old_status + 1 + new_status = DealStatus.find(new_status_id) + next_status_count = Deal.where(:status_id => new_status_id, :project_id => project.id).count + compatible_request :put, :update, :id => 1, :deal => { :status_id => new_status_id }, :status_id => '*', :format => 'js', :project_id => 1 + deal.reload + assert_equal new_status, deal.status + assert_match "#{new_status.name} (#{next_status_count + 1})", @response.body + end + + def test_delete_links_for_watchers + deal = Deal.find(1) + user = User.find(2) + Watcher.create!(:watchable_type => 'Deal', :watchable => deal, :user => user) + @request.session[:user_id] = 1 + compatible_request :get, :show, :id => 1 + assert_response :success + assert_select "ul.watchers li.user-#{user.id} a.delete" + end + def test_create_with_related_product + @request.session[:user_id] = 1 + product = Product.find(1) + compatible_request :post, :create, :project_id => 1, + :deal => { :price => 5500, + :name => 'New deal with product', + :background =>'Background of new created deal', + :contact_id => 2, + :assigned_to_id => 3, + :category_id => 1, + :probability => 30, + :currency => 'RUB', + :lines_attributes => { '0' => { :product_id => product.id, + :description => '', + :quantity => '2', + :price => '223.0', + :tax => '5.0', + :discount => '10', + :_destroy => 'false', + :position => '' } } } + assert_redirected_to :controller => 'deals', :action => 'show', :id => Deal.last.id + + deal = Deal.find_by_name('New deal with product') + assert_not_nil deal + assert_equal 1, deal.category_id + assert_equal 2, deal.contact_id + assert_equal 3, deal.assigned_to_id + assert_equal 30, deal.probability + assert_equal 'RUB', deal.currency + assert_equal 1, deal.lines.count + assert_equal product, deal.lines.last.product + end if RedmineContacts.products_plugin_installed? + + def test_get_show_with_related_product + @request.session[:user_id] = 1 + deal = Deal.find(1) + compatible_request :get, :show, :id => deal.id + + assert_response :success + assert_select 'table.product-lines tr.line-data', 1 + end if RedmineContacts.products_plugin_installed? + + def test_get_edit_with_related_product + @request.session[:user_id] = 1 + deal = Deal.find(1) + compatible_request :get, :edit, :id => deal.id + + assert_response :success + assert_select 'table.product-lines tr.sortable-line', 1 + end if RedmineContacts.products_plugin_installed? + + def test_get_index_with_product_filter + @request.session[:user_id] = 1 + compatible_request :get, :index, :set_filter => '1', :f => ['products', ''], :op => { 'products' => '*' } + assert_equal 5, deals_in_list.count + assert_select 'table.deals.index h1.deal_name a', /First deal with contacts/ + end if RedmineContacts.products_plugin_installed? + + def test_get_index_with_product_category_filter + @request.session[:user_id] = 1 + compatible_request :get, :index, :set_filter => '1', :f => ['product_category_id', ''], :op => { 'product_category_id' => '=' }, :v => { :product_category_id => ['1'] } + assert_equal 1, deals_in_list.count + assert_select 'table.deals.index h1.deal_name a', /First deal with contacts/ + end if RedmineContacts.products_plugin_installed? +end diff --git a/plugins/redmine_contacts/test/functional/issues_controller_test.rb b/plugins/redmine_contacts/test/functional/issues_controller_test.rb new file mode 100644 index 0000000..7329308 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/issues_controller_test.rb @@ -0,0 +1,149 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class IssuesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @request.session[:user_id] = 1 + end + + def test_get_show_issue_with_deal_and_contacts + compatible_request :get, :show, :id => 1 + assert_response :success + assert_select '#issue_contacts span.contact a', /Marat Aminov/ + if Redmine::VERSION.to_s >= '3.2' + assert_select 'div.value a', /Ivan Ivanov: First deal with contacts/ + else + assert_select 'td a', /Ivan Ivanov: First deal with contacts/ + end + end + def test_get_index_with_contacts_and_deals + compatible_request :get, :index, :f => ['status_id', 'companies', 'deal', ''], + :op => { :status_id => 'o', :companies => '=', :deal => '=' }, + :v => { :companies => ['3'], :deal => ['2'] }, + :c => ['subject', 'contacts', 'deal'], + :project_id => 'ecookbook' + assert_response :success + assert_select 'table.list.issues td.contacts span.contact a', /Marat Aminov/ + assert_select 'table.list.issues td.deal a', /Second deal with contacts/ + end + + def test_get_issues_without_contacts + compatible_request :get, :index, :f => ['status_id', 'contacts', ''], + :op => { :status_id => '*', :contacts => '!*' }, + :c => ['subject', 'contacts'], + :project_id => 'ecookbook' + assert_response :success + assert_select 'table.list.issues td.contacts', '' + end + + def test_get_issues_only_with_contacts + compatible_request :get, :index, :f => ['status_id', 'contacts', ''], + :op => { :status_id => '*', :contacts => '*' }, + :c => ['subject', 'contacts'], + :project_id => 'ecookbook' + assert_response :success + assert_select 'table.list.issues td.contacts' + end + + def test_get_new_with_deal + compatible_request :get, :new, :project_id => 'ecookbook', :deal_id => 1 + assert_response :success + assert_select 'select#issue_deals_issue_attributes_deal_id', /First deal with contacts/ + if ActiveRecord::VERSION::MAJOR >= 4 + assert_select "#issue_deals_issue_attributes_deal_id option[value='1']" + else + assert_select '#issue_deals_issue_attributes_deal_id option[value=?]', 1 + end + end + + def test_post_create_with_deal + assert_difference 'DealsIssue.count' do + compatible_request :post, :create, :issue => { :tracker_id => 3, :subject => 'test', :status_id => 2, :priority_id => 5, + :deals_issue_attributes => { :deal_id => 1 } }, + :project_id => 'ecookbook' + end + + issue = Issue.order('id ASC').last + assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id + assert_not_nil issue.deal + end + + def test_post_create_with_invalid_deal_id + assert_no_difference 'Issue.count' do + compatible_request :post, :create, :issue => { :tracker_id => 3, :subject => 'test', :status_id => 2, :priority_id => 5, + :deals_issue_attributes => { :deal_id => 'abc' } }, + :project_id => 'ecookbook' + end + end + + def test_put_update_form + issue = Issue.find(1) + if ActiveRecord::VERSION::MAJOR < 4 + compatible_xhr_request :put, :update_form, :issue => { :tracker_id => 2, + :deals_issue_attributes => { :deal_id => 2 } }, + :project_id => issue.project + assert_response :success + assert_equal 'text/javascript', response.content_type + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 2, issue.deals_issue.deal_id + end + end +end diff --git a/plugins/redmine_contacts/test/functional/notes_controller_test.rb b/plugins/redmine_contacts/test/functional/notes_controller_test.rb new file mode 100644 index 0000000..75c25b6 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/notes_controller_test.rb @@ -0,0 +1,91 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class NotesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + @request.env['HTTP_REFERER'] = '/' + end + + def test_should_post_add_note_to_contact + @request.session[:user_id] = 1 + assert_difference 'Note.count' do + compatible_request :post, :create, :project_id => 1, + :note => { :subject => 'Note subject', + :content => 'Note *content*' }, + :source_type => Contact.to_s, + :source_id => 1 + end + + note = Note.where(:subject => 'Note subject', :content => 'Note *content*').first + assert_not_nil note + assert_equal 1, note.source_id + assert_equal Contact, note.source.class + end + + def test_should_put_update + @request.session[:user_id] = 1 + + note = Note.find(1) + new_content = 'New note content' + + compatible_request :put, :update, :id => 1, :project_id => 1, :note => { :content => new_content } + assert_redirected_to :action => 'show', :project_id => note.source.project, :id => note.id + note.reload + assert_equal new_content, note.content + end +end diff --git a/plugins/redmine_contacts/test/functional/queries_controller_test.rb b/plugins/redmine_contacts/test/functional/queries_controller_test.rb new file mode 100644 index 0000000..de74791 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/queries_controller_test.rb @@ -0,0 +1,45 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) +class QueriesControllerTest < ActionController::TestCase + fixtures :projects, :enabled_modules, + :users, :email_addresses, + :members, :member_roles, :roles, + :trackers, :issue_statuses, :issue_categories, :enumerations, :versions, + :issues, :custom_fields, :custom_values, + :queries + + def setup + User.current = nil + end + def test_filter_for_contact_custom_field + contact_cf = ContactCustomField.create!(:name => 'contact_cf', :is_filter => true, :field_format => 'company') + @request.session[:user_id] = 1 + compatible_request :get, :filter, :params => { :type => 'ContactQuery', :name => contact_cf.name } + + assert_response :success + assert_equal 'application/json', response.content_type + ensure + contact_cf.destroy + end if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? +end diff --git a/plugins/redmine_contacts/test/functional/search_controller_test.rb b/plugins/redmine_contacts/test/functional/search_controller_test.rb new file mode 100644 index 0000000..802290f --- /dev/null +++ b/plugins/redmine_contacts/test/functional/search_controller_test.rb @@ -0,0 +1,86 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class SearchControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + User.current = nil + end + + def test_search_for_contacts + @request.session[:user_id] = 1 + compatible_request :get, :index + assert_response :success + assert_select 'h2', 'Search' + + compatible_request :get, :index, :q => 'ivan' + assert_response :success + assert_select 'h2', 'Search' + assert_match Contact.find(1).first_name, response.body + end + + def test_search_for_contacts_by_email + @request.session[:user_id] = 1 + compatible_request :get, :index + assert_response :success + assert_select 'h2', 'Search' + + compatible_request :get, :index, :q => 'marat@mail.ru' + assert_response :success + assert_select 'h2', 'Search' + assert_match Contact.find(2).first_name, response.body + end +end diff --git a/plugins/redmine_contacts/test/functional/timelog_controller_test.rb b/plugins/redmine_contacts/test/functional/timelog_controller_test.rb new file mode 100644 index 0000000..887a69b --- /dev/null +++ b/plugins/redmine_contacts/test/functional/timelog_controller_test.rb @@ -0,0 +1,89 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class TimelogControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + end + def test_get_report_with_deal + @request.session[:user_id] = 1 + compatible_request :get, :report, :columns => 'month', :criteria => ['deal', 'deal_contact'], :project_id => 'ecookbook' + assert_response :success + assert_select 'table#time-report td', /Domoway/ + assert_select 'table#time-report td', /First deal with contacts/ + assert_select 'table#time-report td', /Second deal with contacts/ + end + + def test_get_index_with_company_cf + @request.session[:user_id] = 1 + project = Project.find(1) + company = Contact.find(3) + @cfield = IssueCustomField.create!(:name => 'COMPANY', :field_format => 'company', :is_filter => true) + @cfield.projects << project + compatible_request :get, :index, :set_filter => 1, + :f => ["issue.cf_#{@cfield.id}", ''], + :op => { "issue.cf_#{@cfield.id}" => '=' }, + :v => { "issue.cf_#{@cfield.id}" => [company.id] }, + :c => ['spent_on', 'user', 'issue'], + :project_id => project.identifier + assert_response :success + assert_match "values\":[[\"#{company.name}\",\"#{company.id}\"]]", response.body + assert_match "\"field_format\":\"company\"", response.body + ensure + @cfield.destroy + end if Redmine::VERSION.to_s > '2.5' +end diff --git a/plugins/redmine_contacts/test/functional/users_controller_test.rb b/plugins/redmine_contacts/test/functional/users_controller_test.rb new file mode 100644 index 0000000..967f517 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/users_controller_test.rb @@ -0,0 +1,67 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class UsersControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + end + def test_get_new_from_contact + @request.session[:user_id] = 1 + compatible_request :get, :new_from_contact, :contact_id => 1, :id => 'current' + assert_response :success + assert_select 'input#user_firstname[value=?]', 'Ivan' + end +end diff --git a/plugins/redmine_contacts/test/functional/wiki_controller_test.rb b/plugins/redmine_contacts/test/functional/wiki_controller_test.rb new file mode 100644 index 0000000..c23e638 --- /dev/null +++ b/plugins/redmine_contacts/test/functional/wiki_controller_test.rb @@ -0,0 +1,112 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class WikiControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :roles, + :enabled_modules, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :wikis, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + EnabledModule.create(:project_id => 1, :name => 'wiki') + @project = Project.find(1) + @wiki = @project.wiki + @page_name = 'contact_macro_test' + @page = @wiki.find_or_new_page(@page_name) + @page.content = WikiContent.new + @page.content.text = 'test' + @page.content.author = User.find(1) + @page.save! + end + + def test_show_with_contact_macro + @request.session[:user_id] = 1 + @page.content.text = '{{contact(1)}}' + @page.content.save! + compatible_request :get, :show, :project_id => 1, :id => @page_name + assert_response :success + assert_select 'h3', 'Wiki' + assert_select 'div.wiki p', /Ivan Ivanov/ + end + + def test_show_with_contact_avatar_macro + @request.session[:user_id] = 1 + @page.content.text = '{{contact_avatar(1)}}' + @page.content.save! + compatible_request :get, :show, :project_id => 1, :id => @page_name + assert_response :success + assert_select 'h3', 'Wiki' + assert_select 'div.wiki p img' + end + + def test_show_with_note_macro + @request.session[:user_id] = 1 + @page.content.text = '{{contact_note(1)}}' + @page.content.save! + compatible_request :get, :show, :project_id => 1, :id => @page_name + assert_response :success + assert_select 'h3', 'Wiki' + assert_select 'div.wiki p', /Note 1 content with wiki syntax/ + end + def test_show_with_deal_macro + @request.session[:user_id] = 1 + @page.content.text = '{{deal(1)}}' + @page.content.save! + compatible_request :get, :show, :project_id => 1, :id => @page_name + assert_response :success + assert_select 'h3', 'Wiki' + assert_select 'div.wiki p', /Ivan Ivanov: First deal with contacts/ + end +end diff --git a/plugins/redmine_contacts/test/integration/api_test/contact.xml b/plugins/redmine_contacts/test/integration/api_test/contact.xml new file mode 100644 index 0000000..8493f68 --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/contact.xml @@ -0,0 +1,9 @@ + + + API contact name + true + contacts-plugin + + + + \ No newline at end of file diff --git a/plugins/redmine_contacts/test/integration/api_test/contacts_projects_test.rb b/plugins/redmine_contacts/test/integration/api_test/contacts_projects_test.rb new file mode 100644 index 0000000..009ef43 --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/contacts_projects_test.rb @@ -0,0 +1,89 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path(File.dirname(__FILE__) + '/../../test_helper') + +class Redmine::ApiTest::NotesTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + Setting.rest_api_enabled = '1' + RedmineContacts::TestCase.prepare + end + + test 'POST /contacts/:contact_id/projects.xml' do + parameters = { :project => { :id => 2 } } + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, '/contacts/1/projects.xml', parameters, :success_code => :success) + end + + compatible_api_request :post, '/contacts/1/projects.xml', parameters, credentials('admin') + assert_response :success + assert_not_nil Contact.find(1).projects.where(:id => 2) + end + + test 'DELETE /contacts/:contact_id/projects.xml' do + contact = Contact.find(1) + contact.projects << Project.find(2) + contact.save + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:delete, '/contacts/1/projects/2.xml', {}, :success_code => :success) + end + + compatible_api_request :delete, '/contacts/1/projects/2.xml', {}, credentials('admin') + assert_response :success + contact.reload + assert_nil contact.projects.where(:id => 2).first + end +end diff --git a/plugins/redmine_contacts/test/integration/api_test/contacts_test.rb b/plugins/redmine_contacts/test/integration/api_test/contacts_test.rb new file mode 100644 index 0000000..b2e940d --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/contacts_test.rb @@ -0,0 +1,163 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path(File.dirname(__FILE__) + '/../../test_helper') + +class Redmine::ApiTest::ContactsTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + Setting.rest_api_enabled = '1' + RedmineContacts::TestCase.prepare + end + + def test_get_contacts_xml + # Use a private project to make sure auth is really working and not just + # only showing public issues. + Redmine::ApiTest::Base.should_allow_api_authentication(:get, '/projects/private-child/contacts.xml') if ActiveRecord::VERSION::MAJOR < 4 + + compatible_api_request :get, '/contacts.xml', {}, credentials('admin') + + att = { :type => 'array', :total_count => 5, :limit => 25, :offset => 0 } + assert_select 'contacts', :attributes => att + end + + def test_post_contacts_xml + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, '/contacts.xml', { :contact => { :project_id => 1, :first_name => 'API test' } }, + { :success_code => :created }) + end + + assert_difference('Contact.count') do + compatible_api_request :post, '/contacts.xml', { :contact => { :project_id => 1, :first_name => 'API test' } }, credentials('admin') + end + + contact = Contact.order('id DESC').first + assert_equal 'API test', contact.first_name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_select 'contact', :child => { :tag => 'id', :content => contact.id.to_s } + end + + def test_post_contacts_xml_redirect + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, '/contacts.xml', { :contact => { :project_id => 1, :first_name => 'API test' } }, + { :success_code => :created }) + end + + assert_difference('Contact.count') do + compatible_api_request :post, '/contacts.xml', { :contact => { :project_id => 1, :first_name => 'API test' }, :redirect_on_success => 'http://ya.ru' }, credentials('admin') + end + + assert_redirected_to 'http://ya.ru' + end + + # Issue 6 is on a private project + def test_put_contacts_1_xml + parameters = { :contact => { :first_name => 'API update' } } + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:put, '/contacts/1.xml', { :contact => { :first_name => 'API update' } }, + { :success_code => :ok }) + end + + assert_no_difference('Contact.count') do + compatible_api_request :put, '/contacts/1.xml', parameters, credentials('admin') + end + + contact = Contact.where(:id => 1).first + assert_equal 'API update', contact.first_name + end + def test_update_contact_with_uploaded_file + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + compatible_api_request :post, '/uploads.xml', 'test_upload_with_upload', { 'CONTENT_TYPE' => 'application/octet-stream' }.merge(credentials('admin')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.order('id DESC').first + + # update the issue with the upload's token + compatible_api_request :put, '/contacts/1.xml', { :contact => {:name => 'Attachment added', :uploads => [{ :token => token, :filename => 'test.png', + :description => 'avatar', + :content_type => 'image/png' }] } }, + credentials('admin') + assert_response :ok + assert_equal '', @response.body + + contact = Contact.where(:id => 1).first + assert_include attachment, contact.attachments + assert_equal attachment, contact.avatar + end + + def test_should_post_with_custom_fields + field = ContactCustomField.create!(:name => 'Test', :field_format => 'int') + assert_difference('Contact.count') do + compatible_api_request :post, '/contacts.xml', { :contact => { :project_id => 1, :first_name => 'API test', + :custom_fields => [{ 'id' => field.id.to_s, 'value' => '12' }] } }, credentials('admin') + end + contact = Contact.last + assert_equal '12', contact.custom_value_for(field.id).value + end + + def test_should_put_with_custom_fields + field = ContactCustomField.create!(:name => 'Test', :field_format => 'text') + assert_no_difference('Contact.count') do + compatible_api_request :put, '/contacts/1.xml', { :contact => { :custom_fields => [{ 'id' => field.id.to_s, 'value' => 'Hello' }] } }, credentials('admin') + end + contact = Contact.where(:id => 1).first + assert_equal 'Hello', contact.custom_value_for(field.id).value + end +end diff --git a/plugins/redmine_contacts/test/integration/api_test/deal.xml b/plugins/redmine_contacts/test/integration/api_test/deal.xml new file mode 100644 index 0000000..0ce71fb --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/deal.xml @@ -0,0 +1,6 @@ + + + API deal name + USD + contacts-plugin + \ No newline at end of file diff --git a/plugins/redmine_contacts/test/integration/api_test/deals_test.rb b/plugins/redmine_contacts/test/integration/api_test/deals_test.rb new file mode 100644 index 0000000..c8eeabf --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/deals_test.rb @@ -0,0 +1,128 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path(File.dirname(__FILE__) + '/../../test_helper') + +class Redmine::ApiTest::DealsTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + Setting.rest_api_enabled = '1' + RedmineContacts::TestCase.prepare + end + + test 'GET /deals.xml' do + # Use a private project to make sure auth is really working and not just + # only showing public issues. + Redmine::ApiTest::Base.should_allow_api_authentication(:get, '/projects/private-child/deals.xml') if ActiveRecord::VERSION::MAJOR < 4 + compatible_api_request :get, '/deals.xml', {}, credentials('admin') + + att = { :type => 'array', :total_count => 2, :limit => 25, :offset => 0 } + assert_select 'deals', :attributes => att + end + + test 'POST /deals.xml' do + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, '/deals.xml', { :deal => { :project_id => 1, :name => 'API test', :contact_id => 1 } }, + { :success_code => :created }) + end + + assert_difference('Deal.count') do + compatible_api_request :post, '/deals.xml', { :deal => { :project_id => 1, :name => 'API test', :contact_id => 1 } }, credentials('admin') + end + + deal = Deal.order('id DESC').first + assert_equal 'API test', deal.name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_select 'deal', :child => { :tag => 'id', :content => deal.id.to_s } + end + + # Issue 6 is on a private project + test 'PUT /deals/1.xml' do + @parameters = { :deal => { :name => 'API update' } } + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:put, '/deals/1.xml', { :deal => { :name => 'API update' } }, + { :success_code => :ok }) + end + + assert_no_difference('Deal.count') do + compatible_api_request :put, '/deals/1.xml', @parameters, credentials('admin') + end + + deal = Deal.where(:id => 1).first + assert_equal 'API update', deal.name + end + + def test_post_with_custom_fields + field = DealCustomField.create!(:name => 'Test', :field_format => 'int') + assert_difference('Deal.count') do + compatible_api_request :post, '/deals.xml', { :deal => { :project_id => 1, :name => 'API test', + :custom_fields => [{ 'id' => field.id.to_s, 'value' => '14' }] } }, + credentials('admin') + end + deal = Deal.last + assert_equal '14', deal.custom_value_for(field.id).value + end + + def test_put_with_custom_fields + field = DealCustomField.create!(:name => 'Test', :field_format => 'text') + assert_no_difference('Deal.count') do + compatible_api_request :put, '/deals/1.xml', { :deal => { :custom_fields => [{ 'id' => field.id.to_s, 'value' => 'Hello deal' }] } }, + credentials('admin') + end + deal = Deal.where(:id => 1).first + assert_equal 'Hello deal', deal.custom_value_for(field.id).value + end +end diff --git a/plugins/redmine_contacts/test/integration/api_test/note.xml b/plugins/redmine_contacts/test/integration/api_test/note.xml new file mode 100644 index 0000000..80498bf --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/note.xml @@ -0,0 +1,5 @@ + + + API note content + Test note + \ No newline at end of file diff --git a/plugins/redmine_contacts/test/integration/api_test/notes_test.rb b/plugins/redmine_contacts/test/integration/api_test/notes_test.rb new file mode 100644 index 0000000..b45a597 --- /dev/null +++ b/plugins/redmine_contacts/test/integration/api_test/notes_test.rb @@ -0,0 +1,113 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path(File.dirname(__FILE__) + '/../../test_helper') + +class Redmine::ApiTest::NotesTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + Setting.rest_api_enabled = '1' + RedmineContacts::TestCase.prepare + end + + test 'POST /notes.xml' do + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, '/notes.xml', { :note => { :project_id => 1, + :source_id => 1, + :source_type => 'Contact', + :content => 'API test' } }, + { :success_code => :created }) + end + + assert_difference('Note.count', 1) do + compatible_api_request :post, '/notes.xml', { :note => { :content => 'API test' }, :project_id => 1, :source_id => 1, :source_type => 'Contact' }, credentials('admin') + end + + note = Note.order('id DESC').first + assert_equal 'API test', note.content + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_select 'note', :child => { :tag => 'id', :content => note.id.to_s } + end + + test 'PUT /notes/1.xml' do + @parameters = { :note => { :content => 'API update' } } + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:put, '/notes/1.xml', @parameters, :success_code => :ok) + end + + assert_no_difference('Note.count') do + compatible_api_request :put, '/notes/1.xml', @parameters, credentials('admin') + assert_response :success + end + + note = Note.where(:id => 1).first + assert_equal 'API update', note.content + end + + test 'DELETE /notes/1.xml' do + @parameters = { :note => { :content => 'API update' } } + + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:put, '/notes/1.xml', @parameters, :success_code => :ok) + end + + assert_difference('Note.count', -1) do + compatible_api_request :delete, '/notes/1.xml', @parameters, credentials('admin') + assert_response :success + end + assert_nil Note.where(:id => 1).first + end +end diff --git a/plugins/redmine_contacts/test/integration/common_views_test.rb b/plugins/redmine_contacts/test/integration/common_views_test.rb new file mode 100644 index 0000000..ec8486d --- /dev/null +++ b/plugins/redmine_contacts/test/integration/common_views_test.rb @@ -0,0 +1,160 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) +require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') + + +class RedmineContacts::CommonViewsTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals_issues, + :deals, + :deal_statuses, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + end + + test 'View user' do + log_user('admin', 'admin') + compatible_request :get, '/users/2' + assert_response :success + end + + test 'View contacts activity' do + log_user('admin', 'admin') + compatible_request :get, '/projects/ecookbook/activity?show_contacts=1' + assert_response :success + end + + test 'View contacts settings' do + log_user('admin', 'admin') + compatible_request :get, '/settings/plugin/redmine_contacts' + assert_response :success + end + + test 'View contacts project settings' do + log_user('admin', 'admin') + compatible_request :get, '/projects/ecookbook/settings/contacts' + assert_response :success + end + + test 'View contact tag edit' do + log_user('admin', 'admin') + compatible_request :get, '/contacts_tags/1/edit' + assert_response :success + end + test 'View deal status edit' do + log_user('admin', 'admin') + compatible_request :get, '/deal_statuses/1/edit' + assert_response :success + end + + test 'View My page with contacts and deals blocks' do + log_user('rhill', 'foo') + user = User.where(:login => 'rhill').first + Contact.all.each { |c| c.assigned_to = user; c.save } + preferences = user.pref + preferences[:my_page_layout] = { 'top' => ['my_contacts', 'my_deals'] } + preferences.save! + + compatible_request :get, '/my/page' + assert_response :success + assert_select 'span.contact', 'Domoway' + end + + def test_new_custom_field + log_user('admin', 'admin') + compatible_request :get, '/custom_fields/new', :type => 'ContactCustomField' + assert_response :success + + compatible_request :get, '/custom_fields/new', :type => 'DealCustomField' + assert_response :success + end + + test 'Global search with contacts' do + log_user('admin', 'admin') + compatible_request :get, '/search?q=Domoway' + assert_response :success + end + + test 'View contacts project notes list' do + log_user('admin', 'admin') + compatible_request :get, '/projects/ecookbook/contacts/notes' + assert_response :success + end + + test 'View contacts notes list' do + log_user('admin', 'admin') + compatible_request :get, '/contacts/notes' + assert_response :success + end + + test 'View issue contacts' do + log_user('admin', 'admin') + EnabledModule.create(:project_id => 1, :name => 'issue_tracking') + issue = Issue.where(:id => 1).first + contact = Contact.where(:id => 1).first + issue.contacts << contact + issue.save + compatible_request :get, '/issues/1' + assert_response :success + end + + test 'View user with contact relation' do + log_user('admin', 'admin') + compatible_request :get, '/users/2' + assert_response :success + # assert_tag :div, + # :content => /John Smith/, + # :attributes => { :class => 'contact card' } + end +end diff --git a/plugins/redmine_contacts/test/integration/routing_test.rb b/plugins/redmine_contacts/test/integration/routing_test.rb new file mode 100644 index 0000000..fe12fdd --- /dev/null +++ b/plugins/redmine_contacts/test/integration/routing_test.rb @@ -0,0 +1,63 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class RoutingTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + def test_contacts + # REST actions + assert_routing({ :path => '/contacts', :method => :get }, { :controller => 'contacts', :action => 'index' }) + assert_routing({ :path => '/contacts.xml', :method => :get }, { :controller => 'contacts', :action => 'index', :format => 'xml' }) + assert_routing({ :path => '/contacts.atom', :method => :get }, { :controller => 'contacts', :action => 'index', :format => 'atom' }) + assert_routing({ :path => '/contacts/notes', :method => :get }, { :controller => 'contacts', :action => 'contacts_notes' }) + assert_routing({ :path => '/contacts/1', :method => :get }, { :controller => 'contacts', :action => 'show', :id => '1' }) + assert_routing({ :path => '/contacts/1/edit', :method => :get }, { :controller => 'contacts', :action => 'edit', :id => '1' }) + assert_routing({ :path => '/contacts/context_menu', :method => :get }, { :controller => 'contacts', :action => 'context_menu' }) + assert_routing({ :path => '/projects/23/contacts', :method => :get }, { :controller => 'contacts', :action => 'index', :project_id => '23' }) + assert_routing({ :path => '/projects/23/contacts.xml', :method => :get }, { :controller => 'contacts', :action => 'index', :project_id => '23', :format => 'xml' }) + assert_routing({ :path => '/projects/23/contacts.atom', :method => :get }, { :controller => 'contacts', :action => 'index', :project_id => '23', :format => 'atom' }) + assert_routing({ :path => '/projects/23/contacts/notes', :method => :get }, { :controller => 'contacts', :action => 'contacts_notes', :project_id => '23' }) + + assert_routing({ :path => '/contacts.xml', :method => :post }, { :controller => 'contacts', :action => 'create', :format => 'xml' }) + + assert_routing({ :path => '/contacts/1.xml', :method => :put }, { :controller => 'contacts', :action => 'update', :format => 'xml', :id => '1' }) + + assert_routing({ :path => '/contacts/bulk_edit', :method => :post }, { :controller => 'contacts', :action => 'bulk_edit' }) + assert_routing({ :path => '/contacts/bulk_edit', :method => :get }, { :controller => 'contacts', :action => 'bulk_edit' }) + assert_routing({ :path => '/contacts/context_menu', :method => :get }, { :controller => 'contacts', :action => 'context_menu' }) + assert_routing({ :path => '/contacts/send_mails', :method => :post }, { :controller => 'contacts', :action => 'send_mails' }) + end + + def test_notes + # REST actions + assert_routing({ :path => '/notes/1', :method => :get }, { :controller => 'notes', :action => 'show', :id => '1' }) + assert_routing({ :path => '/notes/1/edit', :method => :get }, { :controller => 'notes', :action => 'edit', :id => '1' }) + assert_routing({ :path => '/notes/1', :method => :put }, { :controller => 'notes', :action => 'update', :id => '1' }) + assert_routing({ :path => '/notes', :method => :post }, { :controller => 'notes', :action => 'create' }) + end + def test_deals + # REST actions + assert_routing({ :path => '/deals', :method => :get }, { :controller => 'deals', :action => 'index' }) + assert_routing({ :path => '/deals/1', :method => :get }, { :controller => 'deals', :action => 'show', :id => '1' }) + assert_routing({ :path => '/deals/1/edit', :method => :get }, { :controller => 'deals', :action => 'edit', :id => '1' }) + assert_routing({ :path => '/projects/23/deals', :method => :get }, { :controller => 'deals', :action => 'index', :project_id => '23' }) + end +end diff --git a/plugins/redmine_contacts/test/test_helper.rb b/plugins/redmine_contacts/test/test_helper.rb new file mode 100644 index 0000000..5054667 --- /dev/null +++ b/plugins/redmine_contacts/test/test_helper.rb @@ -0,0 +1,150 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') + +def redmine_contacts_fixture_files_path + "#{Rails.root}/plugins/redmine_contacts/test/fixtures/files/" +end + +# Engines::Testing.set_fixture_path +module RedmineContacts + module TestHelper + def compatible_request(type, action, parameters = {}) + return send(type, action, :params => parameters) if Rails.version >= '5.1' + send(type, action, parameters) + end + + def compatible_xhr_request(type, action, parameters = {}) + return send(type, action, :params => parameters, :xhr => true) if Rails.version >= '5.1' + xhr type, action, parameters + end + + def compatible_api_request(type, action, parameters = {}, headers = {}) + return send(type, action, :params => parameters, :headers => headers) if Rails.version >= '5.1' + send(type, action, parameters, headers) + end + + def issues_in_list + ids = css_select('tr.issue td.id').map{ |tag| tag['text'].to_i } + Issue.where(:id => ids).sort_by { |issue| ids.index(issue.id) } + end + + def contacts_in_list + ids = css_select('table.contacts #selected_contacts_').map { |tag| tag['value'].to_i } + Contact.where(:id => ids).sort_by { |contact| ids.index(contact.id) } + end + + def deals_in_list + ids = css_select('.deal_list #ids_').map { |tag| tag['value'].to_i } + Deal.where(:id => ids).sort_by { |contact| ids.index(contact.id) } + end + + def with_contacts_settings(options, &block) + Setting.plugin_redmine_contacts.stubs(:[]).returns(nil) + options.each { |k, v| Setting.plugin_redmine_contacts.stubs(:[]).with(k).returns(v) } + yield + ensure + options.each { |_k, _v| Setting.plugin_redmine_contacts.unstub(:[]) } + end + end +end + +class RedmineContacts::TestCase + include ActionDispatch::TestProcess + def self.plugin_fixtures(plugin, *fixture_names) + plugin_fixture_path = "#{Redmine::Plugin.find(plugin).directory}/test/fixtures" + if fixture_names.first == :all + fixture_names = Dir["#{plugin_fixture_path}/**/*.{yml}"] + fixture_names.map! { |f| f[(plugin_fixture_path.size + 1)..-5] } + else + fixture_names = fixture_names.flatten.map { |n| n.to_s } + end + + ActiveRecord::Fixtures.create_fixtures(plugin_fixture_path, fixture_names) + end + + def uploaded_test_file(name, mime) + ActionController::TestUploadedFile.new(ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime, true) + end + + def self.is_arrays_equal(a1, a2) + (a1 - a2) - (a2 - a1) == [] + end + + def self.create_fixtures(fixtures_directory, table_names, class_names = {}) + if ActiveRecord::VERSION::MAJOR >= 4 + ActiveRecord::FixtureSet.create_fixtures(fixtures_directory, table_names, class_names) + else + ActiveRecord::Fixtures.create_fixtures(fixtures_directory, table_names, class_names) + end + end + + def self.prepare + # User 2 Manager (role 1) in project 1, email jsmith@somenet.foo + # User 3 Developer (role 2) in project 1 + + Role.where(:id => [1, 2, 3, 4]).each do |r| + r.permissions << :view_contacts + r.save + end + + Role.where(:id => [1, 2]).each do |r| + #user_2, user_3 + r.permissions << :add_contacts + r.save + end + + Role.where(:id => 1).each do |r| + #user_2 + r.permissions << :add_deals + r.permissions << :save_contacts_queries + r.save + end + + Role.where(:id => [1, 2]).each do |r| + r.permissions << :edit_contacts + r.save + end + Role.where(:id => [1, 2, 3]).each do |r| + r.permissions << :view_deals + r.save + end + + Role.where(:id => 2).each do |r| + r.permissions << :edit_deals + r.permissions << :manage_contact_issue_relations + r.save + end + + Role.where(:id => [1, 2]).each do |r| + r.permissions << :manage_public_contacts_queries + r.save + end + + Project.where(:id => [1, 2, 3, 4, 5]).each do |project| + EnabledModule.create(:project => project, :name => 'contacts') + EnabledModule.create(:project => project, :name => 'deals') + end + end +end + +include RedmineContacts::TestHelper diff --git a/plugins/redmine_contacts/test/unit/address_test.rb b/plugins/redmine_contacts/test/unit/address_test.rb new file mode 100644 index 0000000..a950093 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/address_test.rb @@ -0,0 +1,137 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class AddressTest < ActiveSupport::TestCase + def setup + Setting.plugin_redmine_contacts['post_address_format'] = nil + end + + def test_should_generate_full_address + address = Address.new + address.street1 = '300 Boylston Ave E' + address.street2 = 'Piso2 Dto.4' + address.city = 'Seattle' + address.region = 'WA' + address.postcode = '98102' + address.country_code = 'US' + address.save + address.reload + + assert_equal '300 Boylston Ave E, Piso2 Dto.4, Seattle, 98102, WA, United States', address.full_address + end + + def test_should_generate_to_s + address = Address.new + address.street1 = '300 Boylston Ave E' + address.street2 = 'Piso2 Dto.4' + address.city = 'Seattle' + address.region = 'WA' + address.postcode = '98102' + address.country_code = 'US' + + assert_equal '300 Boylston Ave E, Piso2 Dto.4, Seattle, 98102, WA, United States', address.to_s + end + + def test_should_generate_particular_full_address + address = Address.new + address.street1 = '300 Boylston Ave E' + address.city = 'Seattle' + address.postcode = '98102' + address.region = '' + address.country_code = 'US' + address.save + address.reload + + assert_equal '300 Boylston Ave E, Seattle, 98102, United States', address.full_address + end + + def test_should_generate_us_post_address + address = Address.new + address.street1 = '300 Boylston Ave E' + address.city = 'Seattle' + address.postcode = '98102' + address.region = 'WA' + address.country_code = 'US' + + assert_equal "300 Boylston Ave E\nSeattle, 98102\nWA\nUnited States", address.post_address + end + + def test_should_generate_us_post_address_with_double_spaces + Setting.plugin_redmine_contacts['post_address_format'] = "%street1%\n%street2%\n%city% %region% %postcode%\n%country%" + address = Address.new + address.street1 = '300 Boylston Ave E' + address.city = 'Seattle' + address.postcode = '98102' + address.region = 'WA' + address.country_code = 'US' + + assert_equal "300 Boylston Ave E\nSeattle WA 98102\nUnited States", address.post_address + end + + def test_should_generate_ru_post_address + address = Address.new + address.street1 = "ул. Маршала Жукова, 6" + address.city = "г. ÐрзамаÑ" + address.postcode = '611137' + address.region = "ÐижегородÑÐºÐ°Ñ Ð¾Ð±Ð»Ð°Ñть" + address.country_code = 'RU' + + assert_equal "ул. Маршала Жукова, 6\nг. ÐрзамаÑ, 611137\nÐижегородÑÐºÐ°Ñ Ð¾Ð±Ð»Ð°Ñть\nRussia", address.post_address + end + + def test_should_generate_ru_post_address_with_empty_region + address = Address.new + address.street1 = "ул. ÐÐ¾Ð²Ð°Ñ Ð‘Ð°ÑманнаÑ, 14" + address.city = "г. МоÑква" + address.postcode = '145013' + address.country_code = 'RU' + + assert_equal "ул. ÐÐ¾Ð²Ð°Ñ Ð‘Ð°ÑманнаÑ, 14\nг. МоÑква, 145013\nRussia", address.post_address + end + + def test_should_strip_empty_lines_and_punctuation + Setting.plugin_redmine_contacts['post_address_format'] = "%street1%,\n,%street2%,,,\n%city%, %postcode%\n%region%\n%country%" + + address = Address.new + address.city = 'Seattle' + address.region = 'WA' + address.country_code = 'US' + + assert_equal "Seattle\nWA\nUnited States", address.post_address + end + + def test_should_create_new_address + address = Address.new + address.street1 = 'ул. ÐÐ¾Ð²Ð°Ñ Ð‘Ð°ÑманнаÑ, 14' + address.city = 'г. МоÑква' + address.postcode = '145013' + address.country_code = 'RU' + address.address_type = 'business' + address.addressable = Contact.first + address.save! + + assert_equal false, address.new_record? + assert_equal 'ул. ÐÐ¾Ð²Ð°Ñ Ð‘Ð°ÑманнаÑ, 14, г. МоÑква, 145013, Russia', address.full_address + end +end diff --git a/plugins/redmine_contacts/test/unit/contact_import_test.rb b/plugins/redmine_contacts/test/unit/contact_import_test.rb new file mode 100644 index 0000000..dcc237f --- /dev/null +++ b/plugins/redmine_contacts/test/unit/contact_import_test.rb @@ -0,0 +1,66 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactImportTest < ActiveSupport::TestCase + fixtures :projects, :users + + def test_open_correct_csv + contact_import = ContactImport.new( + :file => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'correct.csv', 'text/comma-separated-values'), + :project => Project.first, + :quotes_type => '"' + ) + puts contact_import.errors.full_messages unless contact_import.valid? + assert_equal 4, contact_import.imported_instances.count, 'Should find 4 contacts in file' + assert contact_import.save, 'Should save successfully' + end + + def test_should_report_error_line + contact_import = ContactImport.new( + :file => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'with_data_malformed.csv', 'text/comma-separated-values'), + :project => Project.first, + :quotes_type => '"' + ) + assert !contact_import.save, 'Should not save with malformed date' + assert_equal 1, contact_import.errors.count, 'Should have 1 error' + assert contact_import.errors.first.last.include?("Error on line 1"), 'Should mention string number in error message' + end + + def test_open_csv_with_custom_fields + cf1 = ContactCustomField.create!(:name => 'License', :field_format => 'string') + cf2 = ContactCustomField.create!(:name => 'Purchase date', :field_format => 'date') + contact_import = ContactImport.new( + :file => Rack::Test::UploadedFile.new(redmine_contacts_fixture_files_path + 'contacts_cf.csv', 'text/comma-separated-values'), + :project => Project.first, + :quotes_type => '"' + ) + assert_equal 1, contact_import.imported_instances.count, 'Should find 1 contact in file' + assert contact_import.save, 'Should save successfully' + contact = Contact.find_by_first_name('Monica') + assert_equal '12345', contact.custom_field_value(cf1.id) + assert_equal 'rhill', contact.assigned_to.login + assert_equal '123456', contact.postcode + assert_equal 'Moscow', contact.city + assert_equal 'Russia', contact.country + end +end diff --git a/plugins/redmine_contacts/test/unit/contact_test.rb b/plugins/redmine_contacts/test/unit/contact_test.rb new file mode 100644 index 0000000..c741215 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/contact_test.rb @@ -0,0 +1,256 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + end + + def test_find_by_emails_first_email + emails = ['marat@mail.ru', 'domoway@mail.ru'] + assert_equal 2, Contact.find_by_emails(emails).count + end + + def test_find_by_emails_second_email + emails = ['marat@mail.com'] + assert_equal 1, Contact.find_by_emails(emails).count + end + + def test_scope_live_search + assert_equal 4, Contact.live_search('john').first.try(:id) + end + + def test_visible_public_contacts + project = Project.find(1) + contact = Contact.find(1) + user = User.find(1) # John Smith + + contact.visibility = Contact::VISIBILITY_PUBLIC + contact.save! + + assert contact.visible?(user) + end + + def test_visible_scope_for_non_member_without_view_contacts_permissions + # Non member user should not see issues without permission + Role.non_member.remove_permission!(:view_contacts) + user = User.find(9) + assert user.projects.empty? + contacts = Contact.visible(user).all + assert contacts.empty? + assert_visibility_match user, contacts + end + + def test_visible_scope_for_member + user = User.find(2) + # User should see issues of projects for which he has view_issues permissions only + role = Role.create!(:name => 'CRM', :permissions => [:view_contacts]) + Role.non_member.remove_permission!(:view_contacts) + project = Project.find(2) + Contact.delete_all + Member.where(:user_id => user).delete_all + member = Member.create!(:principal => user, :project_id => project.id, :role_ids => [role.id]) + contact = Contact.create!(:project => project, :first_name => 'UnitTest', :visibility => Contact::VISIBILITY_PUBLIC) + + contacts = Contact.visible(user).all + + assert contacts.any? + assert_nil contacts.detect { |c| c.project.id != project.id } + # assert_nil contacts.detect {|c| c.is_private?} + assert_visibility_match user, contacts + + contact.visibility = Contact::VISIBILITY_PRIVATE + contact.save! + contacts = Contact.visible(user).all + assert contacts.blank?, 'Private contacts are visible' + + assert user.allowed_to?(:view_contacts, project) + contact.visibility = Contact::VISIBILITY_PROJECT + contact.save! + contacts = Contact.visible(user).all + assert contacts.any?, "Project contacts doesn't visible with permissions" + + role.remove_permission!(:view_contacts) + user.reload + contact.visibility = Contact::VISIBILITY_PROJECT + contact.save! + contacts = project.contacts.visible(user).all + assert contacts.blank?, 'Contacts visible for user without view_contacts permissions' + + role.add_permission!(:view_private_contacts) + user.reload + contact.visibility = Contact::VISIBILITY_PRIVATE + contact.save! + contacts = Contact.visible(user).all + assert contacts.any?, 'Contacts note visible for user with view_private_contacts permissions' + end + + def test_create_should_send_email_notification + ActionMailer::Base.deliveries.clear + contact = Contact.new(:first_name => 'New contact', :project => Project.find(1)) + + with_settings :notified_events => %w(crm_contact_added) do + assert contact.save + end + assert_equal 1, ActionMailer::Base.deliveries.size + end + + def assert_visibility_match(user, contacts) + assert_equal contacts.collect(&:id).sort, Contact.all.select { |contact| contact.visible?(user) }.collect(&:id).sort + end + + def test_that_contact_with_email_containing_plus_is_valid + contact = Contact.find(1) + contact.email = 'foo+bar-baz@email.example' + assert contact.valid? + end + + def test_if_email_with_local_domain_is_allowed + contact = Contact.find(1) + contact.email = 'email@mydomain' + assert contact.valid? + end + + def test_special_characters_in_email_local_part + contact = Contact.find(1) + contact.email = "#!$%&'{}@email.example" + assert contact.valid? + end + + def test_email_containing_unicode_characters + contact = Contact.find(1) + contact.email = 'дениÑ@пример.рф' + assert contact.valid? + end + + def test_that_email_can_include_ip_address + contact = Contact.find(1) + contact.email = 'foo@[IPv6:2001:db8::1]' + assert contact.valid? + end + + def test_contact_with_multiple_email_addresses + contact = Contact.find(1) + contact.email = 'foo@email.example,bar@email.example' + assert contact.valid? + end + + def test_if_email_without_at_sign_is_invalid + contact = Contact.find(1) + contact.email = 'hello' + assert contact.invalid? + end + + def test_email_transformation_on_create + assert_equal 'test@test.com', Contact.create!(:project => Project.find(1), :first_name => 'Test', :email => ' test@test.com ').email + assert_equal 'test@test.com,foo@bar.com', Contact.create!(:project => Project.find(1), :first_name => 'Test', :email => ' test@test.com , foo@bar.com ').email + end + + def test_duplicates_no_middle_name + project = Project.find(1) + User.current = User.find(1) + + contact1 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => 'Stoyanov') + contact2 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => '') + assert contact2.duplicates.include?(contact1) + assert contact1.duplicates.include?(contact2) + end + + def test_duplicates_by_just_email + project = Project.find(1) + User.current = User.find(1) + + contact1 = Contact.create!(:project => project, :first_name => 'Kristiyan', :email => 'test@test.com') + contact2 = Contact.create!(:project => project, :first_name => 'Peter', :email => 'test@test.com') + assert contact2.duplicates.include?(contact1) + assert contact1.duplicates.include?(contact2) + end + + def test_duplicates_no_middle_name_with_last_name + project = Project.find(1) + User.current = User.find(1) + + contact1 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => '', :last_name => 'Stoyanov') + contact2 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => 'Petrov', :last_name => 'Stoyanov') + assert contact1.duplicates.include?(contact2) + assert contact2.duplicates.include?(contact1) + end + + def test_duplicates_no_middle_name_with_last_name_and_email + project = Project.find(1) + User.current = User.find(1) + + contact1 = Contact.create!(:project => project, :first_name => 'Kristiyan', :last_name => 'Stoyanov', :email => 'test@test.com') + contact2 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => 'Petrov', :last_name => 'Stoyanov', :email => 'test@test.com') + assert contact1.duplicates.include?(contact2) + assert contact2.duplicates.include?(contact1) + end + + def test_multiple_duplicates_different_criteria + project = Project.find(1) + User.current = User.find(1) + + contact1 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => '', :email => 'test@test.com') + contact2 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => '', :last_name => 'Stoyanov', :email => 'test@test.com') + contact3 = Contact.create!(:project => project, :first_name => 'Kristiyan', :middle_name => 'Petrov', :last_name => 'Stoyanov') + contact4 = Contact.create!(:project => project, :first_name => 'Mr.Nobody', :middle_name => '', :last_name => 'Test', :email => 'test@test.com') + + assert contact1.duplicates.include?(contact2) + assert contact1.duplicates.include?(contact3) + assert contact1.duplicates.include?(contact4) + end +end diff --git a/plugins/redmine_contacts/test/unit/contacts_issues_test.rb b/plugins/redmine_contacts/test/unit/contacts_issues_test.rb new file mode 100644 index 0000000..afc7761 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/contacts_issues_test.rb @@ -0,0 +1,31 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsIssuesTest < ActiveSupport::TestCase + fixtures :contacts_issues + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/plugins/redmine_contacts/test/unit/contacts_mailer_test.rb b/plugins/redmine_contacts/test/unit/contacts_mailer_test.rb new file mode 100644 index 0000000..6716779 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/contacts_mailer_test.rb @@ -0,0 +1,151 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactsMailerTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/contacts_mailer' + + def setup + RedmineContacts::TestCase.prepare + + ActionMailer::Base.deliveries.clear + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + end + + test 'Should add contact note from to' do + # This email contains: 'Project: onlinestore' + note = submit_email('new_note.eml').first + assert_instance_of ContactNote, note + assert !note.new_record? + note.reload + assert_equal Contact, note.source.class + assert_equal 'New note from email', note.subject + assert_equal User.find_by_login('admin'), note.author + assert_equal Contact.find(1).id, note.source_id + end + + test 'Should add contact note from ID in to' do + # This email contains: 'Project: onlinestore' + note = submit_email('new_note_by_id.eml').first + assert_instance_of ContactNote, note + assert !note.new_record? + note.reload + assert_equal Contact, note.source.class + assert_equal 'New note from email', note.subject + assert_equal User.find_by_login('admin'), note.author + assert_equal Contact.find(1).id, note.source_id + end + + test 'Should add contact note from ID in cc' do + # This email contains: 'Project: onlinestore' + note = submit_email('new_note_with_cc.eml').first + assert_instance_of ContactNote, note + assert !note.new_record? + note.reload + assert_equal Contact, note.source.class + assert_equal 'New note from email by id in cc', note.subject + assert_equal User.find_by_login('admin'), note.author + assert_equal Contact.find(1).id, note.source_id + end + + test 'Should add deal note from ID in to' do + # This email contains: 'Project: onlinestore' + note = submit_email('new_deal_note_by_id.eml').first + assert_instance_of DealNote, note + assert !note.new_record? + note.reload + assert_equal Deal, note.source.class + assert_equal 'New note from email', note.subject + assert_equal User.find_by_login('admin'), note.author + assert_equal Deal.find(1).id, note.source_id + end + + test 'Should add contact note from forwarded' do + note = submit_email('fwd_new_note_plain.eml').first + assert_instance_of ContactNote, note + assert !note.new_record? + note.reload + assert_equal Contact, note.source.class + assert_equal 'New note from forwarded email', note.subject + assert_match 'From: "Marat Aminov" marat@mail.ru', note.content + assert_equal User.find_by_login('admin'), note.author + assert_equal Contact.find(2).id, note.source_id + end + + test 'Should add contact note from forwarded html' do + note = submit_email('fwd_new_note_html.eml').first + assert_instance_of ContactNote, note + assert !note.new_record? + note.reload + assert_equal Contact, note.source.class + assert_equal 'New note from forwarded html email', note.subject + assert_match 'From: Marat Aminov ', note.content + assert_equal User.find_by_login('admin'), note.author + assert_equal Contact.find(2).id, note.source_id + end + + test 'Should not add contact note from deny user to' do + assert !submit_email('new_deny_note.eml') + end + + private + + def submit_email(filename, options = {}) + raw = IO.read(File.join(FIXTURES_PATH, filename)) + ContactsMailer.receive(raw, options) + end +end diff --git a/plugins/redmine_contacts/test/unit/custom_field_company_format_test.rb b/plugins/redmine_contacts/test/unit/custom_field_company_format_test.rb new file mode 100644 index 0000000..41b80b4 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/custom_field_company_format_test.rb @@ -0,0 +1,78 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) +include RedmineContacts::TestHelper + +if Redmine::VERSION.to_s > '2.5' + class CustomFieldCompanyFormatTest < ActiveSupport::TestCase + fixtures :custom_fields, :projects, :members, :users, :member_roles, :trackers, :issues + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + RedmineContacts::TestCase.prepare + @field = IssueCustomField.create!(:name => 'Tester', :field_format => 'company') + @controller = DealStatusesController.new + Role.anonymous.remove_permission!(:view_contacts) + User.current = nil + end + + def test_possible_values_options_with_no_arguments + with_contacts_settings('cross_project_contacts' => 0) do + User.current = nil + assert_equal [], @field.possible_values_options + assert_equal [], @field.possible_values_options(nil) + end + end + + def test_possible_values_options_with_project_resource + with_contacts_settings('cross_project_contacts' => 1) do + User.current = User.find(1) + project = Project.find(1) + possible_values_options = @field.possible_values_options(project.issues.first) + assert possible_values_options.empty? + end + end + + def test_cast_blank_value + assert_nil @field.cast_value(nil) + assert_nil @field.cast_value('') + end + + def test_cast_valid_value + contact = @field.cast_value('2') + assert_kind_of Contact, contact + assert_equal Contact.find(2), contact + end + + def test_cast_invalid_value + assert_nil @field.cast_value('187') + end + end +end diff --git a/plugins/redmine_contacts/test/unit/deal_import_test.rb b/plugins/redmine_contacts/test/unit/deal_import_test.rb new file mode 100644 index 0000000..e31be35 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/deal_import_test.rb @@ -0,0 +1,53 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + + +require File.expand_path('../../test_helper', __FILE__) + +class DealImportTest < ActiveSupport::TestCase + fixtures :projects, :users + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :deals, + :deal_statuses, + :deal_categories]) + + def fixture_files_path + "#{File.expand_path('../..', __FILE__)}/fixtures/files/" + end + + def test_open_correct_csv + deal_import = DealImport.new( + :file => Rack::Test::UploadedFile.new(fixture_files_path + 'deals_correct.csv', 'text/comma-separated-values'), + :project => Project.first, + :quotes_type => '"' + ) + assert_difference('Deal.count', 1, 'Should have 1 deal in the database') do + assert_equal 1, deal_import.imported_instances.count, 'Should find 1 deal in file' + assert deal_import.save, 'Should save successfully' + end + deal = Deal.last + assert_equal 2, deal.status_id, "Status doesn't mach" + assert_equal 1, deal.category_id, 'Category should be Design' + assert_equal 'rhill', deal.assigned_to.login, 'Assignee should be rhill' + end +end diff --git a/plugins/redmine_contacts/test/unit/deal_status_test.rb b/plugins/redmine_contacts/test/unit/deal_status_test.rb new file mode 100644 index 0000000..bff9f5d --- /dev/null +++ b/plugins/redmine_contacts/test/unit/deal_status_test.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class ContactTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :deal_statuses, + :notes, + :tags, + :taggings, + :queries]) + + # Replace this with your real tests. + def test_destroy + new_status = DealStatus.create(:name => 'New status', :is_default => false, :status_type => DealStatus::OPEN_STATUS) + + assert_difference 'DealStatus.count', -1 do + assert new_status.destroy + end + end + + def test_destroy_status_in_use + status = Deal.find(1).status + + assert_no_difference 'DealStatus.count' do + assert_raise(RuntimeError, "Can't delete status") do + status.destroy + end + end + end +end diff --git a/plugins/redmine_contacts/test/unit/deal_test.rb b/plugins/redmine_contacts/test/unit/deal_test.rb new file mode 100644 index 0000000..4c857a3 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/deal_test.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + + +require File.expand_path('../../test_helper', __FILE__) + +class DealTest < ActiveSupport::TestCase + fixtures :projects, :users + + RedmineContacts::TestCase.create_fixtures( + Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, :contacts_projects, + :deals, :deal_statuses] + ) + + def test_price_to_s_with_custome_settings + Setting.plugin_redmine_contacts['decimal_separator'] = '.' + Setting.plugin_redmine_contacts['thousands_delimiter'] = ' ' + assert_equal '$3 000.00', Deal.find(1).price_to_s + end + + def test_count_for_status_scope + project = Project.find(2) + assert_equal 1, project.deals.with_status(1).count + assert_equal 2, project.deals.with_status(2).count + assert_equal 1, project.deals.with_status(3).count + end + + def test_price_with_big_value + Setting.plugin_redmine_contacts['decimal_separator'] = '.' + Setting.plugin_redmine_contacts['thousands_delimiter'] = ' ' + deal = Deal.find(5) + price = deal.price + deal.update_attributes(:price => 9999999999999) + assert_equal '9 999 999 999 999.00 RUB', Deal.find(5).price_to_s + ensure + deal.update_attributes(:price => price) + end +end diff --git a/plugins/redmine_contacts/test/unit/deals_pipeline_processor_test.rb b/plugins/redmine_contacts/test/unit/deals_pipeline_processor_test.rb new file mode 100644 index 0000000..722c593 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/deals_pipeline_processor_test.rb @@ -0,0 +1,96 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class DealsPipelineProcessorTest < ActiveSupport::TestCase + fixtures :projects, :users + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :deals, + :deal_statuses, + :deal_categories]) + + def setup + Deal.destroy_all + @deal_status_new = DealStatus.find(1) + @deal_status_won = DealStatus.find(2) + @deal_status_lost = DealStatus.find(3) + @deal_status_intermediate1 = DealStatus.find(4) + @deal_status_intermediate2 = DealStatus.find(5) + end + + def test_constructor + assert_not_nil DealsPipelineProcessor.new(Deal) + end + + def test_closed_deal_counts_in_last_unclosed_status + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_won) + assert_equal(1, DealProcess.count) + processor = DealsPipelineProcessor.new(Deal) + assert_equal(1, processor.deals_for_status(@deal_status_new).count) + end + + def test_open_deal_counts_in_last_unclosed_status + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_intermediate1) + assert_equal(1, DealProcess.count) + processor = DealsPipelineProcessor.new(Deal) + assert_equal(1, processor.deals_for_status(@deal_status_new).count) + end + + def test_if_asked_in_status_returns_simple_case + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_won) + + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal 2', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_intermediate1) + + assert_equal(2, DealProcess.count) + processor = DealsPipelineProcessor.new(Deal) + assert_equal(1, processor.deals_for_status(@deal_status_won).count) + end + + def test_if_deal_jumped_over_status + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_intermediate2) + + processor = DealsPipelineProcessor.new(Deal) + assert_equal(1, processor.deals_for_status(@deal_status_intermediate1).count) + end + + def test_if_deal_returned_from_lost + @deal = Deal.create!(:status => @deal_status_new, :name => 'New deal', :project => Project.last) + @deal.init_deal_process(User.first) + @deal.update_attribute(:status, @deal_status_lost) + @deal.update_attribute(:status, @deal_status_intermediate2) + + processor = DealsPipelineProcessor.new(Deal) + assert_equal(1, processor.deals_for_status(@deal_status_intermediate1).count) + end +end diff --git a/plugins/redmine_contacts/test/unit/helpers/contacts_helper_test.rb b/plugins/redmine_contacts/test/unit/helpers/contacts_helper_test.rb new file mode 100644 index 0000000..6ad2daf --- /dev/null +++ b/plugins/redmine_contacts/test/unit/helpers/contacts_helper_test.rb @@ -0,0 +1,87 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../../test_helper', __FILE__) + +class ContactsHelperTest < ActionView::TestCase + include ApplicationHelper + include ContactsHelper + include CustomFieldsHelper + include Redmine::I18n + include ERB::Util + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :versions, + :projects_trackers, + :member_roles, + :members, + :groups_users, + :enabled_modules + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + super + set_language_if_valid('en') + User.current = nil + end + + def test_contacts_to_xls + User.current = User.find(1) + xls_result = contacts_to_xls(Contact.all) + assert_match /First Name/, xls_result + assert_match /Domoway/, xls_result + end + + def test_contacts_to_xls_with_multivalue_custom_field + User.current = User.find(1) + field = ContactCustomField.create!(:name => 'filter', :field_format => 'list', + :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], + :multiple => true) + contact = Contact.find(1) + contact.custom_field_values = { field.id => ['value1', 'value2', 'value3'] } + contact.save! + xls_result = contacts_to_xls([contact]) + assert_match /First Name/, xls_result + assert_match /Domoway/, xls_result + assert_match /value1, value2, value3/, xls_result + end + + def test_mail_macro + field = ContactCustomField.create!(:name => 'Custom field', :field_format => 'string') + contact = Contact.find(1) + contact.custom_field_values = {field.id => 'test value'} + contact.save! + message = "Hello %%NAME%%, %%FULL_NAME%% %%COMPANY%% %%LAST_NAME%% %%MIDDLE_NAME%% %%DATE%% %%Custom field%%" + + result_msg = mail_macro(contact, message) + assert_equal "Hello Ivan, Ivan Ivanov Domoway Ivanov Ivanovich #{format_date(Date.today)} test value", result_msg + end +end diff --git a/plugins/redmine_contacts/test/unit/helpers/deals_helper_test.rb b/plugins/redmine_contacts/test/unit/helpers/deals_helper_test.rb new file mode 100644 index 0000000..39f3219 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/helpers/deals_helper_test.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../../test_helper', __FILE__) + +class DealsHelperTest < ActionView::TestCase + include ApplicationHelper + include DealsHelper + include CustomFieldsHelper + include Redmine::I18n + include ERB::Util + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :versions, + :projects_trackers, + :member_roles, + :members, + :groups_users, + :enabled_modules + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + super + set_language_if_valid('en') + User.current = nil + end + + def test_deals_to_csv + User.current = User.find(1) + csv_result = deals_to_csv(Deal.all) + assert_match /Name/, csv_result + assert_match /First deal with contacts/, csv_result + end + + def test_deals_to_csv_with_multivalue_custom_field + User.current = User.find(1) + field = DealCustomField.create!(:name => 'filter', :field_format => 'list', + :is_filter => true, :is_for_all => true, + :possible_values => ['value1', 'value2', 'value3'], + :multiple => true) + deal = Deal.find(1) + deal.custom_field_values = { field.id => ['value1', 'value2', 'value3'] } + deal.save! + csv_result = deals_to_csv([deal]) + assert_match /Name/, csv_result + assert_match /First deal with contacts/, csv_result + assert_match /value1, value2, value3/, csv_result + end +end diff --git a/plugins/redmine_contacts/test/unit/helpers/notes_helper_test.rb b/plugins/redmine_contacts/test/unit/helpers/notes_helper_test.rb new file mode 100644 index 0000000..9ad05d9 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/helpers/notes_helper_test.rb @@ -0,0 +1,78 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../../test_helper', __FILE__) + +class NotesHelperTest < ActionView::TestCase + include ApplicationHelper + include NotesHelper + include Redmine::I18n + include ERB::Util + + fixtures :projects, :trackers, :issue_statuses, :issues, + :enumerations, :users, :issue_categories, + :versions, + :projects_trackers, + :member_roles, + :members, + :groups_users, + :enabled_modules + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + super + set_language_if_valid('en') + User.current = nil + end + + def test_authoring_note_without_time + RedmineContacts.settings[:note_authoring_time] = false + assert_nothing_raised { authoring_note('2012-12-12 10:00'.to_time, User.find(1)) } + end + + def test_authoring_note_with_time + RedmineContacts.settings[:note_authoring_time] = true + assert_nothing_raised { authoring_note('2012-12-12 10:00'.to_time, User.find(1)) } + end + + def test_authoring_note_without_time_with_empty_time + RedmineContacts.settings[:note_authoring_time] = true + assert_nothing_raised { authoring_note(nil, User.find(1)) } + end + + def test_authoring_note_without_time_with_empty_time + RedmineContacts.settings[:note_authoring_time] = false + assert_nothing_raised { authoring_note(nil, User.find(1)) } + end + + def test_authoring_note_without_time_with_empty_user + RedmineContacts.settings[:note_authoring_time] = true + assert_nothing_raised { authoring_note('2012-12-12 10:00'.to_time, nil) } + end +end diff --git a/plugins/redmine_contacts/test/unit/lib/contacts_project_setting_test.rb b/plugins/redmine_contacts/test/unit/lib/contacts_project_setting_test.rb new file mode 100644 index 0000000..a8cbcf2 --- /dev/null +++ b/plugins/redmine_contacts/test/unit/lib/contacts_project_setting_test.rb @@ -0,0 +1,82 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +# encoding: utf-8 +require File.expand_path('../../../test_helper', __FILE__) + +class ContactsProjectSettingTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :contacts_settings, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + def setup + Setting.plugin_redmine_contacts['post_address_format'] = nil + @project_settings = ContactsProjectSetting.new(Project.find(1), 'redmine_contacts') + end + + def test_read_values + assert_equal 'String value', @project_settings.string_setting + assert_equal true, @project_settings.boolean_setting? + end + + def test_read_global_values + Setting['plugin_redmine_contacts']['global_value'] = 'Global' + assert_equal 'Global', @project_settings.global_value + end + + def test_read_default_values + assert_equal ['USD', 'EUR', 'GBP', 'RUB', 'CHF'].sort, @project_settings.major_currencies.sort + end + + def test_read_default_values_post_address_format + assert_equal "%street1%\n%street2%\n%city%, %postcode%\n%region%\n%country%", @project_settings.post_address_format + end +end diff --git a/plugins/redmine_contacts/test/unit/mailer_patch_test.rb b/plugins/redmine_contacts/test/unit/mailer_patch_test.rb new file mode 100644 index 0000000..fb890ca --- /dev/null +++ b/plugins/redmine_contacts/test/unit/mailer_patch_test.rb @@ -0,0 +1,124 @@ +# encoding: utf-8 +# +# This file is a part of Redmine CRM (redmine_contacts) plugin, +# customer relationship management plugin for Redmine +# +# Copyright (C) 2010-2018 RedmineUP +# http://www.redmineup.com/ +# +# redmine_contacts is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# redmine_contacts is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with redmine_contacts. If not, see . + +require File.expand_path('../../test_helper', __FILE__) + +class MailerPatchTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineContacts::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deal_processes, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/contacts_mailer' + + def setup + RedmineContacts::TestCase.prepare + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + ActionMailer::Base.deliveries.clear + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + end + + def test_crm_note_add + note = Note.find(1) + assert Mailer.crm_note_add(note).deliver + assert_match 'Note 1', last_email.text_part.to_s + end + + def test_crm_note_add_to_company + note = Note.find(4) + assert Mailer.crm_note_add(note).deliver + assert_match 'Note 4', last_email.text_part.to_s + end + + def test_crm_contact_add + contact = Contact.find(1) + assert Mailer.crm_contact_add(contact).deliver + assert_match 'Contact #1: Ivan Ivanov', last_email.text_part.to_s + end + def test_crm_note_add_to_deal + note = Note.find(5) + assert Mailer.crm_note_add(note).deliver + assert_match 'Note 5', last_email.text_part.to_s + end + + def test_crm_deal_add + deal = Deal.find(1) + assert Mailer.crm_deal_add(deal).deliver + assert_match 'Deal #1', last_email.text_part.to_s + end + + def test_crm_deal_updated + deal_process = DealProcess.last + deal_process.author = User.find(2) + deal_process.save + assert Mailer.crm_deal_updated(deal_process).deliver + assert_match 'John Smith', last_email.text_part.to_s + end + + private + + def last_email + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + mail + end + + def text_part + last_email.parts.detect { |part| part.content_type.include?('text/plain') } + end + + def html_part + last_email.parts.detect { |part| part.content_type.include?('text/html') } + end +end diff --git a/plugins/redmine_contacts_helpdesk/.drone.yml b/plugins/redmine_contacts_helpdesk/.drone.yml new file mode 100644 index 0000000..471262e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/.drone.yml @@ -0,0 +1,90 @@ +pipeline: + tests: + image: redmineup/redmine_contacts_helpdesk + commands: + - service postgresql start && service mysql start && sleep 5 + - export PATH=~/.rbenv/shims:$PATH + - export CODEPATH=`pwd` + - /root/run_for.sh redmine_contacts_helpdesk ${RUBY_VER} ${DB} ${REDMINE} redmine_contacts+${LICENSE} +matrix: + include: + - RUBY_VER: ruby-2.2.6 + LICENSE: pro + DB: mysql + REDMINE: redmine-3.3 + - RUBY_VER: ruby-2.2.6 + LICENSE: light + DB: mysql + REDMINE: redmine-3.3 + - RUBY_VER: ruby-2.2.6 + LICENSE: pro + DB: pg + REDMINE: redmine-3.3 + - RUBY_VER: ruby-2.2.6 + LICENSE: light + DB: pg + REDMINE: redmine-3.3 + - RUBY_VER: ruby-2.2.6 + LICENSE: pro + DB: mysql + REDMINE: redmine-3.0 + - RUBY_VER: ruby-1.8.7 + DB: pg + LICENSE: pro + REDMINE: redmine-2.3 + - RUBY_VER: ruby-1.8.7 + DB: pg + LICENSE: pro + REDMINE: redmine-2.6 + - RUBY_VER: ruby-1.8.7 + DB: mysql + LICENSE: pro + REDMINE: redmine-2.3 + - RUBY_VER: ruby-1.8.7 + DB: mysql + LICENSE: pro + REDMINE: redmine-2.6 + - RUBY_VER: ruby-1.9.3 + DB: pg + LICENSE: pro + REDMINE: redmine-2.3 + - RUBY_VER: ruby-1.9.3 + DB: pg + LICENSE: pro + REDMINE: redmine-2.6 + - RUBY_VER: ruby-1.9.3 + DB: pg + LICENSE: pro + REDMINE: redmine-3.3 + - RUBY_VER: ruby-1.9.3 + DB: mysql + LICENSE: pro + REDMINE: redmine-3.3 + - RUBY_VER: ruby-1.9.3 + DB: pg + LICENSE: light + REDMINE: redmine-3.3 + - RUBY_VER: ruby-1.8.7 + DB: pg + LICENSE: light + REDMINE: redmine-2.3 + - RUBY_VER: ruby-1.8.7 + DB: pg + LICENSE: light + REDMINE: redmine-2.6 + # - RUBY_VER: ruby-2.4.1 + # DB: mysql + # LICENSE: pro + # REDMINE: redmine-trunk + # - RUBY_VER: ruby-2.4.1 + # DB: mysql + # LICENSE: light + # REDMINE: redmine-trunk + # - RUBY_VER: ruby-2.4.1 + # DB: pg + # LICENSE: pro + # REDMINE: redmine-trunk + # - RUBY_VER: ruby-2.2.6 + # DB: mysql + # LICENSE: pro + # REDMINE: redmine-trunk diff --git a/plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md b/plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md new file mode 100644 index 0000000..e05b2cc --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/LOCAL_CHANGELOG.md @@ -0,0 +1,26 @@ +# Local Change Log + +This RedmineUP helpdesk plugin is maintained as local legacy code for the +installed Redmine 3.4.4 environment. Keep entries focused on local behavior, +rollback archives, and LAN test status. + +## 2026-04-21 - Helpdesk Search Read API And Outbox Coverage + +- Purpose: make helpdesk ticket and message identity first-class for external + operational search and indexing. +- Rollback archives: + - `dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz` + - `dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz` +- Touched behavior: + - Added read-only JSON endpoints under `helpdesk_search/*`. + - Extended the existing `view_helpdesk_tickets` permission to cover those + endpoints. + - Added event outbox callbacks for `HelpdeskTicket` and `JournalMessage` in + the local `redmine_event_outbox` plugin. +- Payload policy: + - Includes ids, direction, message id, source, and non-body address metadata. + - Does not include full email bodies, private notes, attachment data, or BCC + addresses. +- LAN test result: pending. Validate by creating/updating a controlled helpdesk + ticket and journal message, then checking `event_outbox_events` and the + `helpdesk_search/*` JSON endpoints on the LAN Redmine copy. diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/canned_responses_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/canned_responses_controller.rb new file mode 100644 index 0000000..48c2db9 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/canned_responses_controller.rb @@ -0,0 +1,106 @@ +class CannedResponsesController < ApplicationController + unloadable + + before_filter :find_canned_response, :except => [:new, :create, :index] + before_filter :find_optional_project, :only => [:new, :create, :add, :destroy] + before_filter :find_issue, :only => [:add] + before_filter :require_admin, :only => [:index] + + accept_api_auth :index + + def index + case params[:format] + when 'xml', 'json' + @offset, @limit = api_offset_and_limit + else + @limit = per_page_option + end + + scope = CannedResponse.visible + scope = scope.in_project_or_public(@project) if @project + + @canned_response_count = scope.count + @canned_response_pages = Paginator.new @canned_response_count, @limit, params['page'] + @offset ||= @canned_response_pages.offset + @canned_responses = scope.limit(@limit).offset(@offset).order("#{CannedResponse.table_name}.name") + + respond_to do |format| + format.html + end + end + + def add + @content = HelpdeskMailer.apply_macro(@canned_response.content, @issue.customer, @issue, User.current) + end + + def new + @canned_response = CannedResponse.new + @canned_response.user = User.current + @canned_response.project = @project + @canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin? + end + + def create + @canned_response = CannedResponse.new(params[:canned_response]) + @canned_response.user = User.current + @canned_response.project = params[:canned_response_is_for_all] ? nil : @project + @canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin? + + if @canned_response.save + flash[:notice] = l(:notice_successful_create) + redirect_to_project_or_global + else + render :action => 'new', :layout => !request.xhr? + end + end + + def edit + end + + def update + @canned_response.attributes = params[:canned_response] + @canned_response.project = nil if params[:canned_response_is_for_all] + @canned_response.is_public = false unless User.current.allowed_to?(:manage_public_canned_responses, @project) || User.current.admin? + + if @canned_response.save + flash[:notice] = l(:notice_successful_update) + redirect_to_project_or_global + else + render :action => 'edit' + end + end + + def destroy + @canned_response.destroy + redirect_to_project_or_global + end + +private + def redirect_to_project_or_global + redirect_to @project ? settings_project_path(@project, :tab => 'helpdesk_canned_responses') : path_to_global_setting + end + + def path_to_global_setting + { + :action =>"plugin", + :id => "redmine_contacts_helpdesk", + :controller => "settings", + :tab => 'canned_responses' + } + end + + def find_issue + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_canned_response + @canned_response = CannedResponse.find(params[:id]) + @project = @canned_response.project + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_controller.rb new file mode 100644 index 0000000..f490d02 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_controller.rb @@ -0,0 +1,244 @@ +class HelpdeskController < ApplicationController + unloadable + + before_filter :find_project, :authorize, :except => [:email_note, :update_customer_email] + + accept_api_auth :email_note, :create_ticket + + def save_settings + if request.put? + set_settings + flash[:notice] = l(:notice_successful_update) + end + + redirect_to :controller => 'projects', :action => 'settings', :tab => params[:tab] || 'helpdesk', :id => @project + end + + def show_original + @attachment = Attachment.find(params[:id]) + email = Mail.read(@attachment.diskfile) + part = email.text_part || email.html_part || email + body_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset + plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, body_charset) + headers = email.header.fields.map{|f| "#{f.name}: #{Mail::Encodings.unquote_and_convert_to(f.value, 'utf-8')}"}.join("\n") + @content = headers + "\n\n" + plain_text_body + + render "attachments/file" + end + + def delete_spam + if User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project) + begin + @issue = Issue.find(params[:issue_id]) + @customer = @issue.customer + rescue ActiveRecord::RecordNotFound + render_404 + end + + ActiveRecord::Base.transaction do + ContactsSetting["helpdesk_blacklist", @project.id] = (ContactsSetting["helpdesk_blacklist", @project.id].split("\n") | [@issue.customer.primary_email.strip]).join("\n") + @customer.tickets.map(&:destroy) + @customer.destroy + end + + respond_to do |format| + format.html { redirect_back_or_default(:controller => 'issues', :action => 'index', :project_id => @project) } + format.api { render_api_ok } + end + + else + deny_access + end + end + + def email_note + raise Exception, "Param 'message' should be set" unless params[:message] + @issue = Issue.find(params[:message][:issue_id]) + + raise Exception, "Issue with ID: #{params[:message][:issue_id].to_i} should be present and relate to customer" if @issue.nil? || @issue.customer.nil? + + + @journal = @issue.init_journal(User.current) + @issue.status_id = params[:message][:status_id] if params[:message][:status_id].blank? && IssueStatus.find_by_id(params[:message][:status_id]) + @journal.notes = params[:message][:content] + @issue.save! + + contact = @issue.customer + + HelpdeskMailer.with_activated_perform_deliveries do + if HelpdeskMailer.issue_response(contact, @journal, params).deliver + + @journal_message = JournalMessage.create(:from_address => "", + :to_address => contact.primary_email.downcase, + :is_incoming => false, + :message_date => Time.now, + :contact => contact, + :journal => @journal) + end + end + + respond_to do |format| + format.api { render :action => 'show', :status => :created } + end + + rescue Exception => e + respond_to do |format| + format.api do + @error_messages = [e.message] + render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil + end + end + end + + def create_ticket + raise Exception, "Param 'ticket' should be set" if params[:ticket].blank? + @issue = Issue.new + @issue.project = @project + @issue.author ||= User.current + @issue.safe_attributes = params[:ticket][:issue] + raise Exception, "Contact should have email address" unless params[:ticket][:contact] || params[:ticket][:contact][:email] + + @contact = Contact.find_by_emails([params[:ticket][:contact][:email]]).first + @contact ||= Contact.new(params[:ticket][:contact]) + @contact.projects << @project + + helpdesk_ticket = HelpdeskTicket.new(:from_address => @contact.primary_email, + :to_address => '', + :ticket_date => Time.now, + :customer => @contact, + :is_incoming => true, + :issue => @issue, + :source => HelpdeskTicket::HELPDESK_WEB_SOURCE) + + @issue.helpdesk_ticket = helpdesk_ticket + @issue.assigned_to = @contact.find_assigned_user(@project, @issue.assigned_to) + @issue.save_attachments(params[:attachments] || (params[:ticket][:issue] && params[:ticket][:issue][:uploads])) + if @issue.save + HelpdeskMailer.auto_answer(@contact, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0 + + respond_to do |format| + format.api { redirect_on_create(params) } + end + else + raise Exception, "Can't create issue: #{@issue.errors.full_messages}" + end + + rescue Exception => e + respond_to do |format| + format.api do + @error_messages = [e.message] + HelpdeskLogger.error "API Create Ticket Error: #{e.message}" if HelpdeskLogger + render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil + end + end + end + + def get_mail + set_settings + + msg_count = HelpdeskMailer.check_project(@project.id) + + respond_to do |format| + format.js do + @message = "
        #{l(:label_helpdesk_get_mail_success, :count => msg_count)}
        " + flash.discard + end + format.html {redirect_to :back} + end + rescue Exception => e + respond_to do |format| + format.js do + @message = "
        Error: #{e.message}
        " + Rails.logger.error "Helpdesk MailHandler Error: #{e.message}" if Rails.logger && Rails.logger.error + flash.discard + end + format.html {redirect_to :back} + end + + end + + def update_customer_email + @journal = Journal.find(params[:journal_id]) + @issue = @journal.journalized + @project = @issue.project + @display = HelpdeskSettings[:send_note_by_default, @project] ? 'inline' : 'none' + if @journal.is_incoming? + @contact = @journal.contact + @email = @journal.journal_message.from_address + from_address = HelpdeskSettings['helpdesk_answer_from', @issue.project].blank? ? Setting.mail_from : HelpdeskSettings['helpdesk_answer_from', @issue.project] + @cc_emails = (@issue.helpdesk_ticket.cc_addresses + @journal.journal_message.cc_address.split(',') - [@email, from_address]).uniq + else + @contact = @issue.helpdesk_ticket.last_reply_customer + @email = @issue.helpdesk_ticket.default_to_address + @cc_emails = @issue.helpdesk_ticket.cc_addresses - [@email] + end + end + + private + + def find_project + project_id = params[:project_id] || (params[:ticket] && params[:ticket][:issue] && params[:ticket][:issue][:project_id]) + @project = Project.find(project_id) + rescue ActiveRecord::RecordNotFound + render_404 + end + + def set_settings + set_settings_param("helpdesk_answer_from") + set_settings_param("helpdesk_send_notification") + set_settings_param("helpdesk_is_not_create_contacts") + set_settings_param("helpdesk_created_contact_tag") + set_settings_param("helpdesk_blacklist") + set_settings_param("helpdesk_emails_header") + set_settings_param("helpdesk_answer_subject") + set_settings_param("helpdesk_first_answer_subject") + set_settings_param("helpdesk_first_answer_template") + set_settings_param("helpdesk_emails_footer") + set_settings_param("helpdesk_answered_status") + set_settings_param("helpdesk_reopen_status") + set_settings_param("helpdesk_tracker") + set_settings_param("helpdesk_assigned_to") + set_settings_param("helpdesk_lifetime") + + set_settings_param(:helpdesk_protocol) + set_settings_param(:helpdesk_host) + set_settings_param(:helpdesk_port) + set_settings_param(:helpdesk_password) + set_settings_param(:helpdesk_username) + + set_settings_param(:helpdesk_use_ssl) + set_settings_param(:helpdesk_imap_folder) + set_settings_param(:helpdesk_move_on_success) + set_settings_param(:helpdesk_move_on_failure) + set_settings_param(:helpdesk_apop) + set_settings_param(:helpdesk_delete_unprocessed) + + set_settings_param(:helpdesk_smtp_use_default_settings) + set_settings_param(:helpdesk_smtp_server) + set_settings_param(:helpdesk_smtp_domain) + set_settings_param(:helpdesk_smtp_port) + set_settings_param(:helpdesk_smtp_authentication) + set_settings_param(:helpdesk_smtp_username) + set_settings_param(:helpdesk_smtp_password) + set_settings_param(:helpdesk_smtp_tls) + set_settings_param(:helpdesk_smtp_ssl) + + end + + def set_settings_param(param) + if param == :helpdesk_password || param == :helpdesk_smtp_password + ContactsSetting[param, @project.id] = params[param] if params[param] && !params[param].blank? + else + ContactsSetting[param, @project.id] = params[param] if params[param] + end + end + + def redirect_on_create(options) + if options[:redirect_on_success].to_s.match('^(http|https):\/\/') + redirect_to options[:redirect_on_success].to_s + else + render :text => "Issue #{@issue.id} created", :status => :created, :location => issue_url(@issue) + end + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_mailer_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_mailer_controller.rb new file mode 100644 index 0000000..b0ef1b9 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_mailer_controller.rb @@ -0,0 +1,44 @@ +class HelpdeskMailerController < ActionController::Base + unloadable + before_filter :check_credential + + # Submits an incoming email to ContactsMailer + def index + options = params.dup + if options[:issue].present? + project = Project.find_by_identifier(options[:issue][:project]) + options = HelpdeskMailer.get_issue_options(options, project.id) if project + end + email = options.delete(:email) + if HelpdeskMailer.receive(email, options) + render :nothing => true, :status => :created + else + render :nothing => true, :status => :unprocessable_entity + end + end + + def get_mail + msg_count = 0 + errors = [] + Project.active.has_module(:contacts_helpdesk).each do |project| + begin + msg_count += HelpdeskMailer.check_project(project.id) + rescue Exception => e + errors << e.message + end + + end + + render :status => :ok, :text => {:count => msg_count, :errors => errors}.to_json + + end + + private + + def check_credential + User.current = nil + unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key + render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403 + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_reports_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_reports_controller.rb new file mode 100644 index 0000000..b8fd8ba --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_reports_controller.rb @@ -0,0 +1,35 @@ +class HelpdeskReportsController < ApplicationController + unloadable + menu_item :issues + + helper :helpdesk + helper :queries + include QueriesHelper + + before_filter :find_optional_project, :authorize_global + + def show + retrieve_reports_query + @collector = HelpdeskDataCollectorManager.new(@report).collect_data(@query) + return render_404 unless @collector + respond_to do |format| + format.html + end + end + + private + + def retrieve_reports_query + @report = params[:report] || 'first_response_time' + report_query_class = @report == 'first_response_time' ? HelpdeskReportsFirstResponseQuery : HelpdeskReportsBusiestTimeQuery + if params[:set_filter] || session[:helpdesk_reports_query].nil? || session[:helpdesk_reports_query][:project_id] != (@project ? @project.id : nil) + @query = report_query_class.new(:name => '_', :project => @project) + params.merge!('f' => ['message_date'], 'op' => { 'message_date' => 'm' }) if params['f'].nil? || params['f'].all?(&:blank?) + @query.build_from_params(params) + @query[:filters] = { 'message_date' => { :operator => 'm', :values => [''] } } unless @query[:filters] + session[:helpdesk_reports_query] = { :project_id => @query.project_id, :filters => @query.filters || {} } + else + @query = report_query_class.new(:name => '_', :project => @project, :filters => session[:helpdesk_reports_query][:filters] || {}) + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb new file mode 100644 index 0000000..12c2986 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_search_controller.rb @@ -0,0 +1,201 @@ +class HelpdeskSearchController < ApplicationController + unloadable + + accept_api_auth :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline + + before_filter :require_login + + def usage + # Human/browser probes often stop at /helpdesk_search/issues. Return a + # machine-readable usage response instead of letting Rails raise a route + # exception that looks like an application failure in production.log. + render :json => { + :helpdesk_search => { + :ticket_by_issue => '/helpdesk_search/issues/:issue_id/ticket', + :ticket_by_issue_alias => '/helpdesk_search/issues/:issue_id', + :issues_by_contact => '/helpdesk_search/contacts/:contact_id/issues', + :messages_by_issue => '/helpdesk_search/issues/:issue_id/messages', + :contact_timeline => '/helpdesk_search/contacts/:contact_id/timeline' + } + }, :status => :bad_request + end + + def ticket_by_issue + issue = Issue.find(params[:issue_id]) + return unless authorize_helpdesk_project!(issue.project) + + ticket = HelpdeskTicket.where(:issue_id => issue.id).first + unless ticket + render_404 + return + end + + render :json => {:helpdesk_ticket => serialize_ticket(ticket)} + rescue ActiveRecord::RecordNotFound + render_404 + end + + def issues_by_contact + contact = Contact.find(params[:contact_id]) + tickets = HelpdeskTicket. + includes(:issue => [:project, :status, :tracker, :assigned_to]). + where(:contact_id => contact.id). + order("#{HelpdeskTicket.table_name}.ticket_date DESC"). + limit(api_limit) + + render :json => { + :contact_id => contact.id, + :issues => tickets.map { |ticket| serialize_ticket_issue(ticket) }.compact + } + rescue ActiveRecord::RecordNotFound + render_404 + end + + def messages_by_issue + issue = Issue.find(params[:issue_id]) + return unless authorize_helpdesk_project!(issue.project) + + messages = JournalMessage. + includes(:contact, :journal). + joins(:journal). + where(:journals => {:journalized_type => 'Issue', :journalized_id => issue.id}). + order("#{JournalMessage.table_name}.message_date ASC"). + limit(api_limit) + + render :json => { + :issue_id => issue.id, + :journal_messages => messages.map { |message| serialize_journal_message(message) } + } + rescue ActiveRecord::RecordNotFound + render_404 + end + + def contact_timeline + contact = Contact.find(params[:contact_id]) + tickets = HelpdeskTicket. + includes(:issue => [:project, :status, :tracker]). + where(:contact_id => contact.id). + order("#{HelpdeskTicket.table_name}.ticket_date DESC"). + limit(api_limit) + + messages = JournalMessage. + includes(:contact, :journal). + where(:contact_id => contact.id). + order("#{JournalMessage.table_name}.message_date DESC"). + limit(api_limit) + + events = [] + tickets.each do |ticket| + next unless visible_issue?(ticket.issue) + events << serialize_ticket(ticket).merge(:type => 'helpdesk_ticket', :date => iso8601(ticket.ticket_date)) + end + + messages.each do |message| + issue = message_issue(message) + next unless visible_issue?(issue) + events << serialize_journal_message(message).merge(:type => 'journal_message', :date => iso8601(message.message_date)) + end + + events.sort_by! { |event| event[:date].to_s } + events.reverse! + + render :json => { + :contact_id => contact.id, + :timeline => events.first(api_limit) + } + rescue ActiveRecord::RecordNotFound + render_404 + end + + private + + def authorize_helpdesk_project!(project) + # Search endpoints expose customer/email context, so they reuse the native + # per-project helpdesk visibility permission instead of adding a new role. + return true if project && User.current.allowed_to?(:view_helpdesk_tickets, project) + deny_access + false + end + + def visible_issue?(issue) + issue && issue.project && User.current.allowed_to?(:view_helpdesk_tickets, issue.project) + end + + def api_limit + requested_limit = params[:limit].present? ? params[:limit].to_i : 100 + [[requested_limit, 1].max, 200].min + end + + def serialize_ticket_issue(ticket) + issue = ticket.issue + return nil unless visible_issue?(issue) + + serialize_ticket(ticket).merge( + :issue => { + :id => issue.id, + :project_id => issue.project_id, + :tracker_id => issue.tracker_id, + :tracker_name => issue.tracker.try(:name), + :status_id => issue.status_id, + :status_name => issue.status.try(:name), + :assigned_to_id => issue.assigned_to_id, + :assigned_to_name => issue.assigned_to.try(:name), + :subject => issue.subject, + :created_on => iso8601(issue.created_on), + :updated_on => iso8601(issue.updated_on) + } + ) + end + + def serialize_ticket(ticket) + { + :id => ticket.id, + :issue_id => ticket.issue_id, + :contact_id => ticket.contact_id, + :message_id => ticket.message_id, + :source => ticket.source, + :is_incoming => ticket.is_incoming?, + :from_address => ticket.from_address, + :to_address => ticket.to_address, + :cc_address => ticket.cc_address, + :ticket_date => iso8601(ticket.ticket_date) + } + end + + def serialize_journal_message(message) + journal = message.journal + issue = message_issue(message) + + # Keep this API metadata-only. The external indexer can fetch journal.notes + # with its own read policy; this endpoint should not leak message bodies, + # private notes, attachments, or BCC addresses by accident. + { + :id => message.id, + :journal_id => message.journal_id, + :issue_id => issue.try(:id), + :project_id => issue.try(:project_id), + :contact_id => message.contact_id, + :message_id => message.message_id, + :source => message.source, + :is_incoming => message.is_incoming?, + :from_address => message.from_address, + :to_address => message.to_address, + :cc_address => message.cc_address, + :has_bcc_address => message.bcc_address.present?, + :message_date => iso8601(message.message_date), + :journal_user_id => journal.try(:user_id), + :journal_private_notes => journal.try(:private_notes?), + :journal_has_notes => journal.try(:notes).present? + } + end + + def message_issue(message) + journal = message.journal + return nil unless journal + journal.try(:issue) || journal.try(:journalized) + end + + def iso8601(value) + value.try(:utc).try(:iso8601) + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_tickets_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_tickets_controller.rb new file mode 100644 index 0000000..c232840 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_tickets_controller.rb @@ -0,0 +1,67 @@ +class HelpdeskTicketsController < ApplicationController + unloadable + + before_filter :find_issue, :except => :destroy + before_filter :find_helpdesk_ticket, :only => :destroy + before_filter :authorize + + helper :helpdesk + + def edit + @show_form = "true" + respond_to do |format| + format.js + end + end + + def update + @helpdesk_ticket.attributes = params[:helpdesk_ticket] + @helpdesk_ticket.cc_address = params[:helpdesk_ticket][:cc_address].reject(&:empty?).join(',') if params[:helpdesk_ticket][:cc_address] + @helpdesk_ticket.issue = @issue + @helpdesk_ticket.from_address = @helpdesk_ticket.customer.primary_email if @helpdesk_ticket.customer + + if @helpdesk_ticket.save + flash[:notice] = l(:notice_successful_update) + respond_to do |format| + format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) } + format.api { render_api_ok } + end + else + flash[:error] = @helpdesk_ticket.errors.full_messages.flatten.join("\n") + respond_to do |format| + format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) } + format.api { render_validation_errors(@helpdesk_ticket) } + end + end + end + + def destroy + if @helpdesk_ticket.destroy + flash[:notice] = l(:notice_successful_delete) + respond_to do |format| + format.html { redirect_back_or_default({:controller => 'issues', :action => 'show', :id => @issue}) } + format.api { render_api_ok } + end + else + flash[:error] = l(:notice_unsuccessful_save) + end + end + +private + def find_helpdesk_ticket + @helpdesk_ticket = HelpdeskTicket.find(params[:id]) + @issue = @helpdesk_ticket.issue + @project = @issue.project if @issue + rescue ActiveRecord::RecordNotFound + render_404 + end + + def find_issue + @issue = Issue.find(params[:issue_id]) + @project = @issue.project + @helpdesk_ticket = @issue.helpdesk_ticket || HelpdeskTicket.new(:ticket_date => Time.now, :issue => @issue) + rescue ActiveRecord::RecordNotFound + render_404 + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_votes_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_votes_controller.rb new file mode 100644 index 0000000..fe80439 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_votes_controller.rb @@ -0,0 +1,46 @@ +class HelpdeskVotesController < ApplicationController + unloadable + layout 'public_tickets' + skip_before_filter :check_if_login_required + before_filter :find_ticket, :authorize_ticket + before_filter :fill_data + + helper :issues + + def vote + @ticket.update_vote(params[:vote], params[:vote_comment]) if params[:vote] + end + + def fast_vote + if RedmineHelpdesk.vote_comment_allow? + @ticket.vote = params[:vote] if params[:vote] + render :action => "show" + else + @ticket.update_vote(params[:vote]) if params[:vote] + render :action => "vote" + end + end + +private + + def find_ticket + @ticket = HelpdeskTicket.find(params[:id]) + @issue = @ticket.issue + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def authorize_ticket(action = params[:action]) + allow = true + allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.vote_allow? + allow &&= !@issue.is_private + render_404 unless allow + end + + def fill_data + @previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status + @total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_widget_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_widget_controller.rb new file mode 100644 index 0000000..7ec751d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/helpdesk_widget_controller.rb @@ -0,0 +1,206 @@ +class HelpdeskWidgetController < ApplicationController + unloadable + layout false + helper :custom_fields + protect_from_forgery :except => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe] + skip_before_filter :check_if_login_required, :only => [:widget, :load_form, :load_custom_fields, :avatar, :create_ticket, :iframe] + + before_filter :prepare_data, :only => [:load_custom_fields, :create_ticket] + after_filter :set_access_control_header + + def load_form + render :json => schema.to_json + end + + def load_custom_fields + @issue = @project.issues.build(:tracker => @tracker) if @tracker + @enabled_cf = HelpdeskSettings["helpdesk_widget_available_custom_fields", nil] + end + + def avatar + user = User.where(:login => params[:login]).first + return render :nothing => true, :status => 404 unless user + if user.try(:avatar).nil? + avatar_thumb, avatar_type = gravatar_avatar(user) if Setting.gravatar_enabled? + else + avatar_thumb, avatar_type = local_avatar(user.avatar) + end + return render :nothing => true, :status => 404 unless avatar_thumb + send_avatar(avatar_thumb, avatar_type) + end + + def send_avatar(avatar_thumb, avatar_type) + send_file avatar_thumb, :filename => (request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(params[:login]) : params[:login]), + :type => avatar_type, + :disposition => 'inline' + end + + def gravatar_avatar(user) + email = user.mail if user.respond_to?(:mail) + email = user.to_s[/<(.+?)>/, 1] unless email + return [nil, nil] unless email + default = Setting.gravatar_default ? CGI::escape(Setting.gravatar_default) : '' + temp_file = Tempfile.new([user.login, '.jpeg']) + temp_file.binmode + open("http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}?rating=PG&size=54&default=#{default}") do |url_file| + temp_file.write(url_file.read) + end + temp_file.rewind + [temp_file, 'image/jpeg'] + end + + def local_avatar(user_avatar) + return nil unless user_avatar.readable? || user_avatar.thumbnailable? + if (defined?(RedmineContacts::Thumbnail) == 'constant') && Redmine::Thumbnail.convert_available? + target = File.join(user_avatar.class.thumbnails_storage_path, "#{user_avatar.id}_#{user_avatar.digest}_54x54.thumb") + thumbnail = RedmineContacts::Thumbnail.generate(user_avatar.diskfile, target, '54x54') + elsif Redmine::Thumbnail.convert_available? + thumbnail = user_avatar.thumbnail(:size => '54x54') + else + thumbnail = user_avatar.diskfile + end + [thumbnail, detect_content_type(user_avatar)] + end + + def create_ticket + @issue = prepare_issue + @issue.helpdesk_ticket = prepare_helpdesk_ticket + result = + if valid_email? && @issue.save + save_attachment(@issue) + HelpdeskMailer.auto_answer(@issue.helpdesk_ticket.customer, @issue).deliver if HelpdeskSettings["helpdesk_send_notification", @project].to_i > 0 + { :result => true, :errors => [] } + else + { :result => false, :errors => prepared_errors } + end + render :json => result + end + + private + + def prepare_data + @project = Project.find(params[:project_id]) + @tracker = @project.trackers.where(:id => params[:tracker_id]).first + end + + def schema + if HelpdeskSettings["helpdesk_widget_enable", nil].to_i > 0 + projects = Project.has_module('contacts_helpdesk').where(:id => HelpdeskSettings[:helpdesk_widget_available_projects, nil]) + else + projects = [] + end + data_schema = {} + data_schema[:projects] = Hash[projects.map { |project| [project.name.capitalize, project.id] }] + data_schema[:projects_data] = {} + projects.each do |project| + data_schema[:projects_data][project.id] = {} + if HelpdeskSettings["helpdesk_tracker", project] && HelpdeskSettings["helpdesk_tracker", project] != 'all' + data_schema[:projects_data][project.id][:trackers] = Hash[Tracker.where(id: HelpdeskSettings["helpdesk_tracker", project]) + .map { |tracker| [tracker.name, tracker.id] }] + else + data_schema[:projects_data][project.id][:trackers] = Hash[project.trackers.map { |tracker| [tracker.name, tracker.id] }] + end + end + data_schema[:custom_fields] = Hash[IssueCustomField.where(id: HelpdeskSettings["helpdesk_widget_available_custom_fields", nil]) + .map { |custom_field| [custom_field.name, custom_field.id] }] + data_schema[:avatar] = HelpdeskSettings[:helpdesk_widget_avatar_login, nil] + data_schema + end + + def prepared_errors + errors_hash = @issue.errors.dup + # Username + if errors_hash[:'helpdesk_ticket.customer.first_name'].present? + @issue.errors.delete(:'helpdesk_ticket.customer.first_name') + @issue.errors[:username] = errors_hash[:'helpdesk_ticket.customer.first_name'].collect { |error| ['Username', error].join(' ') } + end + + # Subject + if errors_hash[:subject].present? + errors = errors_hash[:subject].collect { |error| ['Subject', error].join(' ') } + @issue.errors[:subject].clear + @issue.errors[:subject] = errors + end + + # Description + if params[:issue][:description].empty? + @issue.errors[:description] = I18n.t(:label_helpdesk_widget_ticket_error_description) + end + + # Nested objects + if errors_hash[:'helpdesk_ticket.customer.projects'].present? + @issue.errors.delete(:'helpdesk_ticket.customer.projects') + end + @issue.errors + end + + def prepare_issue + redmine_user = User.where(id: params[:redmine_user]).first + author = redmine_user.present? && redmine_user.allowed_to?(:edit_helpdesk_tickets, @project) ? redmine_user : User.anonymous + issue = @project.issues.build(:tracker => @tracker, :author => author) + issue.safe_attributes = params[:issue].deep_dup + issue.assigned_to = widget_contact.find_assigned_user(@project, HelpdeskSettings["helpdesk_assigned_to", @project]) + issue + end + + def prepare_helpdesk_ticket + HelpdeskTicket.new(:from_address => params[:email], + :ticket_date => Time.now, + :customer => widget_contact, + :issue => @issue, + :source => HelpdeskTicket::HELPDESK_WEB_SOURCE) + end + + def save_attachment(issue) + return unless params[:attachment].present? + attachment_hash = split_base64(params[:attachment]) + attachment = Attachment.new(file: Base64.decode64(attachment_hash[:data])) + attachment.filename = params[:attachment_name] || [Redmine::Utils.random_hex(16), attachment_hash[:extension]].join('.') + attachment.content_type = attachment_hash[:type] + attachment.author = User.anonymous + issue.attachments << attachment + issue.save + end + + def split_base64(uri) + matcher = uri.match(/^data:(.*?)\;(.*?),(.*)$/) + { type: matcher[1], + encoder: matcher[2], + data: matcher[3], + extension: matcher[1].split('/')[1] } + end + + def widget_contact + return @widget_contact if @widget_contact + contacts = Contact.find_by_emails([params[:email]]) + return @widget_contact = contacts.first if contacts.any? + @widget_contact = Contact.new(:email => params[:email]) + @widget_contact.first_name, @widget_contact.last_name = params[:username].split(' ') + @widget_contact.projects << @project + @widget_contact + end + + def set_access_control_header + headers['Access-Control-Allow-Origin'] = '*' + headers['X-Frame-Options'] = '*' + end + + def valid_email? + if params[:email].empty? + @issue.errors[:email] = 'Email cannot be empty' + return false + elsif params[:email].match(/\A([\w\.\+\-]+)@([\w\-]+\.)+([\w]{2,})\z/i).nil? + @issue.errors[:email] = 'Email is incorrect' + return false + end + true + end + + def detect_content_type(attachment) + content_type = attachment.content_type + if content_type.blank? + content_type = Redmine::MimeType.of(attachment.filename) + end + content_type.to_s + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/mail_fetcher_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/mail_fetcher_controller.rb new file mode 100644 index 0000000..de393c6 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/mail_fetcher_controller.rb @@ -0,0 +1,89 @@ +class MailFetcherController < ApplicationController + unloadable + require 'timeout' + + before_filter :check_credential + + def receive_imap + imap_options = {:host => params['host'], + :port => params['port'], + :ssl => params['ssl'], + :username => params['username'], + :password => params['password'], + :folder => params['folder'], + :move_on_success => params['move_on_success'], + :move_on_failure => params['move_on_failure']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] } + options[:allow_override] = params['allow_override'] if params['allow_override'] + options[:unknown_user] = params['unknown_user'] if params['unknown_user'] + options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check'] + + begin + Timeout::timeout(15){ Redmine::IMAP.check(imap_options, options) } + rescue Exception => e + @error_messages = [e.message] + end + + + if @error_messages.blank? + respond_to do |format| + format.html { render :nothing => true, :status => :ok } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil } + format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil} + end + end + + end + + def receive_pop3 + pop_options = {:host => params['host'], + :port => params['port'], + :apop => params['apop'], + :username => params['username'], + :password => params['password'], + :delete_unprocessed => params['delete_unprocessed']} + + options = { :issue => {} } + %w(project status tracker category priority).each { |a| options[:issue][a.to_sym] = params[a] if params[a] } + options[:allow_override] = params['allow_override'] if params['allow_override'] + options[:unknown_user] = params['unknown_user'] if params['unknown_user'] + options[:no_permission_check] = params['no_permission_check'] if params['no_permission_check'] + + begin + Timeout::timeout(15){ Redmine::POP3.check(pop_options, options) } + rescue Exception => e + @error_messages = [e.message] + end + + + if @error_messages.blank? + respond_to do |format| + format.html { render :nothing => true, :status => :ok } + format.api { render_api_ok } + end + else + respond_to do |format| + format.html { render :text => @error_messages, :status => :unprocessable_entity, :layout => nil } + format.api { render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil} + end + end + + + end + + private + + def check_credential + User.current = nil + unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key + render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403 + end + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/controllers/public_tickets_controller.rb b/plugins/redmine_contacts_helpdesk/app/controllers/public_tickets_controller.rb new file mode 100644 index 0000000..a36d099 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/controllers/public_tickets_controller.rb @@ -0,0 +1,81 @@ +class PublicTicketsController < ApplicationController + unloadable + layout 'public_tickets' + + skip_before_filter :check_if_login_required + before_filter :find_ticket, :authorize_ticket + + helper :issues + helper :attachments + helper :journals + helper :custom_fields + + def show + @previous_tickets = @ticket.customer.tickets.where(:is_private => false).includes([:status, :helpdesk_ticket]).order_by_status + + @total_spent_hours = @previous_tickets.map.sum(&:total_spent_hours) + + @journals = @issue.journals.includes(:user). + includes(:details). + order("#{Journal.table_name}.created_on ASC"). + where(:private_notes => false). + where("EXISTS (SELECT * FROM #{JournalMessage.table_name} WHERE #{JournalMessage.table_name}.journal_id = #{Journal.table_name}.id)") + @journals = @journals.each_with_index {|j,i| j.indice = i+1}.to_a + @journals.reverse! if User.current.wants_comments_in_reverse_order? + @journal = @issue.journals.new + + @allowed_statuses = @issue.new_statuses_allowed_to(User.current) + @edit_allowed = User.current.allowed_to?(:edit_issues, @project) + @priorities = IssuePriority.active + @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project) + prepend_view_path "app/views/issues" + + end + + def add_comment + @journal = @issue.journals.new(params[:journal]) + @issue.status_id = HelpdeskSettings["helpdesk_reopen_status", @issue.project_id] unless HelpdeskSettings["helpdesk_reopen_status", @issue.project_id].blank? + @journal.user = User.current + @journal.journal_message = JournalMessage.new(:from_address => @ticket.customer_email, + :contact => @ticket.customer, + :journal => @journal, + :is_incoming => true, + :message_date => Time.now) + if @issue.save + flash[:notice] = l(:notice_successful_create) + @journal.save + end + redirect_back_or_default(public_ticket_path(@ticket, @ticket.token)) + end + + def render_404(options={}) + @message = l(:notice_file_not_found) + respond_to do |format| + format.html { + render :template => 'common/error', :status => 404 + } + format.any { head 404 } + end + return false + end + + + private + + def find_ticket + @ticket = HelpdeskTicket.find(params[:id]) + @issue = @ticket.issue + @project = @issue.project + rescue ActiveRecord::RecordNotFound + render_404 + end + + def authorize_ticket(action = params[:action]) + allow = true + allow &&= RedmineHelpdesk.public_comments? if (action.to_s == "add_comment") + allow &&= (@ticket.token == params[:hash]) && RedmineHelpdesk.public_tickets? + allow &&= !@issue.is_private + render_404 unless allow + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_helper.rb b/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_helper.rb new file mode 100644 index 0000000..bf04d77 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_helper.rb @@ -0,0 +1,83 @@ +module HelpdeskHelper + def helpdesk_ticket_source_icon(helpdesk_ticket) + case helpdesk_ticket.source + when HelpdeskTicket::HELPDESK_EMAIL_SOURCE + "icon-email" + when HelpdeskTicket::HELPDESK_PHONE_SOURCE + "icon-call" + when HelpdeskTicket::HELPDESK_WEB_SOURCE + "icon-web" + when HelpdeskTicket::HELPDESK_TWITTER_SOURCE + "icon-twitter" + else + "icon-helpdesk" + end + end + + def helpdesk_tickets_source_for_select + [[l(:label_helpdesk_tickets_email), HelpdeskTicket::HELPDESK_EMAIL_SOURCE.to_s], + [l(:label_helpdesk_tickets_phone), HelpdeskTicket::HELPDESK_PHONE_SOURCE.to_s], + [l(:label_helpdesk_tickets_web), HelpdeskTicket::HELPDESK_WEB_SOURCE.to_s], + [l(:label_helpdesk_tickets_conversation), HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE.to_s] + ] + end + + def helpdesk_send_as_for_select + [[l(:label_helpdesk_not_send), ''], + [l(:label_helpdesk_send_as_notification), HelpdeskTicket::SEND_AS_NOTIFICATION.to_s], + [l(:label_helpdesk_send_as_message), HelpdeskTicket::SEND_AS_MESSAGE.to_s] + ] + end + + def show_customer_vote(vote, comment) + case vote + when 2 + generate_vote_link(vote, 'icon-awesome', comment) + when 1 + generate_vote_link(vote, 'icon-justok', comment) + when 0 + generate_vote_link(vote, 'icon-notgood', comment) + end + end + + def generate_vote_link(vote, vote_class, title) + "
        #{ HelpdeskTicket.vote_message(vote) }
        ".html_safe + end + + def render_helpdesk_chart(report_name, issues_scope) + render :partial => 'helpdesk_reports/chart', :locals => { :report => report_name, :issues_scope => issues_scope } + end + + def helpdesk_time_label(seconds) + hours, minutes = seconds.divmod(60).first.divmod(60) + "#{hours}#{l(:label_helpdesk_hour)} #{minutes}#{l(:label_helpdesk_minute)}".html_safe + end + + def slim_helpdesk_time_label(seconds) + hours, minutes = seconds.divmod(60).first.divmod(60) + "#{hours}#{l(:label_helpdesk_hour)} #{minutes}#{l(:label_helpdesk_minute)}".html_safe + end + + def progress_in_percents(value) + return '0%'.html_safe if value.zero? + "#{value}%".html_safe + end + + def mirror_progress_in_percents(value) + return '0%'.html_safe if value.zero? + "#{value}%".html_safe + end + + def process_deviation(before, now, time = true) + ["#{l(:label_helpdesk_report_previous)}: #{time ? slim_helpdesk_time_label(before) : before}", + "#{l(:label_helpdesk_report_deviation)}: #{time ? slim_helpdesk_time_label(calculate_deviation(before, now)) : calculate_deviation(before, now)}"].join("\n").html_safe + end + + def calculate_deviation(before, now) + before > now ? before - now : now - before + end + + def helpdesk_reply_link + link_to l(:label_helpdesk_reply), edit_issue_path(@issue), :onclick => 'showWithSendAndScrollTo("update", "issue_notes"); return false;', :class => 'icon icon-reply' + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_mailer_helper.rb b/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_mailer_helper.rb new file mode 100644 index 0000000..848d1d0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/helpers/helpdesk_mailer_helper.rb @@ -0,0 +1,13 @@ +# encoding: utf-8 +# include RedCloth + +module HelpdeskMailerHelper + def textile(text) + Redmine::WikiFormatting.to_html(Setting.text_formatting, text) + end + + def message_sender(email) + sender = email.reply_to.try(:first) || email.from_addrs.try(:first) + sender.to_s.strip + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/helpers/public_tickets_helper.rb b/plugins/redmine_contacts_helpdesk/app/helpers/public_tickets_helper.rb new file mode 100644 index 0000000..3f5619a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/helpers/public_tickets_helper.rb @@ -0,0 +1,35 @@ +module PublicTicketsHelper + include HelpdeskHelper + + def authoring_public(journal, options={}) + if journal.journal_message && journal.journal_message.from_address + l(options[:label] || :label_added_time_by, :author => mail_to(journal.journal_message.contact_email), :age => ticket_time_tag(journal.created_on)).html_safe + else + l(options[:label] || :label_added_time_by, :author => journal.user.name, :age => ticket_time_tag(journal.created_on)).html_safe + end + end + + def ticket_time_tag(time) + text = distance_of_time_in_words(Time.now, time) + content_tag('acronym', text, :title => format_time(time)) + end + + def link_to_attachments_with_hash(container, options = {}) + options.assert_valid_keys(:author, :thumbnails) + + if container.attachments.any? + options = {:deletable => container.attachments_deletable?, :author => true}.merge(options) + render :partial => 'attachment_links', + :locals => {:attachments => container.attachments, :options => options, :thumbnails => (options[:thumbnails] && Setting.thumbnails_enabled?)} + end + end + + def link_to_attachment_with_hash(attachment, options={}) + text = options.delete(:text) || attachment.filename + route_method = options.delete(:download) ? :hashed_download_named_attachment_path : :hashed_named_attachment_path + html_options = options.slice!(:only_path) + url = send(route_method, attachment, @ticket.id, @ticket.token, attachment.filename, options) + link_to text, url, html_options + end + +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/models/canned_response.rb b/plugins/redmine_contacts_helpdesk/app/models/canned_response.rb new file mode 100644 index 0000000..9abf7a5 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/canned_response.rb @@ -0,0 +1,28 @@ +class CannedResponse < ActiveRecord::Base + unloadable + attr_accessible :name, :content, :is_public + + belongs_to :project + belongs_to :user + + validates_presence_of :name, :content + validates_length_of :name, :maximum => 255 + + scope :visible, lambda {|*args| + user = args.shift || User.current + base = Project.allowed_to_condition(user, :view_helpdesk_tickets, *args) + user_id = user.logged? ? user.id : 0 + + eager_load(:project).where("(#{CannedResponse.table_name}.project_id IS NULL OR (#{base})) AND (#{CannedResponse.table_name}.is_public = ? OR #{CannedResponse.table_name}.user_id = ?)", true, user_id) + } + + scope :in_project_or_public, lambda {|project| + where("(#{CannedResponse.table_name}.project_id IS NULL) OR #{CannedResponse.table_name}.project_id = ?", project) + } + + # Returns true if the query is visible to +user+ or the current user. + def visible?(user=User.current) + (project.nil? || user.allowed_to?(:view_helpdesk_tickets, project)) && (self.is_public? || self.user_id == user.id) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_busiest_time.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_busiest_time.rb new file mode 100644 index 0000000..ff8ddff --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_busiest_time.rb @@ -0,0 +1,148 @@ +class HelpdeskDataCollectorBusiestTime + MAX_WEIGHT = 200 + + RESPONSE_INTERVALS = {'6_8h' => [6, 8], + '8_10h' => [8, 10], + '10_12h' => [10, 12], + '12_14h' => [12, 14], + '14_16h' => [14, 16], + '16_18h' => [16, 18], + '18_20h' => [18, 20], + '20_22h' => [20, 22], + '22_0h' => [22, 0], + '0_2h' => [0, 2], + '2_4h' => [2, 4], + '4_6h' => [4, 6]} + + def columns + @columns ||= collect_columns + end + + def issue_weight + @issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil + end + + # New tickets + def new_issues_count + return @new_issues_count if @new_issues_count + condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'issues', 'created_on') + @new_issues_count ||= @issues.where(condition).count + end + + def previous_new_issues_count + return @previous_new_issues_count if @previous_new_issues_count + condition = previous_query.send('sql_for_field', nil, previous_query.filters['message_date'][:operator], nil, 'issues', 'created_on') + @previous_new_issues_count ||= @previous_issues.where(condition).count + end + + def new_issue_count_progress + return 0 if previous_new_issues_count.zero? + calculate_progress(previous_new_issues_count, new_issues_count) + end + + # New contacts + def contacts_count + contacts.count + end + + def previous_contacts_count + previous_contacts.count + end + + def total_contacts_count_progress + return 0 if previous_contacts_count.zero? + calculate_progress(previous_contacts_count, contacts_count) + end + + # Total incoming + def issues_count + @issues_count ||= @issues.count + @journal_messages.count + end + + def previous_issues_count + @previous_issues_count ||= @previous_issues.count + @previous_journal_messages.count + end + + def issue_count_progress + return 0 if previous_issues_count.zero? + calculate_progress(previous_issues_count, issues_count) + end + + private + + def initialize(query) + @query = query + @issues = with_created_issues(@query) + @journal_messages = JournalMessage.where(:is_incoming => true). + where(@query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date')) + @previous_issues = with_created_issues(previous_query) + @previous_journal_messages = JournalMessage.where(:is_incoming => true). + where(previous_query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date')) + end + + def collect_columns + columns = [] + RESPONSE_INTERVALS.each do |interval_name, interval_hours| + interval_objects_count = find_incoming_objects_count(interval_hours) + columns << { :name => interval_name, :issues_count => interval_objects_count, + :issues_percent => ((interval_objects_count.to_f / issues_count.to_f) * 100).round(2) } + end + columns + end + + def find_incoming_objects_count(interval) + interval_start = interval.first + interval_end = interval.last - 1 < 0 ? 23 : interval.last - 1 + interval_issues = @issues.each.select do |issue| + issue_time = timezone ? issue.created_on.in_time_zone(timezone) : issue.created_on.localtime + interval_start <= issue_time.hour && issue_time.hour <= interval_end + end + interval_messages = @journal_messages.each.select do |message| + message_time = timezone ? message.message_date.in_time_zone(timezone) : message.message_date.localtime + interval_start <= message_time.hour && message_time.hour <= interval_end + end + interval_issues.count + interval_messages.count + end + + def timezone + @timezone ||= User.current.time_zone + end + + def previous_query + return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil? + return @previous_query if @previous_query + + previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join + previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] }) + @previous_query = HelpdeskReportsBusiestTimeQuery.new(:name => '_', :project => @query.project, :filters => previous_filters) + @previous_query + end + + def with_created_issues(query) + condition = query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'issues', 'created_on') + created_ids = Issue.joins(:project).visible.where(:project_id => query.project).where(condition).pluck(:id) + Issue.where(:id => query.issues.pluck(:id) | created_ids).joins(:helpdesk_ticket) + end + + def contacts + return @contacts if @contacts + condition = @query.send('sql_for_field', nil, @query.filters['message_date'][:operator], nil, 'contacts', 'created_on') + @contacts = Contact.where(:id => @issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition) + end + + def previous_contacts + return @previous_contacts if @previous_contacts + condition = @query.send('sql_for_field', nil, @previous_query.filters['message_date'][:operator], nil, 'contacts', 'created_on') + @previous_contacts = Contact.where(:id => @previous_issues.joins(:customer).map(&:customer).map(&:id).uniq).where(condition) + end + + def calculate_progress(before, now) + progress = + if before.to_f > now.to_f + 100 - (now.to_f * 100 / before.to_f) + else + (100 - (before.to_f * 100 / now.to_f)) * -1 + end + progress.round + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_first_response.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_first_response.rb new file mode 100644 index 0000000..8dcec9c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_first_response.rb @@ -0,0 +1,178 @@ +class HelpdeskDataCollectorFirstResponse + MAX_WEIGHT = 200 + + RESPONSE_INTERVALS = { '0_1h' => [0, 1], + '1_2h' => [1, 2], + '2_4h' => [2, 4], + '4_8h' => [4, 8], + '8_12h' => [8, 12], + '12_24h' => [12, 24], + '24_48h' => [24, 48], + '48_0h' => [48, 0] } + attr_reader :issues + attr_reader :previous_issues + + def columns + @columns ||= collect_columns + end + + def issue_weight + @issue_weight ||= (MAX_WEIGHT.to_f / columns.map { |column| column[:issues_count] }.sort.last).ceil + end + + # First response time + def average_response_time + @average_response_time ||= median(HelpdeskTicket.where(:issue_id => issues.pluck(:id)).pluck(:first_response_time)) + end + + def previous_average_response_time + return 0 if previous_issues_count.zero? + @previous_average_response_time ||= median(HelpdeskTicket.where(:issue_id => previous_issues.pluck(:id)).pluck(:first_response_time)) + end + + def average_response_time_progress + return 0 if previous_issues_count.zero? + calculate_progress(previous_average_response_time, average_response_time) + end + + # Time to close + def average_close_time + return @average_close_time if @average_close_time + closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id) + return 0 if closed_issue_ids.empty? + @average_close_time = median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time)) + end + + def previous_average_close_time + return @previous_average_close_time if @previous_average_close_time.present? + closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id) + return 0 if closed_issue_ids.empty? + @previous_average_close_time ||= median(HelpdeskTicket.where(:issue_id => closed_issue_ids).pluck(:resolve_time)) + end + + def average_close_time_progress + closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id) + return 0 if closed_issue_ids.empty? + calculate_progress(previous_average_close_time, average_close_time) + end + + # Average responses count + def average_response_count + return @average_response_count if @average_response_count + closed_issue_ids = issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id) + return @average_response_count = 0 if closed_issue_ids.empty? + journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'"). + where(journal_message_date_condition(@query)). + where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id) + @average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values) + end + + def previous_average_response_count + return 0 if previous_issues_count.zero? + return @previous_average_response_count if @previous_average_response_count + closed_issue_ids = previous_issues.joins(:status).where("#{IssueStatus.table_name}.is_closed = ?", true).pluck(:id) + return @previous_average_response_count = 0 if closed_issue_ids.empty? + journal_ids = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'"). + where(journal_message_date_condition(previous_query)). + where("journals.journalized_id IN (#{closed_issue_ids.join(',')})").pluck(:journal_id) + + @previous_average_response_count = median(Journal.where(:id => journal_ids).group(:journalized_id).count(:id).values) + end + + def average_response_count_progress + return 0 if previous_average_response_count.zero? + calculate_progress(previous_average_response_count, average_response_count) + end + + # Total replies + def total_response_count + return @total_response_count if @total_response_count + @total_response_count = JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'"). + where(journal_message_date_condition(@query)). + where("journals.journalized_id IN (#{issues.pluck(:id).join(',')})"). + count + end + + def previous_total_response_count + return 0 if previous_issues_count.zero? + return @previous_total_response_count if @previous_total_response_count + @previous_total_response_count ||= JournalMessage.joins(:journal).where("journals.journalized_type = 'Issue'"). + where(journal_message_date_condition(previous_query)). + where("journals.journalized_id IN (#{previous_issues.pluck(:id).join(',')})"). + count + end + + def total_response_count_progress + return 0 if previous_total_response_count.zero? + calculate_progress(previous_total_response_count, total_response_count) + end + + def issues_count + @issues_count ||= issues.count + end + + def previous_issues_count + @previous_issues_count ||= previous_issues.count + end + + private + + def initialize(query) + @query = query + @issues = @query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq + @previous_issues = previous_query.issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > 0').uniq + end + + def collect_columns + columns = [] + RESPONSE_INTERVALS.each do |interval_name, interval_hours| + interval_issues_count = find_issues_count(interval_hours) + columns << { :name => interval_name, :issues_count => interval_issues_count, + :issues_percent => ((interval_issues_count.to_f / issues_count.to_f) * 100).round(2) } + end + columns + end + + def find_issues_count(interval) + interval_start = (interval.first.hours + 1).to_i + interval_end = interval.last.hours.to_i + interval_issues = + if interval.last > 0 + issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time BETWEEN ? AND ?', interval_start, interval_end) + else + issues.joins(:helpdesk_ticket).where('helpdesk_tickets.first_response_time > ?', interval_start) + end + interval_issues.count + end + + def previous_query + return if @query[:filters].nil? || @query[:filters]['message_date'].nil? || @query[:filters]['message_date'][:operator].nil? + return @previous_query if @previous_query + + previous_operator = ['pre_', @query[:filters]['message_date'][:operator]].join + previous_filters = @query[:filters].merge('message_date' => { :operator => previous_operator, :values => [Date.today.to_s] }) + @previous_query = HelpdeskReportsFirstResponseQuery.new(:name => '_', :project => @query.project, :filters => previous_filters) + @previous_query + end + + def journal_message_date_condition(query) + query.send('sql_for_field', nil, query.filters['message_date'][:operator], nil, 'journal_messages', 'message_date') + end + + def median(array) + return 0 if array.compact.empty? + range = array.sort.reverse + middle = range.count / 2 + (range.count % 2).zero? ? (range[middle - 1] + range[middle]) / 2 : range[middle] + end + + def calculate_progress(before, now) + progress = + if before.to_f > now.to_f + 100 - (now.to_f * 100 / before.to_f) + else + (100 - (before.to_f * 100 / now.to_f)) * -1 + end + progress.round + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_manager.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_manager.rb new file mode 100644 index 0000000..7df0a0a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_data_collector_manager.rb @@ -0,0 +1,14 @@ +class HelpdeskDataCollectorManager + def initialize(report) + @report = report + end + + def collect_data(query) + case @report + when 'first_response_time' + HelpdeskDataCollectorFirstResponse.new(query) + when 'busiest_time_of_day' + HelpdeskDataCollectorBusiestTime.new(query) + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb new file mode 100644 index 0000000..4645c5f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_mailer.rb @@ -0,0 +1,823 @@ +require "digest/md5" + +class HelpdeskMailer < MailHandler + include HelpdeskMailerHelper + include AbstractController::Callbacks + after_filter :set_delivery_options + + attr_reader :contact, :user, :email, :project + + def self.default_url_options + { :host => Setting.host_name, :protocol => Setting.protocol } + end + + def self.with_activated_perform_deliveries + perform_delivery_state = ActionMailer::Base.perform_deliveries + ActionMailer::Base.perform_deliveries = true + yield + ensure + ActionMailer::Base.perform_deliveries = perform_delivery_state + end + + def issue_response(contact, journal, options={}) + @project = journal.issue.project + to_address = options[:to_address] || (journal.journal_message && journal.journal_message.to_address) || contact.primary_email + cc_address = options[:cc_address] || (journal.journal_message && journal.journal_message.cc_address) + bcc_address = options[:bcc_address] || (journal.journal_message && journal.journal_message.bcc_address) + from_address = options[:from_address] || (!HelpdeskSettings["helpdesk_answer_from", journal.issue.project].blank? && HelpdeskSettings["helpdesk_answer_from", journal.issue.project] )|| Setting.mail_from + in_reply_to = options[:in_reply_to] || ((journal.issue.helpdesk_ticket.blank? || journal.issue.helpdesk_ticket.message_id.blank?) ? '' : "<#{journal.issue.helpdesk_ticket.message_id}>") + + headers['X-Redmine-Ticket-ID'] = journal.issue.id.to_s + @email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_header", journal.issue.project].blank? + @email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", journal.issue.project], contact, journal.issue, journal.user) unless HelpdeskSettings["helpdesk_emails_footer", journal.issue.project].blank? + @email_body = self.class.apply_macro(journal.notes, contact, journal.issue, journal.user) + @email_body = attachment_macro(@email_body, journal.issue) + + raise MissingInformation.new(l(:text_helpdesk_to_address_cant_be_blank)) if to_address.blank? + raise MissingInformation.new(l(:text_helpdesk_message_body_cant_be_blank)) if @email_body.blank? + raise MissingInformation.new(l(:text_helpdesk_from_address_cant_be_blank)) if from_address.blank? + + subject_macro = self.class.apply_macro(HelpdeskSettings["helpdesk_answer_subject", journal.issue.project], contact, journal.issue) + # subject_macro += " - [##{journal.issue.id}]" if !subject_macro.blank? && !subject_macro.include?("##{journal.issue.id}]") + @email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, journal.issue.project].to_s.html_safe + + extract_attachments(journal) + + if journal.details.blank? && journal.private_notes? && journal.notes.present? + details_journal = Journal.where('id != ?', journal.id).where(:created_on => journal.created_on).first + extract_attachments(details_journal) if details_journal + end + + mail :from => self.class.apply_from_macro(from_address.to_s, journal.user), + :to => to_address.to_s, + :cc => cc_address.to_s, + :bcc => bcc_address.to_s, + :in_reply_to => in_reply_to.to_s, + :subject => (subject_macro.blank? ? journal.issue.subject + " [#{journal.issue.tracker} ##{journal.issue.id}]" : subject_macro) do |format| + format.text { render 'email_layout' } + format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"] + end + end + + def extract_attachments(journal) + journal.details.where(:property => 'attachment').each do |attachment_journal| + if attach = Attachment.where(:id => attachment_journal.prop_key).first + attachments[attach.filename] = File.open(attach.diskfile, 'rb') { |io| io.read } + end + end + end + + def auto_answer(contact, issue) + @project = issue.project + + headers['X-Redmine-Ticket-ID'] = issue.id.to_s + headers['X-Auto-Response-Suppress'] = 'oof' + + confirmation_body = self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_template", issue.project_id], contact, issue) + + @email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project_id].to_s.html_safe + @email_body = confirmation_body + from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project] + + mail :from => self.class.apply_from_macro(from_address.to_s, nil), + :to => contact.primary_email, + :cc => issue.helpdesk_ticket.try(:cc_address), + :subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || "Helpdesk auto answer [Case ##{issue.id}]", + :in_reply_to => issue.helpdesk_ticket.try(:message_id) do |format| + format.text { render 'email_layout'} + format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"] + end + + logger.info "##{issue.id}: Sending confirmation to #{contact.primary_email}" if logger + end + + def initial_message(contact, issue, params) + @project = issue.project + + headers['X-Redmine-Ticket-ID'] = issue.id.to_s + @email_header = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_header", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_header", issue.project].blank? + @email_footer = self.class.apply_macro(HelpdeskSettings["helpdesk_emails_footer", issue.project], contact, issue, issue.author) unless HelpdeskSettings["helpdesk_emails_footer", issue.project].blank? + @email_body = self.class.apply_macro(issue.description, contact, issue, issue.author) + + @email_body = attachment_macro(@email_body, issue) + + raise MissingInformation.new("Contact #{contact.name} should have mail") if contact.email.blank? + raise MissingInformation.new("Message shouldn't be blank") if @email_body.blank? + + @email_stylesheet = HelpdeskSettings[:helpdesk_helpdesk_css, issue.project].to_s.html_safe + + params[:attachments].each_value do |mail_attachment| + if file = mail_attachment['file'] + file.rewind if file + attachments[file.original_filename] = file.read + file.rewind if file + elsif token = mail_attachment['token'] + if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ + attachment_id, attachment_digest = $1, $2 + if a = Attachment.where(:id => attachment_id, :digest => attachment_digest).first + attachments[a.filename] = File.open(a.diskfile, 'rb'){|io| io.read} + end + end + end + end unless params[:attachments].blank? + + to_address = (params[:helpdesk] && !params[:helpdesk][:to_address].blank?) ? params[:helpdesk][:to_address] : contact.primary_email + from_address = HelpdeskSettings["helpdesk_answer_from", issue.project].blank? ? Setting.mail_from : HelpdeskSettings["helpdesk_answer_from", issue.project] + + logger.error "##{issue.id}: From address couldn't be black" if from_address.blank? && logger + + mail :from => self.class.apply_from_macro(from_address.to_s, nil), + :to => to_address, + :subject => self.class.apply_macro(HelpdeskSettings["helpdesk_first_answer_subject", issue.project_id], contact, issue) || issue.subject do |format| + format.text { render 'email_layout' } + format.html { render 'email_layout' } unless RedmineHelpdesk.settings["plain_text_mail"] + end + end + +# Receive email methods + + def self.receive(raw_email, options={}) + @@helpdesk_mailer_options = options.dup + raw_email.force_encoding('ASCII-8BIT') if raw_email.respond_to?(:force_encoding) + email = Mail.new(raw_email) + new.receive(email) + end + + # Processes incoming emails + # Returns the created object (eg. an issue, a message) or false + def receive(email) + @email = email + if !target_project.module_enabled?(:contacts) || !target_project.module_enabled?(:issue_tracking) + logger.error "#{email && email.message_id}: Contacts and issues modules should be enable for #{target_project.name} project" if logger + return false + end + + @@helpdesk_mailer_options = HelpdeskMailer.get_issue_options(@@helpdesk_mailer_options, target_project.id) + sender_email = message_sender(email) + # Ignore emails received from the application emission address to avoid hell cycles + if sender_email.downcase == Setting.mail_from.to_s.strip.downcase + logger.info "#{email && email.message_id}: Ignoring email from Redmine emission address [#{sender_email}]" if logger + return false + end + + return false unless handle_ignored(email) + + if !check_blacklist?(email) + logger.info "#{email && email.message_id}: Email #{sender_email} ignored because in blacklist" if logger + return false + end + + @user = User.find_by_mail(sender_email) || User.anonymous + @contact = contact_from_email(email) + + User.current = @user + if @contact + logger.info "#{email && email.message_id}: [#{@contact.name}] contact created/founded" if logger + else + logger.error "#{email && email.message_id}: could not create/found contact for [#{sender_email}]" if logger + return false + end + + dispatch + end + + def self.check_project(project_id) + msg_count = 0 + unless Project.find_by_id(project_id).blank? || HelpdeskSettings[:helpdesk_protocol, project_id].blank? + + mail_options, options = self.get_mail_options(project_id) + + case mail_options[:protocol] + when "pop3" then + msg_count = RedmineContacts::Mailer.check_pop3(self, mail_options, options) + when "imap" then + msg_count = RedmineContacts::Mailer.check_imap(self, mail_options, options) + end + end + + if project = Project.find_by_id(project_id) + HelpdeskTicket.autoclose(project) + end + + msg_count + end + + def self.get_mail_options(project_id) + case HelpdeskSettings[:helpdesk_protocol, project_id] + when "gmail" + protocol = "imap" + host = "imap.gmail.com" + port = "993" + ssl = "1" + when "yahoo" + protocol = "imap" + host = "imap.mail.yahoo.com" + port = "993" + ssl = "1" + when "yandex" + protocol = "imap" + host = "imap.yandex.ru" + port = "993" + ssl = "1" + else + protocol = HelpdeskSettings[:helpdesk_protocol, project_id] + host = HelpdeskSettings[:helpdesk_host, project_id] + port = HelpdeskSettings[:helpdesk_port, project_id] + ssl = HelpdeskSettings[:helpdesk_use_ssl, project_id] != "1" ? nil : "1" + end + + mail_options = {:protocol => protocol, + :host => host, + :port => port, + :ssl => ssl, + :apop => HelpdeskSettings[:helpdesk_apop, project_id], + :username => HelpdeskSettings[:helpdesk_username, project_id], + :password => HelpdeskSettings[:helpdesk_password, project_id], + :folder => HelpdeskSettings[:helpdesk_imap_folder, project_id], + :move_on_success => HelpdeskSettings[:helpdesk_move_on_success, project_id], + :move_on_failure => HelpdeskSettings[:helpdesk_move_on_failure, project_id], + :delete_unprocessed => HelpdeskSettings[:helpdesk_delete_unprocessed, project_id].to_i > 0 + } + options = get_issue_options({}, project_id) + [mail_options, options] + end + + def self.get_issue_options(options, project_id) + options = { :issue => {} } unless options[:issue] + options[:issue][:project_id] = project_id + options[:issue][:status_id] = HelpdeskSettings[:helpdesk_new_status, project_id] unless options[:issue][:status_id] + options[:issue][:assigned_to_id] = HelpdeskSettings["helpdesk_assigned_to", project_id] unless options[:issue][:assigned_to_id] + options[:issue][:tracker_id] = HelpdeskSettings["helpdesk_tracker", project_id] unless options[:issue][:tracker_id] + options[:issue][:priority_id] = HelpdeskSettings[:helpdesk_issue_priority, project_id] unless options[:issue][:priority_id] + options[:issue][:due_date] = HelpdeskSettings[:helpdesk_issue_due_date, project_id] unless options[:issue][:due_date] + options[:issue][:reopen_status_id] = HelpdeskSettings["helpdesk_reopen_status", project_id] unless options[:issue][:reopen_status_id] + options + end + + def attachment_macro(text, issue) + text.scan(/\{\{send_file\(([^%\}]+)\)\}\}/).flatten.each do |file_name| + attachment = file_name.match(/^(\d)+$/) ? Attachment.where(:id => file_name).first : issue.attachments.where(:filename => file_name).first + attachments[attachment.filename] = File.open(attachment.diskfile, 'rb'){|io| io.read} if attachment + end + text.gsub(/\{\{send_file\(([^%\}]+)\)\}\}/, '') + end + + def self.apply_from_macro(text, journal_user = nil) + return text unless text =~ /\A\{%.+%\}.*?<.+@.+\..{2,}>\z/ + text = text[/<.*>/] if journal_user.nil? || journal_user == User.anonymous + text = text.gsub(/\{%response.author%\}/, journal_user.name) if text =~ /\{%response.author%\}/ + text = text.gsub(/\{%response.author.first_name%\}/, journal_user.firstname) if text =~ /\{%response.author.first_name%\}/ + text + end + + def self.apply_macro(text, contact, issue, journal_user=nil) + return '' if text.blank? + text = text.gsub(/%%NAME%%|\{%contact.first_name%\}/, contact.first_name) + text = text.gsub(/%%FULL_NAME%%|\{%contact.name%\}/, contact.name) + text = text.gsub(/%%COMPANY%%|\{%contact.company%\}/, contact.company) if contact.company + text = text.gsub(/%%LAST_NAME%%|\{%contact.last_name%\}/, contact.last_name.blank? ? "" : contact.last_name) + text = text.gsub(/%%MIDDLE_NAME%%|\{%contact.middle_name%\}/, contact.middle_name.blank? ? "" : contact.middle_name) + text = text.gsub(/\{%contact.email%\}/, contact.primary_email.to_s) + text = text.gsub(/%%DATE%%|\{%date%\}/, ApplicationHelper.format_date(Date.today)) + text = text.gsub(/%%ASSIGNEE%%|\{%ticket.assigned_to%\}/, issue.assigned_to.blank? ? "" : issue.assigned_to.name) + text = text.gsub(/%%ISSUE_ID%%|\{%ticket.id%\}/, issue.id.to_s) if issue.id + text = text.gsub(/%%ISSUE_TRACKER%%|\{%ticket.tracker%\}/, issue.tracker.name) if issue.tracker + text = text.gsub(/%%QUOTED_ISSUE_DESCRIPTION%%|\{%ticket.quoted_description%\}/, issue.description.gsub(/^/, "> ")) if issue.description + text = text.gsub(/%%PROJECT%%|\{%ticket.project%\}/, issue.project.name) if issue.project + text = text.gsub(/%%SUBJECT%%|\{%ticket.subject%\}/, issue.subject) if issue.subject + text = text.gsub(/%%NOTE_AUTHOR%%|\{%response.author%\}/, journal_user.name) if journal_user + text = text.gsub(/%%NOTE_AUTHOR.FIRST_NAME%%|\{%response.author.first_name%\}/, journal_user.firstname) if journal_user + text = text.gsub(/%%NOTE_AUTHOR.LAST_NAME%%|\{%response.author.last_name%\}/, journal_user.lastname) if journal_user + text = text.gsub(/\{%ticket.status%\}/, issue.status.name) if issue.status + text = text.gsub(/\{%ticket.priority%\}/, issue.priority.name) if issue.priority + text = text.gsub(/\{%ticket.estimated_hours%\}/, issue.estimated_hours ? issue.estimated_hours.to_s : "") + text = text.gsub(/\{%ticket.done_ratio%\}/, issue.done_ratio.to_s) if issue.done_ratio + text = text.gsub(/\{%ticket.closed_on%\}/, issue.closed_on ? ApplicationHelper.format_date(issue.closed_on) : "") if issue.respond_to?(:closed_on) + text = text.gsub(/\{%ticket.due_date%\}/, issue.due_date ? ApplicationHelper.format_date(issue.due_date) : "") + text = text.gsub(/\{%ticket.start_date%\}/, issue.start_date ? ApplicationHelper.format_date(issue.start_date) : "") + text = text.gsub(/\{%ticket.public_url%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.public_ticket_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.public_url%\}/) && issue.helpdesk_ticket + if RedmineHelpdesk.vote_allow? + text = text.gsub(/\{%ticket.voting%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_show_path(issue.helpdesk_ticket.id, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting%\}/) + text = text.gsub(/\{%ticket.voting.good%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 2, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.good%\}/) + text = text.gsub(/\{%ticket.voting.okay%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 1, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.okay%\}/) + text = text.gsub(/\{%ticket.voting.bad%\}/, Setting.protocol + '://' + Setting.host_name + Rails.application.routes.url_helpers.helpdesk_votes_fast_vote_path(issue.helpdesk_ticket.id, 0, issue.helpdesk_ticket.token) ) if text.match(/\{%ticket.voting.bad%\}/) + end + + if text =~ /\{%ticket.history%\}/ + ticket_history = '' + issue.journals.eager_load(:journal_message).map(&:journal_message).compact.each do |journal_message| + message_author = "*#{l(:label_crm_added_by)} #{journal_message.is_incoming? ? journal_message.from_address : journal_message.journal.user.name}, #{format_time(journal_message.message_date)}*" + ticket_history = (message_author + "\n" + journal_message.journal.notes + "\n" + ticket_history).gsub(/^/, "> ") + end + text = text.gsub(/\{%ticket.history%\}/, ticket_history) + end + + issue.custom_field_values.each do |value| + text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s) + end + + contact.custom_field_values.each do |value| + text = text.gsub(/%%#{value.custom_field.name}%%/, value.value.to_s) + end if contact.respond_to?("custom_field_values") + + journal_user.custom_field_values.each do |value| + text = text.gsub(/\{%response.author.custom_field: #{value.custom_field.name}%\}/, value.value.to_s) + end if journal_user + + text + end + + private + + def dispatch + m = email.subject && email.subject.match(ISSUE_REPLY_SUBJECT_RE) + journal_message = !email.in_reply_to.blank? && JournalMessage.find_by_message_id(email.in_reply_to) + helpdesk_ticket = !email.in_reply_to.blank? && HelpdeskTicket.find_by_message_id(email.in_reply_to) + if journal_message && journal_message.journal && journal_message.journal.issue + receive_issue_reply(journal_message.journal.issue.id) + elsif helpdesk_ticket && helpdesk_ticket.issue + receive_issue_reply(helpdesk_ticket.issue.id) + elsif m && Issue.exists?(m[1].to_i) + receive_issue_reply(m[1].to_i) + else + dispatch_to_default + end + rescue MissingInformation => e + logger.error "#{email && email.message_id}: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "#{email && email.message_id}: unauthorized attempt from #{user}" if logger + false + rescue Exception => e + # TODO: send a email to the user + logger.error "#{email && email.message_id}: dispatch error #{e.message}" if logger + false + end + + def dispatch_to_default + receive_issue + end + + def target_project + @target_project ||= Project.find_by_identifier(get_keyword(:project) || get_keyword(:project_id)) + @target_project ||= Project.find_by_id(get_keyword(:project_id)) if @target_project.nil? + raise MissingInformation.new('Unable to determine @target_project project') if @target_project.nil? + @target_project + end + + def helpdesk_issue_attributes_from_keywords(issue) + # assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && User.find_by_id(k)) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)) + assigned_to = ((k = get_keyword(:assigned_to_id, :override => true)) && (User.find_by_id(k) || Group.find_by_id(k))) || ((k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)) + + attrs = { + 'status_id' => ((k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:status_id)) && IssueStatus.find_by_id(k).try(:id)), + 'priority_id' => ((k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id)) || ((k = get_keyword(:priority_id)) && IssuePriority.find_by_id(k).try(:id)), + 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id), + 'assigned_to_id' => assigned_to.try(:id), + 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id), + 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'), + 'estimated_hours' => get_keyword(:estimated_hours, :override => true), + 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') + }.delete_if {|k, v| v.blank? } + + attrs + end + + def calculated_tracker_id(issue) + issue_tracker_id = ((k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id)) || + ((k = get_keyword(:tracker_id)) && issue.project.trackers.find_by_id(k).try(:id)) + issue_tracker_id = issue.project.trackers.first.try(:id) unless issue_tracker_id + issue_tracker_id + end + + # Creates a new issue + def receive_issue + project = target_project + issue = Issue.new + issue.author = user + issue.project = project + issue.safe_attributes = helpdesk_issue_attributes_from_keywords(issue) + issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)} + issue.tracker_id = calculated_tracker_id(issue) + issue.subject = cleaned_up_subject(email) + issue.subject = '(no subject)' if issue.subject.blank? + issue.description = escaped_cleaned_up_text_body + issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date? + + helpdesk_ticket = HelpdeskTicket.new(:from_address => message_sender(email).downcase.to_s.slice(0, 255), + :to_address => email.to_addrs.join(',').downcase.to_s.slice(0, 255), + :cc_address => email.cc_addrs.join(',').downcase.to_s.slice(0, 255), + :ticket_date => email.date || Time.now, + :message_id => email.message_id.to_s.slice(0, 255), + :is_incoming => true, + :customer => contact, + :issue => issue, + :source => HelpdeskTicket::HELPDESK_EMAIL_SOURCE) + + issue.helpdesk_ticket = helpdesk_ticket + issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0 + issue.assigned_to = @contact.find_assigned_user(project, issue.assigned_to_id) + + + save_email_as_attachment(helpdesk_ticket) + add_attachments(issue) + + Redmine::Hook.call_hook(:helpdesk_mailer_receive_issue_before_save, { :issue => issue, :contact => contact, :helpdesk_ticket => helpdesk_ticket, :email => email}) + + ActiveRecord::Base.transaction do + issue.save!(:validate => false) + ContactNote.create(:content => "*#{issue.subject}* [#{issue.tracker.name} - ##{issue.id}]\n\n" + issue.description, + :type_id => Note.note_types[:email], + :source => contact, + :author_id => issue.author_id) if HelpdeskSettings["helpdesk_add_contact_notes", project] + begin + notification = HelpdeskMailer.auto_answer(contact, issue).deliver if HelpdeskSettings["helpdesk_send_notification", project].to_i > 0 + logger.info "#{email && email.message_id}: notification was sent to #{notification.to_addrs.first}" if logger && notification + rescue Exception => e + logger.error "#{email && email.message_id}: notification was not sent #{e.message}" if logger + false + end + + logger.info "#{email && email.message_id}: issue ##{issue.id} created by #{user} for #{contact.name}" if logger + issue + end #transaction + + end + + # Adds a note to an existing issue + def receive_issue_reply(issue_id) + issue = Issue.find_by_id(issue_id) + return unless issue + # if lifetime expaired create new issue + if (HelpdeskSettings["helpdesk_lifetime", target_project].to_i > 0) && issue.journals && issue.journals.last && ((Date.today) - issue.journals.last.created_on.to_date > HelpdeskSettings["helpdesk_lifetime", target_project].to_i) + email.subject = email.subject.to_s.gsub(ISSUE_REPLY_SUBJECT_RE, '') + return receive_issue + end + journal = issue.init_journal(user) + journal.notes = escaped_cleaned_up_text_body + + journal_message = JournalMessage.create(:from_address => message_sender(email).downcase, + :to_address => email.to_addrs.join(',').downcase, + :bcc_address => email.bcc_addrs.join(',').downcase, + :cc_address => email.cc_addrs.join(',').downcase, + :message_id => email.message_id, + :is_incoming => true, + :message_date => email.date || Time.now, + :contact => contact, + :journal => journal) + + issue.contacts << cc_contacts if HelpdeskSettings["helpdesk_save_cc", target_project.id].to_i > 0 + + add_attachments(issue) + + save_email_as_attachment(journal_message, "reply-#{DateTime.now.strftime('%d.%m.%y-%H.%M.%S')}.eml") + + if reopen_status_id = ((k = @@helpdesk_mailer_options[:reopen_status]) && IssueStatus.named(k).first.try(:id) ) || ((k = get_keyword(:reopen_status_id)) && IssueStatus.find_by_id(k).try(:id)) + issue.status_id = reopen_status_id + end + + issue.save! + logger.info "#{email && email.message_id}: issue ##{issue.id} updated by #{user}" if logger + journal + end + + # Reply will be added to the issue + def receive_journal_reply(journal_id) + journal = Journal.find_by_id(journal_id) + if journal && journal.journalized_type == 'Issue' + receive_issue_reply(journal.journalized_id) + end + end + + def add_attachments(obj) + fwd_attachments = email.parts.map { |p| + if p.content_type =~ /message\/rfc822/ + Mail.new(p.body).attachments + elsif p.parts.empty? + p if p.attachment? + else + p.attachments + end + }.flatten.compact + + email_attachments = fwd_attachments | email.attachments + + unless email_attachments.blank? + email_attachments.each do |attachment| + if RUBY_VERSION < '1.9' + attachment_filename = (attachment[:content_type].filename rescue nil) || + (attachment[:content_disposition].filename rescue nil) || + (attachment[:content_location].location rescue nil) || + "attachment" + attachment_filename = Mail::Encodings.unquote_and_convert_to(attachment_filename, 'utf-8') rescue 'unprocessable_filename' + attachment_filename = helpdesk_to_utf8(attachment_filename) + else + attachment_filename = helpdesk_to_utf8(attachment.filename, 'binary') + end + + new_attachment = Attachment.new(:container => obj, + :file => (attachment.decoded rescue nil) || (attachment.decode_body rescue nil) || attachment.raw_source, + :filename => attachment_filename, + :author => user, + :content_type => attachment.mime_type) + if obj.attachments.where(:digest => attachment_digest(attachment.body.to_s)).empty? && accept_attachment?(new_attachment) + obj.attachments << new_attachment + logger.info "#{email && email.message_id}: attachment #{attachment_filename} added to ticket: '#{obj.subject}'" if logger + end + end + end + end + + def get_keyword(attr, options = {}) + @keywords ||= {} + if !@keywords.has_key?(attr) + if (options[:override] || attr_overridable?(attr)) && + v = extract_keyword!(escaped_cleaned_up_text_body, attr, options[:format]) + @keywords[attr] = v + elsif !@@helpdesk_mailer_options[:issue][attr].blank? + @keywords[attr] = @@helpdesk_mailer_options[:issue][attr] + end + end + @keywords[attr] + end + + def attr_overridable?(attr) + @@helpdesk_mailer_options[:allow_override].present? && + @@helpdesk_mailer_options[:allow_override].include?(attr.to_s) + end + + def find_user_from_keyword(keyword) + user ||= User.find_by_mail(keyword) + user ||= User.find_by_login(keyword) + if user.nil? && keyword.match(/ /) + firstname, lastname = *(keyword.split) # "First Last Throwaway" + user ||= User.find_by_firstname_and_lastname(firstname, lastname) + end + user + end + + def check_blacklist?(email) + return true if HelpdeskSettings["helpdesk_blacklist", target_project].blank? + addr = email.from_addrs.first.to_s.strip + from_addr = addr # (addr && !addr.spec.blank?) ? addr.spec : email.header["from"].inspect.match(/[-A-z0-9.]+@[-A-z0-9.]+/).to_s + cond = "(" + HelpdeskSettings["helpdesk_blacklist", target_project].split("\n").map{|u| u.strip unless u.blank?}.compact.join('|') + ")" + !from_addr.match(cond) + end + + def new_contact_from_attributes(email_address, fullname=nil) + contact = Contact.new + + # Truncating the email address would result in an invalid format + contact.email = email_address + names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split + contact.first_name = names.shift.slice(0, 255) + contact.last_name = names.join(' ').slice(0, 255) + contact.company = email_address.downcase.slice(0, 255) + contact.last_name = '-' if contact.last_name.blank? + + if contact.last_name =~ %r(\((.*)\)) + contact.last_name, contact.company = $`, $1 + end + + if contact.first_name =~ /,$/ + contact.first_name = contact.last_name + contact.last_name = $` # everything before the match + end + + contact.projects << target_project + contact.tag_list = HelpdeskSettings["helpdesk_created_contact_tag", target_project] if HelpdeskSettings["helpdesk_created_contact_tag", target_project] + + contact + end + + def cc_contacts + email[:cc].to_s + email.cc_addrs.each_with_index.map do |cc_addr, index| + cc_name = email[:cc].display_names[index] + create_contact_from_address(cc_addr, cc_name) + end.compact + end + + def create_contact_from_address(addr, name) + contacts = Contact.find_by_emails([addr]) + unless contacts.blank? + contact = contacts.first + if contact.projects.blank? || HelpdeskSettings[:helpdesk_add_contact_to_project, target_project].to_i > 0 + contact.projects << target_project + contact.save! + end + + return contact + end + + if HelpdeskSettings["helpdesk_is_not_create_contacts", target_project].to_i > 0 + logger.error "#{email && email.message_id}: can't find contact with email: #{addr} in whitelist. Not create new contacts option enable" if logger + nil + else + contact = new_contact_from_attributes(addr, name) + if contact.save(:validate => false) + contact + else + logger.error "Helpdeks MailHandler: failed to create Contact: #{contact.errors.full_messages}" if logger + nil + end + end + end + + # Get or create contact for the +email+ sender + def contact_from_email(email) + # from = email.header['from'].to_s + # debugger + from = cleaned_up_from_address + addr, name = from, nil + if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/) + addr, name = m[2], m[1] + end + if addr.present? + create_contact_from_address(addr, name) + else + logger.error "#{email && email.message_id}: failed to create Contact: no FROM address found" if logger + nil + end + + end + + # Returns a Hash of issue custom field values extracted from keywords in the email body + def custom_field_values_from_keywords(customized) + customized.custom_field_values.inject({}) do |h, v| + if value = get_keyword(v.custom_field.name, :override => true) + h[v.custom_field.id.to_s] = value + end + h + end + end + + def save_email_as_attachment(container, filename="message.eml") + Attachment.create(:container => container, + :file => email.raw_source.to_s, + :author => user, + :filename => filename, + :content_type => "message/rfc822") + end + + def plain_text_body + return @plain_text_body unless @plain_text_body.nil? + part = email.text_part || email.html_part + unless part + return @plain_text_body = '' if email.parts.present? && email.parts.all? { |part| part.attachment? } + part = email + end + + is_html = email.text_part.blank? + part_charset = Mail::RubyVer.pick_encoding(part.charset).to_s rescue part.charset + @plain_text_body = helpdesk_to_utf8(part.body.decoded, part_charset) + + # strip html tags and remove doctype directive + @plain_text_body.gsub! %r{^[ ]+}, '' + if is_html && RedmineHelpdesk.strip_tags? + @plain_text_body.gsub! %r{(?:.|\n|\r)+?<\/head>}, "" + @plain_text_body.gsub! %r{<\/(li|ol|ul|h1|h2|h3|h4)>}, "\r\n" + @plain_text_body.gsub! %r{<\/(p|div|pre)>}, "\r\n\r\n" + @plain_text_body.gsub! %r{
      • }, " - " + @plain_text_body.gsub! %r{]*>}, "\r\n" + @plain_text_body = strip_tags(@plain_text_body.strip) + @plain_text_body.sub! %r{^ e + logger.error "#{email && email.message_id}: Message body processing error - #{e.message}" if logger + @plain_text_body = '(Unprocessable message body)' + end + + def cleaned_up_subject(email) + return "" if email[:subject].blank? + subject = decode_subject(email[:subject].value) + subject = helpdesk_to_utf8(subject) + subject.strip[0,255] + rescue Exception => e + logger.error "#{email && email.message_id}: Message subject processing error - #{e.message}" if logger + '(Unprocessable subject)' + end + + def cleaned_up_from_address + from = email.header['reply-to'].to_s.present? ? email.header['reply-to'] : email.header['from'] + from.to_s.strip[0, 255] + end + + def logger + HelpdeskLogger + end + + def helpdesk_to_utf8(str, encoding="UTF-8") + return str if str.nil? + if str.respond_to?(:force_encoding) + begin + cleaned = str.force_encoding('UTF-8') + cleaned = cleaned.encode("UTF-8", encoding) if encoding.upcase == 'ISO-2022-JP' + unless cleaned.valid_encoding? + cleaned = str.encode('UTF-8', encoding, :invalid => :replace, :undef => :replace, :replace => '').chars.select{|i| i.valid_encoding?}.join + end + str = cleaned + rescue EncodingError + str.encode!( 'UTF-8', :invalid => :replace, :undef => :replace ) + end + elsif RUBY_PLATFORM == 'java' + begin + ic = Iconv.new('UTF-8', encoding + '//IGNORE') + str = ic.iconv(str) + rescue + str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?') + end + else + ic = Iconv.new('UTF-8', encoding + '//IGNORE') + txtar = "" + begin + txtar += ic.iconv(str) + rescue Iconv::IllegalSequence + txtar += $!.success + str = '?' + $!.failed[1,$!.failed.length] + retry + rescue + txtar += $!.success + end + str = txtar + end + str + end + + def attachment_digest(file_source) + encoder = Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? ? Digest::SHA256.new : Digest::MD5.new + encoder.update(file_source) + encoder.hexdigest + end + + def set_delivery_options + return false if HelpdeskSettings[:helpdesk_smtp_use_default_settings, project.id].to_i == 0 + message.delivery_method(:smtp) + settings = {:address => HelpdeskSettings[:helpdesk_smtp_server, project.id], + :port => HelpdeskSettings[:helpdesk_smtp_port, project.id] || 25, + :domain => HelpdeskSettings[:helpdesk_smtp_domain, project.id], + :enable_starttls_auto => true, + :ssl => HelpdeskSettings[:helpdesk_smtp_ssl, project.id].to_i > 0 && + HelpdeskSettings[:helpdesk_smtp_tls, project.id].to_i == 0} + authentication = HelpdeskSettings[:helpdesk_smtp_authentication, project.id] + unless authentication.blank? + settings.merge!(:authentication => authentication, + :user_name => HelpdeskSettings[:helpdesk_smtp_username, project.id], + :password => HelpdeskSettings[:helpdesk_smtp_password, project.id]) + end + message.delivery_method.settings.merge!(settings) + end + + def decode_subject(str) + # Optimization: If there's no encoded-words in the string, just return it + return str unless str.index("=?") + + str = str.gsub(/\?=(\s*)=\?/, '?=????=?') # Replace whitespaces between 'encoded-word's on special symbols + str.split('????').map do |text| + if text.index('=?') .nil? + text + else + text.gsub!(/[\r\n]/, '') + text.scan(/\=\?.+?\?[qQbB]\?.+?\?\=/).map do |part| + if part.index(/\=\?.+\?[Bb]\?.+\?\=/m) + part.gsub(/\=\?.+\?[Bb]\?.+\?=/m) { |substr| Mail::Encodings.b_value_decode(substr) } + elsif part.index(/\=\?.+\?[Qq]\?.+\?\=/m) + part.gsub(/\=\?.+\?[Qq]\?.+\?\=/m) { |substr| Mail::Encodings.q_value_decode(substr) } + end + end + end + end.join('') + end + + def self.ignored_helpdesk_headers + helpdesk_headers = { + 'X-Auto-Response-Suppress' => /\A(all|AutoReply|oof)/ + } + ignored_emails_headers.merge(helpdesk_headers) + end + + def handle_ignored(email) + self.class.ignored_helpdesk_headers.each do |key, ignored_value| + value = email.header[key] + if value + value = value.to_s.downcase + if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value + if logger + logger.info "#{email && email.message_id}: ignoring email with #{key}:#{value} header" + end + return false + end + end + end + return true + end + + def escaped_cleaned_up_text_body + text_body = cleaned_up_text_body + return text_body unless (ActiveRecord::Base.connection.adapter_name =~ /mysql/i).present? + text_body.gsub(/./) { |c| c.bytesize == 4 ? ' ' : c } + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_busiest_time_query.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_busiest_time_query.rb new file mode 100644 index 0000000..d8607ee --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_busiest_time_query.rb @@ -0,0 +1,18 @@ +class HelpdeskReportsBusiestTimeQuery < HelpdeskReportsQuery + def sql_for_staff_field(_field, operator, value) + issue_table = Issue.table_name + compare = operator == '=' ? 'IN' : 'NOT IN' + staff_ids = value.join(',') + "#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} WHERE (#{issue_table}.assigned_to_id #{compare} (#{staff_ids})))" + end + + private + + def collect_answered_users + return [] unless project + user_ids = Issue.joins(:project). + visible.uniq. + pluck(:assigned_to_id).compact + User.where(:id => user_ids) + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_first_response_query.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_first_response_query.rb new file mode 100644 index 0000000..859392d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_first_response_query.rb @@ -0,0 +1,21 @@ +class HelpdeskReportsFirstResponseQuery < HelpdeskReportsQuery + def sql_for_staff_field(_field, operator, value) + issue_table = Issue.table_name + journal_table = Journal.table_name + compare = operator == '=' ? 'IN' : 'NOT IN' + staff_ids = value.join(',') + "#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))" + end + + private + + def collect_answered_users + return [] unless project + user_ids = Issue.joins(:project). + joins(:journals). + joins(:journals => :journal_message). + visible.uniq. + pluck(:assigned_to_id).compact + User.where(:id => user_ids) + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_query.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_query.rb new file mode 100644 index 0000000..793eeb3 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_reports_query.rb @@ -0,0 +1,94 @@ +class HelpdeskReportsQuery < Query + self.queried_class = JournalMessage + operators_by_filter_type[:time_interval] = ['t', 'ld', 'w', 'l2w', 'm', 'lm', 'y'] + + def initialize_available_filters + add_available_filter 'message_date', :type => :time_interval, :name => l(:label_helpdesk_filter_time_interval) + author_values = collect_answered_users.collect { |user| [user.name, user.id.to_s] } + add_available_filter 'staff', :type => :list, :name => l(:field_assigned_to), :values => author_values + end + + def build_from_params(params) + if params[:fields] || params[:f] + self.filters = {} + add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) + else + available_filters.keys.each do |field| + add_short_filter(field, params[field]) if params[field] + end + end + self + end + + def issues(options = {}) + scope = issue_scope.eager_load((options[:include] || []).uniq). + where(options[:conditions]). + limit(options[:limit]). + offset(options[:offset]) + scope + rescue ::ActiveRecord::StatementInvalid => e + raise StatementInvalid.new(e.message) + end + + def sql_for_staff_field(_field, operator, value) + issue_table = Issue.table_name + journal_table = Journal.table_name + compare = operator == '=' ? 'IN' : 'NOT IN' + staff_ids = value.join(',') + "#{issue_table}.id IN(SELECT #{issue_table}.id FROM #{issue_table} INNER JOIN #{journal_table} ON #{journal_table}.journalized_id = #{issue_table}.id AND #{journal_table}.journalized_type = 'Issue' WHERE (#{journal_table}.user_id #{compare} (#{staff_ids})))" + end + + private + + def collect_answered_users + return [] unless project + user_ids = Issue.joins(:project). + joins(:journals). + joins(:journals => :journal_message). + visible.uniq. + pluck(:assigned_to_id).compact + User.where(:id => user_ids) + end + + def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter = false) + sql = '' + first_day_of_week = l(:general_first_day_of_week).to_i + date = Date.today + day_of_week = date.cwday + case operator + when 'pre_t' + sql = date_clause_selector(db_table, db_field, -1, -1, is_custom_filter) + when 'pre_ld' + sql = date_clause_selector(db_table, db_field, -2, -2, is_custom_filter) + when 'pre_w' + days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = date_clause_selector(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter) + when 'pre_l2w' + days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) + sql = date_clause_selector(db_table, db_field, - days_ago - 28, - days_ago - 14 - 1, is_custom_filter) + when 'pre_m' + sql = date_clause_selector_for_date(db_table, db_field, (date - 1.month).beginning_of_month, (date - 1.month).end_of_month, is_custom_filter) + when 'pre_lm' + sql = date_clause_selector_for_date(db_table, db_field, (date - 2.months).beginning_of_month, (date - 2.months).end_of_month, is_custom_filter) + when 'pre_y' + sql = date_clause_selector_for_date(db_table, db_field, (date - 1.year).beginning_of_year, (date - 1.year).end_of_year, is_custom_filter) + end + sql = super(field, operator, value, db_table, db_field, is_custom_filter) if sql.blank? + + sql + end + + def date_clause_selector(table, field, from, to, is_custom_filter) + return date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil)) if Redmine::VERSION.to_s < '3.0' + date_clause(table, field, (from ? Date.today + from : nil), (to ? Date.today + to : nil), is_custom_filter) + end + + def date_clause_selector_for_date(table, field, date_from, date_to, is_custom_filter) + return date_clause(table, field, date_from, date_to) if Redmine::VERSION.to_s < '3.0' + date_clause(table, field, date_from, date_to, is_custom_filter) + end + + def issue_scope + Issue.visible.joins(:project, :journals => :journal_message).where(statement).uniq + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/helpdesk_ticket.rb b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_ticket.rb new file mode 100644 index 0000000..d850072 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/helpdesk_ticket.rb @@ -0,0 +1,268 @@ +class HelpdeskTicket < ActiveRecord::Base + HELPDESK_EMAIL_SOURCE = 0 + HELPDESK_WEB_SOURCE = 1 + HELPDESK_PHONE_SOURCE = 2 + HELPDESK_TWITTER_SOURCE = 3 + HELPDESK_CONVERSATION_SOURCE = 4 + + SEND_AS_NOTIFICATION = 1 + SEND_AS_MESSAGE = 2 + + attr_accessible :vote, :vote_comment,:from_address, + :to_address, :cc_address, :ticket_date, + :message_id, :is_incoming, :customer, :issue, :source, :contact_id, :ticket_time + + attr_accessor :ticket_time + + unloadable + belongs_to :customer, :class_name => 'Contact', :foreign_key => 'contact_id' + belongs_to :issue + has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy + + acts_as_attachable :view_permission => :view_issues, + :delete_permission => :edit_issues + + + if ActiveRecord::VERSION::MAJOR >= 4 + acts_as_activity_provider :type => 'helpdesk_tickets', + :permission => :view_helpdesk_tickets, + :timestamp => "#{table_name}.ticket_date", + :author_key => "#{Issue.table_name}.author_id", + :scope => eager_load(:issue => :project) + else + acts_as_activity_provider :type => 'helpdesk_tickets', + :permission => :view_helpdesk_tickets, + :timestamp => "#{table_name}.ticket_date", + :author_key => "#{Issue.table_name}.author_id", + :find_options => {:include => {:issue => :project}} + end + + acts_as_event :datetime => :ticket_date, + :project_key => "#{Project.table_name}.id", + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue_id}}, + :type => Proc.new {|o| 'icon icon-email' + (o.issue.closed? ? ' closed' : '') if o.issue }, + :title => Proc.new {|o| "##{o.issue.id} (#{o.issue.status}): #{o.issue.subject}" if o.issue }, + :author => Proc.new {|o| o.customer}, + :description => Proc.new{|o| o.issue.description if o.issue} + + accepts_nested_attributes_for :customer + + after_create :set_ticket_private + before_save :calculate_metrics + validates_presence_of :customer, :ticket_date + + def initialize(attributes=nil, *args) + super + if new_record? + # set default values for new records only + self.ticket_date ||= Time.now + self.source ||= HelpdeskTicket::HELPDESK_EMAIL_SOURCE + end + end + + def ticket_time + self.ticket_date.to_s(:time) unless self.ticket_date.blank? + end + + def ticket_time=(val) + if !self.ticket_date.blank? && val.to_s.gsub(/\s/, "").match(/^(\d{1,2}):(\d{1,2})$/) + timezone = ticket_date.try(:time_zone).try(:name) || Time.zone.name + self.ticket_date = ActiveSupport::TimeZone.new(timezone).local_to_utc(self.ticket_date.utc). + in_time_zone(timezone). + change({:hour => $1.to_i % 24, :min => $2.to_i % 60}) + end + end + + def recalculate_events + unless issue.closed? + close_journal_id = nil + end + end + + def available_addresses + @available_addresses ||= ([self.default_to_address] | self.customer.emails.map{|e| e} | [self.from_address.blank? ? nil : self.from_address.downcase.strip]).compact.uniq if self.customer + end + + def default_to_address + return last_response_address if last_journal_message && last_journal_message.is_incoming? + address = self.from_address.blank? ? "" : self.from_address.downcase.strip + self.customer.emails.include?(address) ? address : self.customer.primary_email + end + + def last_reply_customer + return customer unless default_to_address + customer.primary_email == default_to_address ? customer : Contact.find_by_emails([default_to_address]).first + end + + def cc_addresses + @cc_addresses = ((self.issue.contacts ? self.issue.contacts.map(&:primary_email) : []) | cc_address.to_s.split(',')).compact.uniq + end + + def project + issue.project if issue + end + + def author + issue.author if issue + end + + def customer_name + customer.name if customer + end + + def responses + @responses ||= JournalMessage. + joins(:journal). + where(:journals => {:journalized_id => self.issue_id}). + order("#{JournalMessage.table_name}.message_date ASC") + end + + def reaction_date + @reaction_date ||= self.issue.journals. + joins(:journal_message). + where("#{JournalMessage.table_name}.journal_id IS NULL OR #{JournalMessage.table_name}.is_incoming = ?", false). + order("#{Journal.table_name}.created_on ASC"). + first. + try(:created_on).try(:utc) + end + + def response_addresses + responses.where(:is_incoming => true).map { |response| response.from_address }.uniq + end + + def first_response_date + @first_response_date ||= responses.select {|r| !r.is_incoming? }.first.try(:message_date).try(:utc) + end + + def last_response_time + @last_response_time ||= last_journal_message && last_journal_message.is_incoming? && !self.issue.closed? ? last_journal_message.message_date.utc : nil + end + + def last_response_address + response_addresses.last + end + + def last_agent_response + @last_agent_response ||= responses.select { |r| !r.is_incoming? }.last + end + + def last_journal_message + @last_journal_message ||= responses.last + end + + def last_customer_response + @last_customer_response ||= responses.select { |r| r.is_incoming? }.last + end + + def average_response_time + + end + + def ticket_source_name + case self.source + when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then l(:label_helpdesk_tickets_email) + when HelpdeskTicket::HELPDESK_PHONE_SOURCE then l(:label_helpdesk_tickets_phone) + when HelpdeskTicket::HELPDESK_WEB_SOURCE then l(:label_helpdesk_tickets_web) + when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then l(:label_helpdesk_tickets_twitter) + when HelpdeskTicket::HELPDESK_CONVERSATION_SOURCE then l(:label_helpdesk_tickets_conversation) + else "" + end + end + + def ticket_source_icon + case self.source + when HelpdeskTicket::HELPDESK_EMAIL_SOURCE then "icon-email" + when HelpdeskTicket::HELPDESK_PHONE_SOURCE then "icon-call" + when HelpdeskTicket::HELPDESK_WEB_SOURCE then "icon-web" + when HelpdeskTicket::HELPDESK_TWITTER_SOURCE then "icon-twitter" + else "icon-helpdesk" + end + end + + def content + issue.description if issue + end + + def customer_email + customer.primary_email if customer + end + + def last_message + @last_message ||= JournalMessage.eager_load(:journal => :issue).where(:issues => {:id => issue.id}).order("#{Journal.table_name}.created_on ASC").last || self + end + + def last_message_date + last_message.is_a?(HelpdeskTicket) ? self.ticket_date : last_message.message_date if last_message + end + + def ticket_date + return nil if super.blank? + zone = User.current.time_zone + zone ? super.in_time_zone(zone) : (super.utc? ? super.localtime : super) + end + + def token + Digest::MD5.hexdigest("#{issue.id}:#{self.ticket_date.utc}:#{Rails.application.config.secret_token}") + end + + def calculate_metrics + self.reaction_time = reaction_date - ticket_date.utc if reaction_date && ticket_date + self.first_response_time = first_response_date - ticket_date.utc if first_response_date && ticket_date + self.resolve_time = self.issue.closed? ? self.issue.closed_on - ticket_date.utc : nil if ticket_date && self.issue.closed_on && last_agent_response + self.last_agent_response_at = last_agent_response.message_date if last_agent_response + self.last_customer_response_at = last_customer_response.message_date if last_customer_response + end + + def self.vote_message(vote) + case vote.to_i + when 0 + l(:label_helpdesk_mark_notgood) + when 1 + l(:label_helpdesk_mark_justok) + when 2 + l(:label_helpdesk_mark_awesome) + else + "" + end + end + + def update_vote(new_vote, comment = nil) + old_vote = vote + old_vote_comment = vote_comment + if update_attributes(:vote => new_vote, :vote_comment => comment ) + if old_vote != vote || old_vote_comment != vote_comment + journal = Journal.new(:journalized => issue, :user => User.current) + journal.details << JournalDetail.new(:property => 'attr', + :prop_key => 'vote', + :old_value => old_vote, + :value => vote) if old_vote != vote + journal.details << JournalDetail.new(:property => 'attr', + :prop_key => 'vote_comment', + :old_value => old_vote_comment, + :value => vote_comment) if old_vote_comment != vote_comment + journal.save + end + end + end + + def self.autoclose(project) + return unless RedmineHelpdesk.autoclose_tickets_after > 0 + issues = Issue.includes(:helpdesk_ticket).where(:project_id => project.id). + where(:status_id => RedmineHelpdesk.autoclose_from_status). + where('created_on < ?', Time.now - RedmineHelpdesk.autoclose_time_interval) + issues.find_each do |issue| + issue.init_journal(User.anonymous) + issue.current_journal.notes = I18n.t('label_helpdesk_autoclosed_ticket') + issue.status_id = RedmineHelpdesk.autoclose_to_status + issue.save + end + end + + private + + def set_ticket_private + return unless RedmineHelpdesk.settings["helpdesk_assign_contact_user"].to_i > 0 + issue.assign_attributes(:is_private => true) if RedmineHelpdesk.settings["helpdesk_create_private_tickets"].to_i > 0 + issue.save unless issue.new_record? + end +end diff --git a/plugins/redmine_contacts_helpdesk/app/models/journal_message.rb b/plugins/redmine_contacts_helpdesk/app/models/journal_message.rb new file mode 100644 index 0000000..7d7bf4f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/models/journal_message.rb @@ -0,0 +1,58 @@ +class JournalMessage < ActiveRecord::Base + unloadable + belongs_to :contact + belongs_to :journal + has_one :message_file, :class_name => "Attachment", :as => :container, :dependent => :destroy + + attr_accessible :source, :from_address, :to_address, :bcc_address, :cc_address, :message_id, :is_incoming, :message_date, :contact, :journal + + acts_as_attachable :view_permission => :view_issues, + :delete_permission => :edit_issues + + if ActiveRecord::VERSION::MAJOR >= 4 + acts_as_activity_provider :type => 'helpdesk_tickets', + :permission => :view_helpdesk_tickets, + :timestamp => "#{table_name}.message_date", + :author_key => "#{Journal.table_name}.user_id", + :scope => eager_load({:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact) + else + acts_as_activity_provider :type => 'helpdesk_tickets', + :permission => :view_helpdesk_tickets, + :timestamp => "#{table_name}.message_date", + :author_key => "#{Journal.table_name}.user_id", + :find_options => {:include => [{:journal => [{:issue => [:project, :tracker]}, :details, :user]}, :contact]} + end + + acts_as_event :title => Proc.new {|o| "#{o.journal.issue.tracker} ##{o.journal.issue.id}: #{o.journal.issue.subject}" if o.journal && o.journal.issue}, + :datetime => :message_date, + :group => :helpdesk_ticket, + :project_key => "#{Project.table_name}.id", + :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.journal.issue.id, :anchor => "change-#{o.id}"} if o.journal}, + :type => Proc.new {|o| ('icon' + (o.is_incoming? ? " icon-email" : " icon-email-to")) }, + :author => Proc.new {|o| o.is_incoming? ? o.contact : o.journal.user }, + :description => Proc.new{|o| o.journal.notes if o.journal} + + validates_presence_of :contact, :journal, :message_date + + def project + journal.project + end + + def contact_name + contact.name + end + + def contact_email + contact.emails.first + end + + def helpdesk_ticket + journal.issue.helpdesk_ticket + end + + def content + journal.notes + end + + +end diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_form.html.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_form.html.erb new file mode 100644 index 0000000..90fee4a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_form.html.erb @@ -0,0 +1,41 @@ +<%= back_url_hidden_field_tag %> +<%= error_messages_for 'canned_response' %> + +
        +

        <%= f.text_field :name, :size => 80, :required => true %>

        +<% if User.current.admin? || User.current.allowed_to?(:manage_public_canned_responses, @project) %> + <% if @canned_response.user %> +

        + + <%= @canned_response.user.name %> +

        + <% end %> + +

        +<%= f.check_box :is_public, + :label => l(:field_is_public), + :onchange => (User.current.admin? ? nil : 'if (this.checked) {$("#canned_response_is_for_all").removeAttr("checked"); $("#canned_response_is_for_all").attr("disabled", true);} else {$("#canned_response_is_for_all").removeAttr("disabled");}') %> +

        +<% end %> + +

        +<%= check_box_tag 'canned_response_is_for_all', 1, @canned_response.project.nil?, + :disabled => (!@canned_response.new_record? && (@canned_response.project.nil? || (@canned_response.is_public? && !User.current.admin?))) %>

        + +

        <%= f.text_area :content, :required => true, :class => 'wiki-edit', :rows => 5 %> +<%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.map{|m| link_to m, "#", :class => "mail-macro"}.join(', ')).html_safe %> +<%= wikitoolbar_for 'canned_response_content' %> +

        + +
        + + + +<% content_for :header_tags do %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_index.html.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_index.html.erb new file mode 100644 index 0000000..4926644 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/_index.html.erb @@ -0,0 +1,37 @@ +
        + <%= link_to l(:label_helpdesk_new_canned_response), {:controller => "canned_responses", :action => 'new'}, :class => 'icon icon-add' %> +
        +

        <%= l(:label_helpdesk_canned_response_plural) %>

        + +<% if @canned_responses.any? %> + + + + + + + + + + + <% @canned_responses.each do |canned_response| %> + + + + + + + + + <% end %> + +
        <%= l(:field_name) %><%= l(:field_content) %><%= l(:field_is_public) %><%= l(:field_author) %><%= l(:field_project) %>
        <%= canned_response.name %><%= canned_response.content.gsub(/$/, ' ').truncate(250) %><%= checked_image canned_response.is_public? %><%= canned_response.user.try(:name) %><%= canned_response.project ? canned_response.project.name : l(:field_is_for_all) %> + <%= link_to l(:button_edit), edit_canned_response_path(canned_response), :class => 'icon icon-edit' %> + <%= delete_link canned_response_path(canned_response, :project_id => canned_response.project) %> +
        + <% if @canned_response_pages %> +

        <%= pagination_links_full @canned_response_pages %>

        + <% end %> +<% else %> +

        <%= l(:label_no_data) %>

        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/add.js.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/add.js.erb new file mode 100644 index 0000000..c7948a2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/add.js.erb @@ -0,0 +1,43 @@ +(function($) { + +$.fn.insertAtCaret = function (myValue) { + + return this.each(function() { + + //IE support + if (document.selection) { + + this.focus(); + sel = document.selection.createRange(); + sel.text = myValue; + this.focus(); + + } else if (this.selectionStart || this.selectionStart == '0') { + + //MOZILLA / NETSCAPE support + var startPos = this.selectionStart; + var endPos = this.selectionEnd; + var scrollTop = this.scrollTop; + this.value = this.value.substring(0, startPos)+ myValue+ this.value.substring(endPos,this.value.length); + this.focus(); + this.selectionStart = startPos + myValue.length; + this.selectionEnd = startPos + myValue.length; + this.scrollTop = scrollTop; + + } else { + + this.value += myValue; + this.focus(); + } + }); +}; + +})(jQuery); + +$('#issue_notes').insertAtCaret("<%= raw escape_javascript(@content) %>") + +$('#helpdesk_canned_response').val(""); + +if ($('#cke_issue_notes').length > 0) { + CKEDITOR.instances['issue_notes'].insertHtml("<%= raw escape_javascript(@content) %>"); +} diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/edit.html.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/edit.html.erb new file mode 100644 index 0000000..58f669d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/edit.html.erb @@ -0,0 +1,6 @@ +

        <%=l(:label_helpdesk_canned_response)%>

        + +<%= labelled_form_for :canned_response, @canned_response, :url => { :action => 'update', :project_id => @project } do |f| %> +<%= render :partial => 'canned_responses/form', :locals => { :f => f } %> +<%= submit_tag l(:button_save) %> +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/index.html.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/index.html.erb new file mode 100644 index 0000000..e3d1b7b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/index.html.erb @@ -0,0 +1 @@ +<%= render :partial => 'index' %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/canned_responses/new.html.erb b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/new.html.erb new file mode 100644 index 0000000..69fb475 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/canned_responses/new.html.erb @@ -0,0 +1,6 @@ +

        <%=l(:label_helpdesk_new_canned_response)%>

        + +<%= labelled_form_for :canned_response, @canned_response, :url => { :action => 'create', :project_id => @project } do |f| %> +<%= render :partial => 'canned_responses/form', :locals => { :f => f } %> +<%= submit_tag l(:button_create) %> +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/contacts/_helpdesk_tickets.html.erb b/plugins/redmine_contacts_helpdesk/app/views/contacts/_helpdesk_tickets.html.erb new file mode 100644 index 0000000..20a800f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/contacts/_helpdesk_tickets.html.erb @@ -0,0 +1,54 @@ +<% tickets_scope = @contact.all_tickets.visible.order_by_status %> + +<% tickets = tickets_scope %> +
        +
        + <%= link_to l(:label_helpdesk_ticket_new), {:controller => 'issues', + :action => 'new', + :customer_id => @contact, + :tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id], + :project_id => @project} if User.current.allowed_to?(:add_issues, @project) && User.current.allowed_to?(:send_response, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %> +
        + +

        <%= link_to(l(:label_helpdesk_ticket_plural), {:controller => 'issues', + :action => 'index', + :set_filter => 1, + :customer => [@contact.id], + :status_id => "*", + :c => ["project", "tracker", "status", "subject", "customer", "customer_company", "last_message"], + :sort => 'priority:desc,updated_on:desc'}) %>

        + +<% if tickets && tickets.any? %> + <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %> + + + <% for ticket in tickets %> + + + + <% if @contact.is_company %> + + <% end %> + + + <% end %> + +
        + <%= check_box_tag("ids[]", ticket.id, false, :style => 'display:none;', :id => nil) %> + + + <%= link_to "##{ticket.id} - #{truncate(ticket.subject, :length => 60)} (#{ticket.status})", issue_path(ticket), :class => ticket.css_classes %> + <%= contact_tag(ticket.customer, :type => 'plain') %> + <%= ticket.description.truncate(250) %> +
        + <% end %> + <% if Redmine::VERSION.to_s >= '3.4' || RedmineContacts.unstable_branch? %> + <%= context_menu %> + <% else %> + <%= context_menu issues_context_menu_path %> + <% end %> +<% else %> +

        <%= l(:label_no_data) %>

        +<% end %> + +
        diff --git a/plugins/redmine_contacts_helpdesk/app/views/context_menus/_helpdesk_contacts.html.erb b/plugins/redmine_contacts_helpdesk/app/views/context_menus/_helpdesk_contacts.html.erb new file mode 100644 index 0000000..b511eeb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/context_menus/_helpdesk_contacts.html.erb @@ -0,0 +1,13 @@ +<% if @contact && User.current.allowed_to?(:view_helpdesk_tickets, @project) && User.current.allowed_to?(:add_issues, @project) && HelpdeskSettings["helpdesk_tracker", @project.id] %> +
      • <%= context_menu_link l(:label_helpdesk_ticket_new), {:controller => 'issues', + :action => 'new', + :customer_id => @contact, + :tracker_id => HelpdeskSettings["helpdesk_tracker", @project.id], + :project_id => @project, + :back_url => @back}, + :method => :get, + :class => 'icon-support' %> +
      • +<% end %> + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_index.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_index.html.erb new file mode 100644 index 0000000..1c52ea0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_index.html.erb @@ -0,0 +1,81 @@ +
        +<% if !@query.new_record? && @query.editable_by?(User.current) %> + <%= link_to l(:button_edit), edit_query_path(@query), :class => 'icon icon-edit' %> + <%= delete_link query_path(@query) %> +<% end %> +
        + +

        <%= @query.new_record? ? l(:label_issue_plural) : h(@query.name) %>

        +<% html_title(@query.new_record? ? l(:label_issue_plural) : @query.name) %> + +<%= form_tag({ :controller => 'issues', :action => 'index', :project_id => @project }, + :method => :get, :id => 'query_form') do %> + <%= hidden_field_tag 'set_filter', '1' %> +
        +
        "> + <%= l(:label_filter_plural) %> +
        "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
        +
        + +
        +

        + + <%= link_to_function l(:button_apply), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> + <% if @query.new_record? && User.current.allowed_to?(:save_queries, @project, :global => true) %> + <%= link_to_function l(:button_save), + "$('#query_form').attr('action', '#{ @project ? new_project_query_path(@project) : new_query_path }'); submit_query_form('query_form')", + :class => 'icon icon-save' %> + <% end %> +

        +<% end %> + +<%= error_messages_for 'query' %> +<% if @query.valid? %> +<% if @issues.empty? %> +

        <%= l(:label_no_data) %>

        +<% else %> +<%= render :partial => 'helpdesk/list', :locals => {:issues => @issues, :query => @query} %> +<%= pagination_links_full @issue_pages, @issue_count %> +<% end %> + +<% end %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/sidebar' %> +<% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %> +<% end %> + +<%= context_menu issues_context_menu_path %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_list.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_list.html.erb new file mode 100644 index 0000000..e8c4c8d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/_list.html.erb @@ -0,0 +1,66 @@ +<%= form_tag({}) do -%> + <%= hidden_field_tag 'back_url', url_for(params) %> + <%= hidden_field_tag 'project_id', @project.id if @project %> + + + + <% previous_group = false %> + <% @issues.each do |issue| %> + <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %> + <% reset_cycle %> + + + + <% previous_group = group %> + <% end %> + + + <% if Setting.gravatar_enabled? %> + + <% end %> + + + + + <% end %> + + +
        +   + <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> (<%= @issue_count_by_group[group] %>) + <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}", "toggleAllRowGroups(this)", :class => 'toggle-all') %> +
        + <%= check_box_tag("ids[]", issue.id, false, :id => nil) %> + + <% if issue.customer %> + <%= link_to avatar_to(issue.customer, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => issue.customer.id}, :id => "avatar" %> + <% else %> + <%= avatar(issue.author, :size => "32x32", :height => 32, :width => 32) %> + <% end %> + +

        <%= link_to "#{issue.subject}", {:controller => :issues, :action => :show, :id => issue.id} %> #<%= issue.id %>

        +

        + <%= issue.description.gsub("(\n|\r)", "").strip.truncate(100) unless issue.description.blank? %> +

        +

        + <%= issue.customer ? "#{content_tag('span', '', :class => "icon icon-email", :title => l(:label_note_type_email))} #{l(:label_helpdesk_from)}: #{link_to_source(issue.customer)}, ".html_safe : "#{l(:label_helpdesk_from)}: #{link_to_user issue.author}, ".html_safe %> + <%= l(:label_updated_time, time_tag(issue.updated_on)).html_safe %> +

        +
        + <%= content_tag(:span, issue.status.name, :class => "deal-status ticket-status tags status-#{issue.status.id}") %> + + <% if issue.assigned_to %> +
        <%= l(:field_assigned_to) %>: <%= link_to_user issue.assigned_to %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %> +
        + <% end %> +
        <%= l(:field_priority) %>: <%= issue.priority.name %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %> +
        + <% if issue.due_date %> +
        <%= l(:field_due_date) %>: <%= format_date issue.due_date %><%# "#{issue.currency} " if issue.currency %><%# issue_price(issue.amount) %> +
        + <% end %> +
        + +<% end %> + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/get_mail.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/get_mail.js.erb new file mode 100644 index 0000000..0023a78 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/get_mail.js.erb @@ -0,0 +1 @@ +$('#test_connection_messages').html('<%= escape_javascript @message.html_safe %>') \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.api.rsb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.api.rsb new file mode 100644 index 0000000..c47bc1e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.api.rsb @@ -0,0 +1,7 @@ +api.message do + api.journal_id @journal.id + api.content @journal.notes + api.to_address @journal_message.to_address + api.message_date format_date(@journal_message.message_date) + api.customer(:id => @issue.customer.id, :name => @issue.customer.name) unless @issue.customer.nil? +end diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.html.erb new file mode 100644 index 0000000..e41db5a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/show.html.erb @@ -0,0 +1,332 @@ +<%= render :partial => 'issues/action_menu' %> + + + +<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %> + +<% end %> + +<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %> + +<% end %> + + +

        <%= issue_heading(@issue) %>

        + +
        + <% if @prev_issue_id || @next_issue_id %> + + <% end %> + +
        +<%= render_issue_subject_with_tree(@issue) %> +
        +

        + <%= l(:label_added_time_by, :author => @issue.author.instance_of?(AnonymousUser) ? link_to_source(@issue.contacts.first) : link_to_user(@issue.author), :age => time_tag(@issue.created_on)).html_safe %> + <% if @issue.created_on != @issue.updated_on %> + <%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>. + <% end %> +

        + +<% if @issue.description? || @issue.attachments.any? -%> +
        +<% if @issue.description? %> +
        + <%= link_to l(:button_quote), + {:controller => 'journals', :action => 'new', :id => @issue}, + :remote => true, + :method => 'post', + :class => 'icon icon-comment' if authorize_for('issues', 'edit') %> +
        + +
        + <%= textilizable @issue, :description, :attachments => @issue.attachments %> +
        +<% end %> +<%= link_to_attachments @issue, :thumbnails => true %> +<% end -%> + +
        + +<% if @journals.present? %> +
        +

        <%=l(:label_history)%>

        + <% reply_links = authorize_for('issues', 'edit') -%> + <% for journal in @journals.select{|j| !j.notes.blank? } %> +
        + <% if journal.is_incoming? %> + <%= link_to avatar_to(journal.contacts.first, :size => "32"), {:controller => 'contacts', :action => 'show', :project_id => @project, :id => journal.contacts.first.id}, :id => "avatar", :class => "ticket-avatar gravatar" unless journal.contacts.blank? %> + <% else %> + <%= avatar(journal.user, :size => "32x32", :height => 32, :width => 32, :class => "ticket-avatar gravatar") %> + <% end %> + +
        +

        + <%= link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes", + { :controller => 'journals', :action => 'edit', :id => journal, :format => 'js' }, + :title => l(:button_edit), + :class => "journal-link") if reply_links %> + <%= link_to(image_tag('comment.png'), + {:controller => 'journals', :action => 'new', :id => @issue, :journal_id => journal}, + :remote => true, + :method => 'post', + :title => l(:button_quote), + :class => "journal-link") %> + <% if journal.contacts && journal.contacts.any? && User.current.allowed_to?(:view_helpdesk_tickets, @project) %> + + + + <% if journal.is_incoming? %> + <%= "#{link_to_source journal.contacts.first} (#{journal.journal_messages.first.email})".html_safe unless journal.contacts.blank? %> + <% if journal.journal_messages.first.attachments.any? %> + <% attachment = journal.journal_messages.first.attachments.first %> + + <%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :class => 'icon icon-attachment' -%> + <%= h(" - #{attachment.description}") unless attachment.description.blank? %> + (<%= number_to_human_size attachment.filesize %>) + <%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"), + :controller => 'helpdesk', :action => 'show_original', + :id => attachment, :project_id => @project %> + + <% end %> + <% else %> + <%= link_to_user journal.user %> + + <%= l(:label_sent_to) %> + <% journal.journal_messages.each do |journal_message| %> + + <%= link_to_source(journal_message.contact) %> + (<%= journal_message.email %>) + + <% end %> + + <% end %> + - <%= format_time(@issue.updated_on).html_safe %>. + + <%# authoring journal.created_on, journal.user, :label => :label_updated_time_by %> + + + + + + + <% end %> +

        + +
        + <%= textilizable(journal, :notes) %> +
        +
        +
        + <% end %> + + <% heads_for_wiki_formatter if User.current.allowed_to?(:edit_issue_notes, @issue.project) || User.current.allowed_to?(:edit_own_issue_notes, @issue.project) %> + +
        +<% end %> + + +
        +<%= render :partial => 'issues/action_menu' %> + +
        +<% if authorize_for('issues', 'edit') %> + +<% end %> + +<% other_formats_links do |f| %> + <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %> + <%= f.link_to 'PDF' %> +<% end %> + +<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> + +<% content_for :sidebar do %> +
        + <% if authorize_for('issues', 'edit') %> +
        + <%= link_to l(:button_update), :onclick => '#' %> +
        + <% end %> +

        <%= l(:label_helpdesk_ticket_attributes) %>

        + + <%= issue_fields_rows do |rows| + rows.left l(:field_status), h(@issue.status.name), :class => 'status' + rows.left l(:field_priority), h(@issue.priority.name), :class => 'priority' + + unless @issue.disabled_core_fields.include?('assigned_to_id') + rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to' + end + unless @issue.disabled_core_fields.include?('category_id') || @issue.category.blank? + rows.left l(:field_category), h(@issue.category ? @issue.category.name : "-"), :class => 'category' + end + unless @issue.disabled_core_fields.include?('fixed_version_id') || @issue.fixed_version.blank? + rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version' + end + + unless @issue.disabled_core_fields.include?('start_date') || @issue.start_date.blank? + rows.left l(:field_start_date), format_date(@issue.start_date), :class => 'start-date' + end + unless @issue.disabled_core_fields.include?('due_date') || @issue.due_date.blank? + rows.left l(:field_due_date), format_date(@issue.due_date), :class => 'due-date' + end + unless @issue.disabled_core_fields.include?('done_ratio') + rows.left l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress' + end + unless @issue.disabled_core_fields.include?('estimated_hours') + unless @issue.estimated_hours.nil? + rows.left l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' + end + end + if User.current.allowed_to?(:view_time_entries, @project) && @issue.total_spent_hours > 0 + rows.left l(:label_spent_time), (@issue.total_spent_hours > 0 ? (link_to l_hours(@issue.total_spent_hours), {:controller => 'timelog', :action => 'index', :project_id => @project, :issue_id => @issue}) : "-"), :class => 'spent-time' + end + end %> + <%= render_custom_fields_rows(@issue) %> +
        +
        + +
        + + + + <% if RedmineHelpdesk.settings[:show_contact_card] %> +

        <%= l(:label_helpdesk_contact) %>

        + <% @issue.contacts.each do |contact| %> + + <%= render :partial => 'contacts/contact_card', :object => contact %> + + <% end %> + <% end %> + + <% if (issues_count = Issue.count(:include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"]) - 1) > 0 %> +

        <%= "#{l(:label_helpdesk_contact_activity)} (#{issues_count})" %>

        + +
          + <% (Issue.visible.find(:all, :include => :contacts, :conditions => ["#{Contact.table_name}.id IN (#{@issue.contact_ids.join(', ')})"], :order => "#{Issue.table_name}.status_id, #{Issue.table_name}.due_date DESC, #{Issue.table_name}.updated_on DESC", :limit => RedmineHelpdesk.settings[:last_message_count].to_i > 0 ? RedmineHelpdesk.settings[:last_message_count].to_i : 11) - [@issue]).each do |issue| %> +
        • + <%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project)) %> +
        • + <% end %> +
        +
        + <%= link_to l(:label_issue_view_all), {:controller => 'issues', + :action => 'index', + :set_filter => 1, + :f => [:contacts, :status_id], + :v => {:contacts => @issue.contact_ids}, + :op => {:contacts => '=', :status_id => '*'}} %> +
        + <% end %> + +
        + + <% if User.current.allowed_to?(:add_issue_watchers, @project) || + (@issue.watchers.present? && User.current.allowed_to?(:view_issue_watchers, @project)) %> +
        + <%= render :partial => 'watchers/watchers', :locals => {:watched => @issue} %> +
        + <% end %> +<% end %> + +<% content_for :header_tags do %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %> + <%= auto_discovery_link_tag(:atom, {:format => 'atom', :key => User.current.rss_key}, :title => "#{@issue.project} - #{@issue.tracker} ##{@issue.id}: #{@issue.subject}") %> +<% end %> + +<%= context_menu issues_context_menu_path %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_customer_email.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_customer_email.js.erb new file mode 100644 index 0000000..e35eade --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_customer_email.js.erb @@ -0,0 +1,9 @@ +$('#customer_to_email').html('<%= escape_javascript(render :partial => "issues/customer_to_email", :locals => {:contact => @contact, :contact_email => @email}) %>') + +$("#helpdesk_to").val("<%= @email %>"); +$("#helpdesk_to").attr("value", "<%= @email %>") +$("#helpdesk_to").trigger('change'); +$("#helpdesk_cc").val("<%= @cc_emails.join(',') %>") +$("#helpdesk_cc").trigger('change'); + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_ticket_data.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_ticket_data.js.erb new file mode 100644 index 0000000..e64ad9a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk/update_ticket_data.js.erb @@ -0,0 +1 @@ +$('#cusomer_profile_and_issues').html('<%= escape_javascript(render :partial => "issues/helpdesk_customer_profile") %>') \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.html.erb new file mode 100644 index 0000000..4d2c4fd --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.html.erb @@ -0,0 +1,21 @@ + + + + + + + +
        + <%= textile(@email_header.to_s).html_safe unless @email_header.blank? %> +
        +
        + <%= textile(@email_body.to_s).html_safe %> +
        + + + \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.text.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.text.erb new file mode 100644 index 0000000..cc825fa --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_mailer/email_layout.text.erb @@ -0,0 +1,3 @@ +<%= @email_header %> +<%= @email_body %> +<%= @email_footer %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_busiest_time_of_day_metrics.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_busiest_time_of_day_metrics.html.erb new file mode 100644 index 0000000..0539303 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_busiest_time_of_day_metrics.html.erb @@ -0,0 +1,25 @@ + + +

        <%= l(:label_helpdesk_busiest_time_of_day_new_tickets) %>

        +
        <%= @collector.new_issues_count %>
        +
        + <%= progress_in_percents(-@collector.new_issue_count_progress) %> +
        + + +

        <%= l(:label_helpdesk_busiest_time_of_day_new_contacts) %>

        +
        <%= @collector.contacts_count %>
        +
        + <%= progress_in_percents(-@collector.total_contacts_count_progress) %> +
        + + + + +

        <%= l(:label_helpdesk_busiest_time_of_day_total_incoming) %>

        +
        <%= @collector.issues_count %>
        +
        + <%= progress_in_percents(-@collector.issue_count_progress) %> +
        + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_chart.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_chart.html.erb new file mode 100644 index 0000000..f6b7a2b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_chart.html.erb @@ -0,0 +1,33 @@ +<% if @collector.issues_count.zero? %> +

        <%= l(:label_no_data) %>

        +<% else %> +
        + + + <% @collector.columns.each do |column| %> + + <% end %> + + + <% @collector.columns.each do |column| %> + + <% end %> + + + <% @collector.columns.each do |column| %> + + <% end %> + + <%= render :partial => "#{@report}_metrics" %> +
        +

        <%= column[:issues_count] %>

        +

        <%= [column[:issues_percent], '%'].join %>

        +
        + <% if column[:issues_count] > 0 %> +
        + <% end %> +
        +
        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_first_response_time_metrics.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_first_response_time_metrics.html.erb new file mode 100644 index 0000000..e7f147e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/_first_response_time_metrics.html.erb @@ -0,0 +1,33 @@ + + +

        <%= l(:label_helpdesk_average_first_response_time) %>

        +
        <%= helpdesk_time_label(@collector.average_response_time) %>
        +
        + <%= mirror_progress_in_percents(@collector.average_response_time_progress) %> +
        + + +

        <%= l(:label_helpdesk_average_time_to_close) %>

        +
        <%= helpdesk_time_label(@collector.average_close_time) %>
        +
        + <%= mirror_progress_in_percents(@collector.average_close_time_progress) %> +
        + + + + +

        <%= l(:label_helpdesk_average_responses_count) %>

        +
        <%= @collector.average_response_count %>
        +
        + <%= progress_in_percents(@collector.average_response_count_progress) %> +
        + + +

        <%= l(:label_helpdesk_total_replies) %>

        +
        <%= @collector.total_response_count %>
        +
        + <%= progress_in_percents(-@collector.total_response_count_progress) %> +
        + + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/show.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/show.html.erb new file mode 100644 index 0000000..796f2eb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_reports/show.html.erb @@ -0,0 +1,29 @@ +

        <%= l("label_helpdesk_report_names_#{@report}") %>

        +<% html_title(l("label_helpdesk_report_names_#{@report}")) %> + + +<%= form_tag({ :controller => 'helpdesk_reports', :action => 'show', :project_id => @project }, + :method => :get, :id => 'query_form') do %> +
        + <%= hidden_field_tag 'set_filter', '1' %> +
        +
        "> + <%= l(:label_filter_plural) %> +
        "> + <%= render :partial => 'queries/filters', :locals => {:query => @query} %> +
        +
        +
        +

        + <%= link_to_function l(:button_apply), '$("#query_form").submit()', :class => 'icon icon-checked' %> + <%= link_to l(:button_clear), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> +

        +
        +<% end %> + +<%= error_messages_for 'query' %> +<%= render :partial => 'chart' %> + +<% content_for :sidebar do %> + <%= render :partial => 'issues/helpdesk_reports' %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/destroy.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/destroy.html.erb new file mode 100644 index 0000000..6a42727 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/destroy.html.erb @@ -0,0 +1 @@ +

        HelpdeskTicketsController#destroy

        diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/edit.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/edit.js.erb new file mode 100644 index 0000000..e7e6d2c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/edit.js.erb @@ -0,0 +1,23 @@ +$('#cusomer_profile_and_issues').html('<%= escape_javascript(render :partial => "issues/helpdesk_customer_profile") %>') +$('#helpdesk_ticket_cc_address').select2({ + ajax: { + url: '<%= auto_complete_contacts_path(:project_id => (ContactsSetting.cross_project_contacts? ? nil : @project), :is_company => nil) %>', + dataType: 'json', + delay: 250, + data: function (params) { + return { q: params.term }; + }, + processResults: function (data, params) { + return { results: $.grep(data, function(elem){ elem.id = elem.email; return elem }) }; + }, + cache: true + }, + tags: true, + placeholder: ' ', + minimumInputLength: 1, + width: '100%', + templateResult: ccEmailTagResult, + templateSelection: ccEmailTagSelection, +}).on('select2:open', function (e) { + $('#helpdesk_ticket_cc_address').closest('p').find('.select2-search__field').val(' ').trigger($.Event('input', { which: 13 })).val(''); +}); diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/update.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/update.html.erb new file mode 100644 index 0000000..ad7dfda --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_tickets/update.html.erb @@ -0,0 +1 @@ +

        HelpdeskTicketsController#update

        diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/show.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/show.html.erb new file mode 100644 index 0000000..41e49b0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/show.html.erb @@ -0,0 +1,28 @@ +
        +

        <%= l(:label_helpdesk_mark) %>

        + <%= form_tag helpdesk_votes_vote_path(:id => @ticket.id, :hash => @ticket.token) do %> +

        + <%= label_tag :vote_2, nil, :class => 'vote-value' do %> + <%= radio_button_tag('vote', 2, @ticket.vote == 2 || @ticket.vote == nil ? true : false) %> + <%= t(:label_helpdesk_mark_awesome) %> + <% end %> + + <%= label_tag :vote_1, nil, :class => 'vote-value' do %> + <%= radio_button_tag('vote', 1, @ticket.vote == 1 ? true : false) %> + <%= t(:label_helpdesk_mark_justok) %> + <% end %> + + <%= label_tag :vote_0, nil, :class => 'vote-value' do %> + <%= radio_button_tag('vote', 0, @ticket.vote == 0 ? true : false) %> + <%= t(:label_helpdesk_mark_notgood) %> + <% end %> +

        + <%- if RedmineHelpdesk.vote_comment_allow? %> + <%= text_area_tag('vote_comment', nil, { :size => '60x12', :placeholder => t(:label_helpdesk_vote_comment_placeholder) }) %> + <% end %> + +
        + <%= submit_tag(t(:label_helpdesk_submit)) %> +
        + <% end %> +
        diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/vote.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/vote.html.erb new file mode 100644 index 0000000..b471e45 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_votes/vote.html.erb @@ -0,0 +1,3 @@ +
        +

        <%= t(:label_helpdesk_vote_thank) %>

        +
        \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/avatar.html.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/avatar.html.erb new file mode 100644 index 0000000..9c18ca4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/avatar.html.erb @@ -0,0 +1 @@ +<%= avatar(@user, :size => 54, :id => 'avatar') %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/iframe.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/iframe.js.erb new file mode 100644 index 0000000..99a9e62 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/iframe.js.erb @@ -0,0 +1,226 @@ +function getXmlHttp(){ + var xmlhttp; + try { + xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + try { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (E) { + xmlhttp = false; + } + } + if (!xmlhttp && typeof XMLHttpRequest!='undefined') { + xmlhttp = new XMLHttpRequest(); + } + return xmlhttp; +} + +function serialize(form){ + var boundary = String(Math.random()).slice(2); + var boundaryMiddle = '--' + boundary + '\r\n'; + var boundaryLast = '--' + boundary + '--\r\n' + var cont_start = 'Content-Disposition: form-data; name="'; + var cont_middle = '"\r\n\r\n'; + var cont_end = '\r\n'; + var field = ''; + var body = ['\r\n']; + if (typeof form == 'object' && form.nodeName == "FORM") { + for (index = form.elements.length - 1; index >= 0; index--) { + field = form.elements[index]; + if (field.type == 'select-multiple') { + for (option = form.elements[index].options.length - 1; option >= 0; option--) { + if (field.options[option].selected) { body.push(cont_start + field.name + cont_middle + field.options[option].value + cont_end); } + } + } else { + if (field.type != 'submit' && field.type != 'file' && field.type != 'button') { + if ((field.type != 'checkbox' && field.type != 'radio') || field.checked) { + body.push(cont_start + field.name + cont_middle + field.value + cont_end); + } + } else { + if (field.type == 'file'){ + if (field.files.length > 0) { + body.push(cont_start + field.name + cont_middle + field.attributes['data-value'] + cont_end); + body.push(cont_start + field.name + '_name' + cont_middle + field.files[0].name + cont_end); + } + } + } + } + } + } + return [boundary, body.join(boundaryMiddle) + boundaryLast]; +} + +function translation(field){ + return RedmineHelpdeskIframe.configuration['translation'] ? RedmineHelpdeskIframe.configuration['translation'][field] : null; +} + +function ticketCreated(){ + success_div = document.createElement('div'); + success_div.id = 'submit_button'; + success_div.className = 'success-message'; + success_div.style.textAlign = 'center'; + success_div.style.margin = '15%'; + success_div.style.font = '20px Arial'; + success_div.innerHTML = translation('createSuccessLabel') || '<%= t(:label_helpdesk_widget_ticket_created) %>'; + + success_desc_div = document.createElement('div'); + success_desc_div.style.textAlign = 'center'; + success_desc_div.style.margin = '5%'; + success_desc_div.style.font = '14px Arial'; + success_desc_div.innerHTML = translation('createSuccessDescription'); + + document.getElementById('widget_form').innerHTML = ''; + document.getElementById('widget_form').appendChild(success_div); + document.getElementById('widget_form').appendChild(success_desc_div); +} + +function ticketErrors(errors){ + errors_div = document.createElement('div'); + errors_div.id = 'ticket-error-details'; + errors_div.className = 'ticket-error-details'; + + error_p = document.createElement('div'); + error_p.innerHTML = translation('createErrorLabel') || '<%= t(:label_helpdesk_widget_ticket_errors) %>'; + errors_div.appendChild(error_p); + + errors_link = document.createElement('a'); + errors_link.id = 'ticket-errors-link'; + errors_link.href = 'javascript:void(0)'; + errors_link.style.paddingLeft = '10px'; + errors_link.addEventListener('click', function(){ toggleErrorsList() }); + errors_link.innerHTML = '<%= t(:label_helpdesk_widget_ticket_error_details) %>'; + error_p.appendChild(errors_link); + + ul = document.createElement('ul'); + ul.id = 'ticket-errors'; + ul.className = 'ticket-errors'; + ul.style.display = 'none'; + errors_div.appendChild(ul); + + for (var key in errors) { + if (key != 'base') { + processErrorForField(ul, key, errors[key]) + } else { + errors[key].forEach(function(error_text) { + processErrorForCustomField(ul, key, error_text); + }); + } + } + document.getElementById('flash').appendChild(errors_div); +} + +function createErrorLi(target, text){ + li = document.createElement('li'); + li.id = 'ticket-error'; + li.className = 'ticket-error'; + li.innerHTML = text; + target.appendChild(li); +} + +function markFieldAsError(element){ + element.style.border = ''; + element.classList.add('error_field'); + element.addEventListener('keyup', checkFieldContent); +} + +function markRequireFieldsAsError(){ + fields = document.querySelectorAll("[data-require='true'] > input, [data-require='true'] > select, [data-require='true'] > textarea, .required-field"); + required_fields = Array.from(fields); + var respose = false; + required_fields.forEach(function(field) { + if (field.value.length == 0) { + markFieldAsError(field); + respose = true; + } + }); + return respose; +} + +function unmarkFieldsAsError(){ + error_fields = Array.from(document.getElementsByClassName('error_field')); + error_fields.forEach(function(field) { + field.classList.remove('error_field'); + field.removeEventListener('keyup', checkFieldContent); + }); +} + +function checkFieldContent(){ + if (this.value.length > 0) { + this.style.border = '1px solid #d9d9d9'; + } else { + this.style.border = '1px solid red'; + } +} + +function processErrorForField(ul, key, error_text) { + createErrorLi(ul, error_text); + field = document.getElementById(key); + if (field != null) { markFieldAsError(field); } +} + +function processErrorForCustomField(ul, key, error_text) { + checkCustomFieldsOnError(error_text); + createErrorLi(ul, error_text); +} + +function checkCustomFieldsOnError(error_text){ + custom_fields = Array.from(document.getElementsByClassName('custom_field')); + custom_fields.forEach(function(cfield) { + cfield_regex = new RegExp(cfield.attributes['data-error-key'].value); + if (cfield_regex.test(error_text)){ + markFieldAsError(cfield.getElementsByTagName('input')[0]); + } + }); +} + +function toggleErrorsList(){ + errors_list = document.getElementById('ticket-errors'); + if (errors_list == null) { return true } + if (errors_list && errors_list.style.display == 'block') { + errors_list.style.display = 'none'; + } else { + errors_list.style.display = 'block'; + } +} + +function processResponse(response){ + if (response['result']) { + ticketCreated(); + parent.postMessage(JSON.stringify({ reload: true }), "*"); + } else { + ticketErrors(response['errors']); + } + var formSubmitBtn = document.getElementById('form-submit-btn'); + if (formSubmitBtn){ + formSubmitBtn.disabled = false; + } +} + +function needReloadProjectData(){ + parent.postMessage(JSON.stringify({ project_reload: true }), "*"); +} + +function submitTicketForm(){ + document.getElementById('flash').innerHTML = ''; + unmarkFieldsAsError(); + if (markRequireFieldsAsError()){ return false; } + + var base_url = '<%= Setting.protocol %>://<%= Setting.host_name %>' + var xmlhttp = getXmlHttp(); + var serialize_result = serialize(document.getElementById('widget_form')); + var boundary = serialize_result[0]; + var form_params = serialize_result[1]; + document.getElementById('form-submit-btn').disabled = true; + xmlhttp.open('POST', base_url + '/helpdesk_widget/create_ticket.js', true); + xmlhttp.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + if (xmlhttp.status == 200 || xmlhttp.status == 304) { + processResponse(JSON.parse(xmlhttp.responseText)); + } else { + processResponse({result: false, errors: []}); + } + } + }; + xmlhttp.send(form_params); +} diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/load_custom_fields.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/load_custom_fields.erb new file mode 100644 index 0000000..009b36a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/load_custom_fields.erb @@ -0,0 +1,6 @@ +<% @issue.editable_custom_field_values.each do |value| %> + <% if @enabled_cf && @enabled_cf.include?(value.custom_field_id.to_s) %> + <% required = value.custom_field.is_required? %> +

        <%= custom_field_tag_with_label :issue, value, :required => required %>

        + <% end %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.css b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.css new file mode 100644 index 0000000..2f6198d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.css @@ -0,0 +1,135 @@ +#widget_form { + padding: 10px; + font-family: helvetica, arial, sans-serif; +} + +#widget_form .form-control, +#widget_form .custom_fields input, +#widget_form .custom_fields select, +#widget_form .custom_fields textarea { + background: 0 0; + border: 1px solid #d9d9d9; + border-radius: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + color: #333; + display: block; + font-size: 12px; + font-weight: 400; + height: 36px; + line-height: 16px; + width: 100%; + padding: 8px 12px; + margin-bottom: 12px; + -webkit-appearance: none; + -moz-appearance: none; +} + +#widget_form select.form-control, +#widget_form .custom_fields select { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAAAXNSR0IArs4c6QAAATNJREFUKBW1kc1KxDAUhW/CxM2M4EO4m7UwrzDDDBUXRRDxPbpw4cL3EHFhYcTSEh/BhevufAhhOptJabwnmhIjI+JPIDS5P1/OPSX64yW01o/GmNMkSZ5/wy6KYl8pdS0F0UQNBk8Mnv4UiF4wwJLW2qUQYo+srTiRMZTj316YMEMvGI4FgK6qjIS44KDk5N1qvT5L07T5Cpvn+Wh3OLziviMGddx3PpvPL3s1D2U5s1LeOLVE9caYw22+wq8dpe75wTHDXkTXnUwXCw0B0qtAwLTtAd9r3mN4gkd83n8RQw41vGv0eBhqeiAuULRqmgnGdp5IWQa+Or94itJNAWu4Np6iHxnAYH3y1eUivzhmg563kjgQ3iNfWfhHv8Jaf96m0Ofp/QfcgrZp2+N4xL7wvw6vkGme5fEw/bwAAAAASUVORK5CYII=) 97% 14px no-repeat; + background-size: 12px 9px; +} + +#widget_form .custom_fields textarea, +#widget_form textarea.form-control { + height: 120px; +} + + +#widget_form .custom_fields select[multiple] { + height: 120px; +} + +#widget_form .title { + color: #333; + padding-bottom: 3px; + font-size: 12px; +} + +#widget_form .custom_fields label > span { + color: #333; + padding-bottom: 3px; + font-size: 12px; +} + +#widget_form #flash { + color: red; + padding-bottom: 3px; + font-size: 12px; +} + +#widget_form input.error_field, +#widget_form select.error_field, +#widget_form textarea.error_field, +#widget_form .custom_fields input.error_field, +#widget_form .custom_fields select.error_field, +#widget_form .custom_fields textarea.error_field { + border: 1px solid red; +} + +#widget_form .submit_button{ + height: 40px; +} + +#widget_form .attach_div{ + position: relative; + float: right; + margin: 10px; + min-width: 80px; + text-align: right; +} + +#widget_form .attach_link{ + position: relative; + font: 14px Arial; + color: #3699ca; +} + +#widget_form .attach_field{ + position: absolute; + top: 0; + left: -80px; + width: 160px; + opacity: 0; + cursor: pointer; +} + +#widget_form .btn { + float: right; + background: #3699ca; + color: #fff; + height: 36px; + padding: 0 14px; + line-height: 30px; + font-weight: 500; + text-align: center; + border: 0px; + font-size: 14px; + border-radius: 4px; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +#widget_from .attach { + display: inline-block; + float: right; + font-size: 12px; + margin: 10px 16px 0 0; + max-width: 210px; + text-decoration: underline; + color: #2996cc; +} + +#helpdesk_widget.open { + border-radius: 0 100% 100% !important; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); +} diff --git a/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.js.erb b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.js.erb new file mode 100644 index 0000000..1436578 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/helpdesk_widget/widget.js.erb @@ -0,0 +1,556 @@ +function getXmlHttp(){ + var xmlhttp; + try { + xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); + } catch (e) { + try { + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } catch (E) { + xmlhttp = false; + } + } + if (!xmlhttp && typeof XMLHttpRequest!='undefined') { + xmlhttp = new XMLHttpRequest(); + } + return xmlhttp; +} + +var RedmineHelpdeskWidget = { + widget: document.getElementById('helpdesk_widget'), + widget_button: null, + width: 400, + height: 500, + margin: 20, + iframe: null, + form: null, + schema: null, + reload: false, + configuration: {}, + attachment: null, + base_url: '<%= Setting.protocol %>://<%= Setting.host_name %>', + config: function(configuration){ + this.configuration = configuration; + this.apply_config(); + }, + apply_config: function(){ + if (this.configuration['color']) { + this.widget_button.style.backgroundColor = this.configuration['color']; + } + switch (this.configuration['position']) { + case 'topLeft': + this.widget.style.top = '20px'; + this.widget.style.left = '20px'; + break; + case 'topRight': + this.widget.style.top = '20px'; + this.widget.style.right = '20px'; + break; + case 'bottomLeft': + this.widget.style.bottom = '20px'; + this.widget.style.left = '20px'; + break; + case 'bottomRight': + this.widget.style.bottom = '20px'; + this.widget.style.right = '20px'; + break; + default: + widget.style.bottom = '20px'; + widget.style.right = '20px'; + } + }, + translation: function(field){ + return this.configuration['translation'] && this.configuration['translation'][field] ? this.configuration['translation'][field] : null; + }, + identify: function(field){ + return this.configuration['identify'] && this.configuration['identify'][field] ? this.configuration['identify'][field] : null + }, + load: function() { + this.widget.addEventListener('click', function(){ RedmineHelpdeskWidget.toggle() }); + this.create_widget_button(); + this.decorate_widget_button(); + this.create_iframe(); + this.decorate_iframe(); + this.load_schema(); + this.created = true; + }, + load_schema: function() { + var xmlhttp = getXmlHttp(); + xmlhttp.open('GET', this.base_url + '/helpdesk_widget/load_form.json', true); + xmlhttp.responseType = 'json'; + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + if (xmlhttp.status == 200 || xmlhttp.status == 304) { + RedmineHelpdeskWidget.schema = xmlhttp.response; + RedmineHelpdeskWidget.fill_form(); + } else { + RedmineHelpdeskWidget.schema = {}; + } + } + }; + xmlhttp.send(null); + }, + create_widget_button: function(){ + button = document.createElement('div'); + button.id = 'widget_button'; + button.className = 'widget_button'; + button.innerHTML = this.configuration['icon'] || '?'; + button.setAttribute('name', 'helpdesk_widget_button'); + button.style.backgroundColor = '#7E8387'; + button.style.backgroundSize = '15px 15px'; + button.style.cursor = 'pointer'; + button.style.color = 'white'; + button.style.textAlign = 'center'; + button.style.fontSize = '32px'; + button.style.verticalAlign = 'middle'; + button.style.lineHeight = '54px'; + button.style.borderRadius = '30px'; + button.style.boxShadow = 'rgba(0, 0, 0, 0.258824) 0px 2px 5px 0px'; + button.style.display = 'none'; + button.style.webkitTransition = "transform 0.2s ease"; + this.widget_button = button; + this.widget.appendChild(button); + }, + decorate_widget_button: function(){ + widget = this.widget; + widget.style.position = 'fixed'; + widget.style.bottom = '20px'; + widget.style.right = '20px'; + widget.style.width = '54px'; + widget.style.height = '54px'; + widget.style.zIndex = 9999; + }, + create_iframe: function(){ + this.iframe = document.createElement('iframe'); + this.widget.appendChild(this.iframe); + }, + decorate_iframe: function(){ + iframe = this.iframe; + iframe.setAttribute('id', 'helpdesk_ticket_container'); + iframe.setAttribute('width', this.width); + iframe.setAttribute('height', 0); + iframe.setAttribute('frameborder', 0); + iframe.style.visibility = 'hidden'; + iframe.style.position = 'absolute'; + iframe.style.opacity = '0'; + iframe.style.width = this.width; + iframe.style.backgroundColor = 'white'; + iframe.style.webkitTransition = "opacity 0.2s ease"; + iframe.style.boxShadow = 'rgba(0, 0, 0, 0.258824) 0px 1px 4px 0px'; + iframe.setAttribute('name', 'helpdesk_widget_iframe'); + }, + fill_form: function(){ + if (Object.keys(this.schema.projects).length > 0) { + this.apply_avatar(); + this.create_form(); + this.create_form_title(); + this.create_error_flash(); + + if (this.identify('redmineUserID')) { + this.create_form_hidden(this.form, 'redmine_user', 'redmine_user', 'form-control', this.identify('redmineUserID')); + } + if (this.identify('nameValue')) { + this.create_form_hidden(this.form, 'username', 'username', 'form-control', this.identify('nameValue')); + } else { + this.create_form_text(this.form, 'username', 'username', this.translation('nameLabel') || '<%= t(:label_helpdesk_widget_name) %>', 'form-control', this.identify('nameValue'), true); + } + if (this.identify('emailValue')) { + this.create_form_hidden(this.form, 'email', 'email', 'form-control', this.identify('emailValue')); + } else { + this.create_form_text(this.form, 'email', 'email' , this.translation('emailLabel') || '<%= t(:label_helpdesk_widget_email) %>', 'form-control', this.identify('emailValue'), true); + } + if (this.identify('subjectValue')) { + this.create_form_hidden(this.form, 'subject', 'issue[subject]', 'form-control', this.identify('subjectValue')); + } else { + this.create_form_text(this.form, 'subject', 'issue[subject]' , this.translation('subjectLabel') || '<%= t(:label_helpdesk_widget_subject) %>', 'form-control', this.identify('subjectValue'), true); + } + this.create_projects_selector(); + this.create_form_area(this.form, 'description', 'issue[description]' , this.translation('descriptionLabel') || '<%= t(:label_helpdesk_widget_description) %>', 'form-control', true); + + var project_id = null; + var tracker_id = null; + if (RedmineHelpdeskWidget.configuration['identify']){ + project_id = RedmineHelpdeskWidget.schema.projects[RedmineHelpdeskWidget.configuration['identify']['projectValue']]; + if (project_id) { + tracker_id = RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']]; + } + } + + this.load_project_data(project_id || this.schema.projects[Object.keys(this.schema.projects)[0]], tracker_id); + this.iframe.contentWindow.document.body.appendChild(this.form); + this.append_stylesheets(); + this.append_scripts(); + this.create_message_listener(); + } else { + this.widget.style.display = 'none'; + } + }, + apply_avatar: function(){ + button = document.getElementById('widget_button'); + avatar = RedmineHelpdeskWidget.configuration['user_avatar']; + if (avatar && avatar.length > 0) { + var xmlhttp = getXmlHttp(); + xmlhttp.open('GET', RedmineHelpdeskWidget.base_url + '/helpdesk_widget/avatar/' + avatar, true); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + if (xmlhttp.status == 200 || xmlhttp.status == 304) { + button.style.backgroundSize = 'cover'; + button.style.backgroundImage = 'url(' + RedmineHelpdeskWidget.base_url + '/helpdesk_widget/avatar/' + avatar + ')' ; + button.style.border = '2px solid'; + button.innerHTML = ' '; + } else { + button.style.backgroundSize = '15px 15px'; + button.innerHTML = '?'; + } + button.style.display = 'block'; + button.style.lineHeight = '50px'; + } + }; + xmlhttp.send(null); + } else { + button.style.lineHeight = '54px'; + button.style.backgroundSize = '15px 15px'; + button.innerHTML = '?'; + button.style.display = 'block'; + } + }, + append_stylesheets: function(){ + + if (this.configuration['styles']) { + styles_css = document.createElement('style'); + styles_css.innerHTML = this.configuration['styles']; + styles_css.type = "text/css"; + } + widget_css = document.createElement('link'); + widget_css.href = this.base_url + '/helpdesk_widget/widget.css'; + widget_css.rel = "stylesheet"; + widget_css.type = "text/css"; + this.iframe.contentWindow.document.head.appendChild(widget_css); + if (this.configuration['styles']) { + this.iframe.contentWindow.document.head.appendChild(styles_css); + } + }, + append_scripts: function(){ + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = this.base_url + '/helpdesk_widget/iframe.js'; + this.iframe.contentWindow.document.head.appendChild(script); + + config_script = document.createElement('script'); + config_script.innerHTML = "var RedmineHelpdeskIframe = {configuration: "+ JSON.stringify(this.configuration) +"}"; + this.iframe.contentWindow.document.head.appendChild(config_script); + }, + create_form: function(){ + this.form = document.createElement('form'); + this.form.action = this.base_url + '/helpdesk_widget/create_ticket'; + this.form.acceptCharset = 'UTF-8'; + this.form.method = 'post'; + this.form.id = 'widget_form'; + this.form.setAttribute('onSubmit', 'submitTicketForm(); return false;'); + this.form.style.marginBottom = 0; + }, + create_form_title: function(){ + if (this.configuration['title']) { + title_div = document.createElement('div'); + title_div.id = 'title'; + title_div.className = 'title'; + title_div.innerHTML = this.configuration['title']; + this.form.appendChild(title_div); + } + }, + create_error_flash: function(){ + flash_div = document.createElement('div'); + flash_div.id = 'flash'; + flash_div.className = 'flash'; + this.form.appendChild(flash_div); + }, + create_projects_selector: function(){ + var project_id = null; + if (RedmineHelpdeskWidget.configuration['identify']){ + project_id = RedmineHelpdeskWidget.schema.projects[RedmineHelpdeskWidget.configuration['identify']['projectValue']]; + } + if (project_id) { + this.create_form_hidden(this.form, 'project_id', 'project_id', 'form-control projects', project_id); + } else { + this.create_form_select(this.form, 'project_id', 'project_id', RedmineHelpdeskWidget.schema.projects, project_id, 'form-control projects'); + } + }, + load_project_data: function(project_id, tracker_id){ + container_div = this.form.getElementsByClassName('container')[0] + if (container_div) { container_div.remove() }; + + container_div = document.createElement('div'); + container_div.id = 'container'; + container_div.className = 'container'; + + custom_div = document.createElement('div'); + custom_div.id = 'custom_fields'; + custom_div.className = 'custom_fields'; + + submit_div = document.createElement('div'); + submit_div.id = 'submit_button'; + submit_div.className = 'submit_button'; + + container_div.appendChild(custom_div); + container_div.appendChild(submit_div); + + if (RedmineHelpdeskWidget.configuration['identify'] && RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']]){ + tracker_id = RedmineHelpdeskWidget.schema.projects_data[project_id].trackers[RedmineHelpdeskWidget.configuration['identify']['trackerValue']] + this.create_form_hidden(custom_div, 'tracker_id', 'tracker_id', 'form-control trackers', tracker_id); + } else { + this.create_form_select(custom_div, 'tracker_id', 'tracker_id', this.schema.projects_data[project_id].trackers, tracker_id, 'form-control trackers'); + tracker_id = custom_div.getElementsByClassName('trackers')[0].value; + } + this.load_custom_fields(custom_div, project_id, tracker_id); + + this.create_form_submit(submit_div, this.translation('createButtonLabel') || '<%= l(:label_helpdesk_widget_create_ticket) %>'); + this.create_attch_link(submit_div); + + this.form.appendChild(container_div); + }, + reload_project_data: function(){ + project_id = this.form.getElementsByClassName('projects')[0].value; + tracker_id = container_div.getElementsByClassName('trackers')[0].value; + + this.load_project_data(project_id, tracker_id); + this.positionate_iframe(); + }, + create_form_select: function(target, field_id, field_name, values, selected, field_class){ + + if (Object.keys(values).length == 1) { + field = document.createElement('input'); + field.type = 'hidden'; + field.id = field_id; + field.name = field_name; + field.className = field_class; + field.value = values[Object.keys(values)[0]]; + } else { + field = document.createElement('select'); + field.id = field_id; + field.name = field_name; + field.className = field_class; + for (var project in values) { + option = document.createElement('option'); + option.value = values[project] + if(values[project] == selected) { option.selected = 'selected'; } + option.innerHTML = project; + field.appendChild(option); + } + } + field.setAttribute('onChange', 'needReloadProjectData();'); + target.appendChild(field); + }, + create_form_hidden: function(target, field_id, field_name, field_class, value){ + field = document.createElement('input'); + field.type = 'hidden'; + field.id = field_id; + field.name = field_name; + field.value = value; + field.className = field_class; + target.appendChild(field); + }, + create_form_text: function(target, field_id, field_name, field_placeholder, field_class, value, required){ + field = document.createElement('input'); + field.type = 'text'; + field.id = field_id; + field.name = field_name; + field.value = value; + field.placeholder = field_placeholder; + field.className = required ? field_class + ' required-field' : field_class; + target.appendChild(field); + }, + create_form_area: function(target, field_id, field_name, field_placeholder, field_class, required){ + field = document.createElement('textarea'); + field.cols = 55; + field.rows = 10; + field.id = field_id; + field.name = field_name; + field.placeholder = field_placeholder; + field.className = required ? field_class + ' required-field' : field_class; + target.appendChild(field); + }, + create_form_submit: function(target, label){ + field = document.createElement('input'); + field.id = 'form-submit-btn'; + field.type = 'submit'; + field.name = 'submit'; + field.className = 'btn'; + field.value = label; + field.title = this.translation('buttomLabel') || ''; + if (RedmineHelpdeskWidget.configuration['color']) { + field.style.background = RedmineHelpdeskWidget.configuration['color']; + } + target.appendChild(field); + }, + create_attch_link: function(target){ + if (this.configuration['attachment'] != false ) { + attach_div = document.createElement('div'); + attach_div.className = 'attach_div'; + + attach_link = document.createElement('a'); + attach_link.className = 'attach_link'; + attach_link.href = 'javascript:void(0)'; + attach_link.innerHTML = this.translation('attachmentLinkLabel') || 'Attach a file'; + attach_div.appendChild(attach_link); + + attach_field = document.createElement('input'); + attach_field.type = 'file'; + attach_field.id = 'attachment'; + attach_field.className = 'attach_field'; + attach_field.name = 'attachment'; + attach_field.attributes['data-max-size'] = <%= Setting[:attachment_max_size].to_i * 1024 %>; + attach_field.addEventListener('change', function(){ RedmineHelpdeskWidget.upload_file() }); + attach_div.appendChild(attach_field); + this.attachment = attach_field; + + target.appendChild(attach_div); + } + }, + upload_file: function(){ + if (this.attachment.attributes['data-max-size'] > this.attachment.files[0].size) { + this.read_file(this.attachment.files[0], function(e){ + attach_field = RedmineHelpdeskWidget.form.getElementsByClassName('attach_field')[0] + attach_field.attributes['data-value'] = e.target.result; + displayed_name = (attach_field.files[0].name.length <= 20) ? attach_field.files[0].name : attach_field.files[0].name.substring(0, 20) + '...'; + RedmineHelpdeskWidget.form.getElementsByClassName('attach_link')[0].innerHTML = displayed_name; + }); + } else { + this.attachment.attributes['data-value'] = ''; + RedmineHelpdeskWidget.form.getElementsByClassName('attach_link')[0].innerHTML = '<%= t(:label_helpdesk_widget_file_large) %>'; + } + }, + read_file: function(file, callback){ + var reader = new FileReader(); + reader.onload = callback + reader.readAsDataURL(file); + }, + load_custom_fields: function(target, project_id, tracker_id){ + var xmlhttp = getXmlHttp(); + var params = 'project_id=' + encodeURIComponent(project_id) + '&tracker_id=' + encodeURIComponent(tracker_id); + custom_div = document.createElement('div'); + xmlhttp.open('GET', this.base_url + '/helpdesk_widget/load_custom_fields?' + params, true); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4) { + if (xmlhttp.status == 200 || xmlhttp.status == 304) { + custom_div.innerHTML = xmlhttp.responseText; + target.appendChild(custom_div); + RedmineHelpdeskWidget.set_custom_values(); + RedmineHelpdeskWidget.positionate_iframe(); + } + } + }; + xmlhttp.send(null); + }, + set_custom_values: function(){ + if (this.configuration['identify'] && this.configuration['identify']['customFieldValues']){ + for(var cf in this.configuration['identify']['customFieldValues']) { + custom_field = this.form.querySelector('#issue_custom_field_values_' + this.schema.custom_fields[cf]) + if (custom_field){ + switch (custom_field.tagName){ + case 'INPUT': + custom_field.type = 'hidden'; + custom_field.value = this.configuration['identify']['customFieldValues'][cf]; + this.form.querySelector("[data-error-key='" + cf + "']").style.display = 'none'; + break; + case 'SELECT': + options = custom_field.options; + for(var option, index = 0; option = options[index]; index++) { + if(option.value == this.configuration['identify']['customFieldValues'][cf]) { + this.create_form_hidden(custom_field.parentElement, custom_field.id, custom_field.name, custom_field.classList.toString(), this.configuration['identify']['customFieldValues'][cf]); + custom_field.remove(); + this.form.querySelector("[data-error-key='" + cf + "']").style.display = 'none'; + break; + } + } + break; + } + } + } + } + }, + positionate_iframe: function(){ + widget_height = this.form.offsetHeight > this.height ? this.height : this.form.offsetHeight; + this.iframe.setAttribute('height', this.margin + widget_height); + switch (this.configuration['position']) { + case 'topLeft': + this.iframe.style.top = (this.margin + this.widget_button.offsetWidth) + 'px'; + iframe.style.left = (this.margin - this.widget_button.offsetWidth / 2) + 'px'; + break; + case 'topRight': + this.iframe.style.top = (this.margin + this.widget_button.offsetWidth) + 'px'; + iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px'; + break; + case 'bottomLeft': + this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px'; + iframe.style.left = (this.margin - this.widget_button.offsetWidth / 2) + 'px'; + break; + case 'bottomRight': + this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px'; + iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px'; + break; + default: + this.iframe.style.top = (- this.margin * 2 - widget_height) + 'px'; + iframe.style.left = (this.margin + this.widget_button.offsetWidth - this.width - 20) + 'px'; + } + }, + create_message_listener: function(){ + var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; + var eventer = window[eventMethod]; + var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; + + eventer(messageEvent,function(e) { + data = JSON.parse(e.data); + if (data['reload'] == true) { + RedmineHelpdeskWidget.reload = true; + } + if (data['project_reload'] == true) { + RedmineHelpdeskWidget.reload_project_data(); + } + },false); + }, + reload_form: function(){ + this.iframe.remove(); + this.create_iframe(); + this.fill_form(); + this.decorate_iframe(); + this.reload = false; + }, + show: function() { + this.iframe.style.visibility = 'visible'; + this.iframe.style.opacity = '1'; + this.positionate_iframe(); + switch (this.configuration['position']) { + case 'topLeft': + case 'topRight': + this.widget_button.style.borderRadius = '50% 50% 0%'; + break; + case 'bottomLeft': + case 'bottomRight': + this.widget_button.style.borderRadius = '0 100% 100%'; + break; + default: + this.widget_button.style.borderRadius = '0 100% 100%'; + } + this.widget_button.style.webkitTransform = 'rotate(45deg)'; + this.widget_button.style.mozTransform = 'rotate(45deg)'; + this.widget_button.style.msTransform = 'rotate(45deg)'; + this.widget_button.style.oTransform = 'rotate(45deg)'; + }, + hide: function() { + if (this.reload == true) { + this.reload_form(); + } + body = this.iframe.contentWindow.document.body; + this.iframe.style.visibility = 'hidden'; + this.iframe.style.opacity = '0'; + this.widget_button.style.borderRadius = '30px'; + this.widget_button.style.webkitTransform = ''; + this.widget_button.style.mozTransform = ''; + this.widget_button.style.msTransform = ''; + this.widget_button.style.oTransform = ''; + }, + toggle: function() { + (this.iframe.style.visibility == 'visible') ? this.hide() : this.show(); + } +} + +RedmineHelpdeskWidget.load(); diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_customer_to_email.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_customer_to_email.html.erb new file mode 100644 index 0000000..06e4a42 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_customer_to_email.html.erb @@ -0,0 +1,2 @@ +<%= contact_tag(contact, :id => "customer_send_tag") %> +(<%= contact_email %>) diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_customer_profile.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_customer_profile.html.erb new file mode 100644 index 0000000..b5e4588 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_customer_profile.html.erb @@ -0,0 +1,87 @@ +<% if !@issue.blank? && User.current.allowed_to?(:view_helpdesk_tickets, @project) %> + +
        +
        + <%= link_to l(:button_update), + {:controller => 'helpdesk_tickets', + :action => 'edit', + :issue_id => @issue}, + :remote => true if User.current.allowed_to?(:edit_helpdesk_tickets, @project) %> + +
        +

        <%= l(:label_helpdesk_contact) %>

        + <% unless !(@show_form == "true") %> + <%= form_for @helpdesk_ticket, :url => {:controller => 'helpdesk_tickets', + :action => 'update', + :issue_id => @issue}, + :html => {:id => 'ticket_data_form', + :method => :put} do |f| %> + + <% unless @helpdesk_ticket.new_record? %> +
        + <%= link_to image_tag('link_break.png'), + {:controller => 'helpdesk_tickets', :action => 'destroy', :id => @helpdesk_ticket}, + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :title => l(:label_relation_delete) %> +
        + <% end %> +

        <%= label_tag :helpdesk_ticket_contact_id, l(:label_helpdesk_contact)%>
        + <%= select_contact_tag('helpdesk_ticket[contact_id]', @helpdesk_ticket.customer, :is_select => Contact.visible.by_project(ContactsSetting.cross_project_contacts? ? nil : @project).count < 50, :include_blank => false, :add_contact => true, :display_field => @helpdesk_ticket.customer.blank?) %> +

        + +

        <%= label_tag :helpdesk_ticket_source, l(:label_helpdesk_ticket_source)%>
        + <%= f.select :source, helpdesk_tickets_source_for_select %>

        + +

        <%= f.text_field :ticket_date, :size => 12, :required => true, :value => @helpdesk_ticket.ticket_date.to_date, :label => l(:label_helpdesk_ticket_date) %> <%= f.text_field :ticket_time, :value => @helpdesk_ticket.ticket_date.to_s(:time), :size => 5 %><%= calendar_for('helpdesk_ticket_ticket_date') %>

        + +

        + <%= label_tag :helpdesk_ticket_cc_address, l(:label_helpdesk_cc_address) %>
        + <% @cc_address = @helpdesk_ticket.cc_address.try(:split, ',') || [] %> + <%= f.select :cc_address, options_for_select(@cc_address.map { |email|[email, email] }, @cc_address), {}, {:multiple => true } %> +
        +

        + + <%= submit_tag l(:button_update) %> + <%= link_to l(:button_cancel), {}, :onclick => "$('#ticket_data_form').hide(); return false;" %> + + <% end %> + + <% end %> + + <%= render :partial => 'contacts/contact_card', :object => @issue.customer if @issue.customer %> + +
        + + <% if @issue.customer && (customer_issues = @issue.customer.all_tickets.preload(:status, :tracker, :helpdesk_ticket).visible.order_by_status.to_a).count - 1 > 0 %> +
        +
        + <%= link_to l(:label_helpdesk_all) + " (#{customer_issues.count})", {:controller => 'issues', + :action => 'index', + :set_filter => 1, + :f => [:customer, :status_id], + :v => {:customer => [@issue.customer.id]}, + :op => {:customer => '=', :status_id => '*'}} %> +
        + +

        <%= l(:label_helpdesk_contact_activity) %>

        + +
          + <% (customer_issues.first(5)).each do |issue| %> +
        • " > + + + <%= link_to_issue(issue, :truncate => 60, :project => (@project != issue.project), :tracker => false) %> + + + <%= format_time(issue.created_on) %> + <%= "- #{issue.assigned_to.name}" if issue.assigned_to %> + +
        • + <% end %> +
        +
        + <% end %> + +
        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_reports.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_reports.html.erb new file mode 100644 index 0000000..7e0d70b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_helpdesk_reports.html.erb @@ -0,0 +1,6 @@ +<% if @project && @project.module_enabled?(:contacts_helpdesk)%> +

        <%= l(:label_helpdesk_reports) %>

        + <%= link_to l(:label_helpdesk_first_response_time), project_helpdesk_reports_path(@project, :report => 'first_response_time') %> +
        + <%= link_to l(:label_helpdesk_busiest_time_of_day), project_helpdesk_reports_path(@project, :report => 'busiest_time_of_day') %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_send_response.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_send_response.html.erb new file mode 100644 index 0000000..60e7403 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_send_response.html.erb @@ -0,0 +1,154 @@ +<% if authorize_for(:issues, :send_helpdesk_response) && @issue.customer && @issue.customer.primary_email %> + +<% canned_responses = CannedResponse.visible.in_project_or_public(@project) %> +<% if canned_responses.any? %> + +<% end %> + +<% unless HelpdeskSettings["helpdesk_emails_header", @project].blank? %> + +<% end %> + +<% unless HelpdeskSettings["helpdesk_emails_footer", @project].blank? %> + +<% end %> + +

        + <%= check_box_tag 'helpdesk[is_send_mail]', 1, HelpdeskSettings["send_note_by_default", @project], :onclick => "toggleSendMail(this);" %> + <%= label_tag :helpdesk_is_send_mail, l(:label_is_send_mail), :class => "icon icon-email-to", :style => "" %> + + +

        + + + +<% end %> + +<% content_for :header_tags do %> + <%= javascript_include_tag :redmine_helpdesk, :plugin => 'redmine_contacts_helpdesk' %> + <%= stylesheet_link_tag :helpdesk, :plugin => 'redmine_contacts_helpdesk' %> +<% end %> + +<% if authorize_for(:issues, :send_helpdesk_response) && @issue.customer && @issue.customer.primary_email %> + <%= javascript_tag do %> + $('#content .contextual:first a:first').before('<%= helpdesk_reply_link %>') + $('#content .contextual:last a:first').before('<%= helpdesk_reply_link %>') + <% end %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data.html.erb new file mode 100644 index 0000000..6f6189d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data.html.erb @@ -0,0 +1,56 @@ +<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) && @issue.is_ticket? %> +
        +
        + <%= link_to l(:label_helpdesk_public_link), public_ticket_path(@issue.helpdesk_ticket, :hash => @issue.helpdesk_ticket.token), :class => "icon icon-public-link" if RedmineHelpdesk.public_tickets? && !@issue.is_private %> + <%# link_to l(:label_helpdesk_spam), + {:controller => 'helpdesk', + :action => 'delete_spam', + :project_id => @project, + :issue_id => @issue}, + :method => :delete, + :data => {:confirm => l(:text_are_you_sure)}, + :class => "icon icon-email-spam" if @issue.helpdesk_ticket.source == HelpdeskTicket::HELPDESK_EMAIL_SOURCE && @issue.customer.primary_email && User.current.allowed_to?(:send_response, @project) && User.current.allowed_to?(:delete_issues, @project) && User.current.allowed_to?(:delete_contacts, @project) %> +
        + + <%= @issue.helpdesk_ticket.is_incoming? ? l(:label_helpdesk_from) : l(:label_sent_to) %> + + + <%= contact_tag(@issue.customer, :type => "plain") %> + (<%= @issue.helpdesk_ticket.from_address %>) + + <% if attachment = @issue.helpdesk_ticket.message_file %> + + <%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :download => true, :class => 'icon icon-attachment' -%> + <%= h(" - #{attachment.description}") unless attachment.description.blank? %> + (<%= number_to_human_size attachment.filesize %>) + <%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"), + :controller => 'helpdesk', :action => 'show_original', + :id => attachment, :project_id => @project %> + + <% end %> + <%= format_time(@issue.helpdesk_ticket.ticket_date) %> +
        + <% unless @issue.helpdesk_ticket.cc_address.blank? %> + <%= l(:label_helpdesk_cc) %>: <%= @issue.helpdesk_ticket.cc_address.split(',').join(', ') %> + <% end %> +
        +<% if Redmine::VERSION.to_s > '3.2' %> + +<% else %> +
        +<% end %> + +<%= issue_fields_rows do |rows| + rows.left l(:label_helpdesk_ticket_reaction_time), distance_of_time_in_words(@issue.helpdesk_ticket.reaction_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.reaction_time + rows.left l(:label_helpdesk_ticket_resolve_time), distance_of_time_in_words(@issue.helpdesk_ticket.resolve_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.resolve_time + rows.right l(:label_helpdesk_contact_vote), show_customer_vote(@issue.helpdesk_ticket.vote, @issue.helpdesk_ticket.vote_comment) if @issue.helpdesk_ticket.vote && @issue.helpdesk_ticket.vote >= 0 + rows.right l(:label_helpdesk_ticket_first_response_time), distance_of_time_in_words(@issue.helpdesk_ticket.first_response_time, 0, :include_seconds => true) if @issue.helpdesk_ticket.first_response_time + rows.right l(:label_helpdesk_ticket_last_response_time), distance_of_time_in_words(@issue.helpdesk_ticket.last_response_time, Time.now.utc) if @issue.helpdesk_ticket.last_response_time +end %> + +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data_form.html.erb b/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data_form.html.erb new file mode 100644 index 0000000..d9fbd8b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/issues/_ticket_data_form.html.erb @@ -0,0 +1,26 @@ +<% if @issue.new_record? && !@copy_from && User.current.allowed_to?(:edit_helpdesk_tickets, @project) && (@issue.tracker_id.to_s == HelpdeskSettings["helpdesk_tracker", @project.id] || HelpdeskSettings["helpdesk_tracker", @project.id] == 'all') %> + + +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/journals/_journal_contact.html.erb b/plugins/redmine_contacts_helpdesk/app/views/journals/_journal_contact.html.erb new file mode 100644 index 0000000..de3f3b3 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/journals/_journal_contact.html.erb @@ -0,0 +1,41 @@ +<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) && @issue.journal_messages && journal_message = @issue.journal_messages.detect{|j| j.journal_id == journal.id} %> +

        + + <%= journal_message.is_incoming? ? l(:label_received_from) : l(:label_sent_to) %> + + + <% if journal_message.is_incoming? %> + <%= "#{contact_tag(journal_message.contact)} (#{journal_message.from_address})".html_safe %> + <% else %> + <%= journal_message.contact.emails.include?(journal_message.to_address) ? "#{contact_tag(journal_message.contact)} (#{journal_message.to_address})".html_safe : journal_message.to_address %> + <% end %> + + + <% if attachment = journal_message.message_file %> + + <%= link_to_attachment attachment, :text => l(:label_helpdesk_original), :class => 'icon icon-attachment' -%> + <%= h(" - #{attachment.description}") unless attachment.description.blank? %> + (<%= number_to_human_size attachment.filesize %>) + <%= link_to_if_authorized image_tag('magnifier.png', :plugin => "redmine_contacts_helpdesk"), + :controller => 'helpdesk', :action => 'show_original', + :id => attachment, :project_id => @project %> + + <% end %> + <%= format_time(journal_message.message_date) %> + + <% unless journal_message.bcc_address.blank? && journal_message.cc_address.blank? %> +
        + + <%= "#{l(:label_helpdesk_cc)}: #{journal_message.cc_address}" unless journal_message.cc_address.blank? %><%= ", #{l(:label_helpdesk_bcc)}: #{journal_message.bcc_address}" unless journal_message.bcc_address.blank? %> + + <% end %> + + <% if false && journal_message.is_incoming? %> + + <%= link_to "Split", "", :class => "icon icon-split" %> + + <% end %> + +

        +<% end %> + diff --git a/plugins/redmine_contacts_helpdesk/app/views/layouts/public_tickets.html.erb b/plugins/redmine_contacts_helpdesk/app/views/layouts/public_tickets.html.erb new file mode 100644 index 0000000..4b0fe00 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/layouts/public_tickets.html.erb @@ -0,0 +1,51 @@ + + + + + +<%= csrf_meta_tag %> +<%= favicon %> +<%= stylesheet_link_tag 'jquery/jquery-ui-1.9.2', 'application', :media => 'all' %> +<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %> +<%= javascript_heads %> +<%= heads_for_theme %> +<%= call_hook :view_layouts_base_html_head %> + +<%= yield :header_tags -%> + + +
        +
        +
        + + + +
        + + + + + + +
        +
        +<%= call_hook :view_layouts_base_body_bottom %> + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/my/blocks/_my_helpdesk_tickets.html.erb b/plugins/redmine_contacts_helpdesk/app/views/my/blocks/_my_helpdesk_tickets.html.erb new file mode 100644 index 0000000..b175024 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/my/blocks/_my_helpdesk_tickets.html.erb @@ -0,0 +1,56 @@ +<% tickets_scope = Issue.visible.open.joins(:helpdesk_ticket).where(:assigned_to_id => User.current.id) %> + +<% tickets = tickets_scope.limit(10) %> + +

        <%= l(:my_helpdesk_tickets) %> (<%= tickets_scope.count %>)

        + +<% if tickets && tickets.any? %> +<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do %> + + + + + + + + + <% for ticket in tickets %> + + + + + + + <% end %> + +
        <%=l(:field_subject)%><%=l(:field_project)%><%=l(:label_helpdesk_contact)%><%=l(:label_helpdesk_last_message)%>
        + <%= check_box_tag("ids[]", ticket.id, false, :style => 'display:none;', :id => nil) %> + + <%= link_to "##{ticket.id} - #{truncate(ticket.subject, :length => 60)}", issue_path(ticket) %> (<%=h ticket.status %>) + <%= link_to_project(ticket.project) %><%= contact_tag(ticket.customer) + (ticket.customer_company.blank? ? "" : " (#{ticket.customer_company})") if ticket.customer %> + <%= (ticket.last_message.blank? ? ticket.description : ticket.last_message).truncate(250) %> +
        +<% end %> +<% else %> +

        <%= l(:label_no_data) %>

        +<% end %> + +<% if tickets.length > 0 %> +

        <%= link_to l(:label_helpdesk_view_all_tickets), :controller => 'issues', + :action => 'index', + :set_filter => 1, + :assigned_to_id => 'me', + :customer => "*", + :status_id => "o", + :c => ["project", "tracker", "status", "subject", "customer", "customer_company", "last_message"], + # :op => {:assigned_to_id => "=", :customer => "*", :status_id => "o"}, + + :sort => 'priority:desc,updated_on:desc' %>

        +<% end %> + +<% content_for :header_tags do %> +<%= auto_discovery_link_tag(:atom, + {:controller => 'issues', :action => 'index', :set_filter => 1, + :assigned_to_id => 'me', :format => 'atom', :key => User.current.rss_key}, + {:title => l(:label_assigned_to_me_issues)}) %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/_helpdesk_tickets.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/_helpdesk_tickets.html.erb new file mode 100644 index 0000000..0e20bc9 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/_helpdesk_tickets.html.erb @@ -0,0 +1,10 @@ +<% if User.current.allowed_to?(:view_helpdesk_tickets, @project) %> + <% if tickets = HelpdeskTicket.includes(:issue => [:project]).where(:projects => {:id => @project}) %> + <% customers = Contact.includes(:tickets => :project).where(:projects => {:id => @project}) %> +

        <%= l(:label_helpdesk_ticket_plural) %>

        +

        <%= l(:text_helpdesk_ticket_count, :count => tickets.count) %>

        +

        <%= l(:text_helpdesk_customer_count, :count => customers.count) %>

        +

        <%# link_to(l(:label_report), {:controller => "helpdesk_reports", :action => "tickets_report", :project_id => @project}) %>

        + <%= call_hook(:view_projects_show_helpdesk_sidebar_bottom, :project => @project) %> + <% end %> +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_canned_responses.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_canned_responses.html.erb new file mode 100644 index 0000000..1b84111 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_canned_responses.html.erb @@ -0,0 +1,43 @@ +<%= error_messages_for 'helpdesk_settings' %> + +<% if @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %> + +<% if canned_responses = CannedResponse.visible.in_project_or_public(@project).order("#{CannedResponse.table_name}.name") %> + + + + + + + + + +<% canned_responses.each do |canned_response| %> + + + + + + + +<% end %> + +
        <%= l(:field_name) %><%= l(:field_content) %><%= l(:field_is_public) %><%= l(:field_is_for_all) %>
        <%= canned_response.name %><%= canned_response.content.gsub(/$/, ' ').truncate(250) %><%= checked_image canned_response.is_public? %><%= checked_image canned_response.project.blank? %> + <% if User.current.allowed_to?(:manage_canned_responses, @project) %> + <%= link_to l(:button_edit), edit_canned_response_path(canned_response), :class => 'icon icon-edit' %> + <%= delete_link canned_response_path(canned_response, :project_id => @project) %> + <% end %> +
        +<% else %> +

        <%= l(:label_no_data) %>

        +<% end %> + +

        <%= link_to l(:label_helpdesk_new_canned_response), new_project_canned_response_path(@project), :class => 'icon icon-add' if User.current.allowed_to?(:manage_canned_responses, @project) %>

        + +<% else %> +

        <%= l(:label_helpdesk_enable_modules) %>

        +<% end %> + + + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_general.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_general.html.erb new file mode 100644 index 0000000..01450c7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_general.html.erb @@ -0,0 +1,58 @@ +
        + +

        + + <%= text_field_tag "helpdesk_answer_from", HelpdeskSettings["helpdesk_answer_from", @project.id], :size => "60", :placeholder => RedmineHelpdesk.settings["helpdesk_answer_from"] %> + <%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::FROM_MACRO_LIST.join(', ')) %> +

        + + +

        + + <%= select_tag "helpdesk_answered_status", ("" + options_for_select(IssueStatus.all.collect {|p| [p.name, p.id.to_s]}, HelpdeskSettings["helpdesk_answered_status", @project.id])).html_safe %> +

        + +

        + + <%= select_tag "helpdesk_reopen_status", ("" + options_for_select(IssueStatus.all.collect {|p| [p.name, p.id.to_s]}, HelpdeskSettings["helpdesk_reopen_status", @project.id])).html_safe %> +

        + +

        + + <%= select_tag "helpdesk_tracker", options_for_select([[l(:label_all), "all"]] + @project.trackers.collect {|t| [t.name, t.id.to_s]}, HelpdeskSettings["helpdesk_tracker", @project.id]), :include_blank => true %> +

        + +

        + + <%= select_tag "helpdesk_assigned_to", ("" + options_for_select(@project.assignable_users.collect {|t| [t.name, t.id.to_s]}, HelpdeskSettings["helpdesk_assigned_to", @project.id])).html_safe %> +

        + +

        + + <%= text_field_tag "helpdesk_lifetime", HelpdeskSettings["helpdesk_lifetime", @project.id], :size => "5" %> <%= l(:label_day_plural) %> +

        + +
        + +

        + + + <%= hidden_field_tag("helpdesk_is_not_create_contacts", 0) %> + <%= check_box_tag "helpdesk_is_not_create_contacts", 1, HelpdeskSettings["helpdesk_is_not_create_contacts", @project.id].to_i > 0, :onclick => '$("#add_tags").toggle();' %> +

        + +

        + + <%= text_area_tag "helpdesk_blacklist", HelpdeskSettings["helpdesk_blacklist", @project.id].blank? ? '' : HelpdeskSettings["helpdesk_blacklist", @project.id].split("\n").map{|u| u.strip}.join("\n"), :rows => 10 %> +
        <%= l(:text_custom_field_possible_values_info) %>

        + + + +
        0 %>> +

        + + <%= text_field_tag "helpdesk_created_contact_tag", HelpdeskSettings["helpdesk_created_contact_tag", @project.id], :size => 10, :class => 'hol' %><%= tagsedit_for('#helpdesk_created_contact_tag', Contact.available_tags(:project => @project).map(&:name).join("\',\'").html_safe ) %> +

        +
        + +
        diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_server.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_server.html.erb new file mode 100644 index 0000000..9d2f57c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_server.html.erb @@ -0,0 +1,184 @@ +
        <%= l(:label_helpdesk_incoming_mail_server) %> + +

        + + <%= select_tag :helpdesk_protocol, options_for_select([['', ""], ["pop3", "pop3"], ["imap", "imap"], ["Gmail", "gmail"], ["Yahoo", "yahoo"], ["Yandex", "yandex"]] , HelpdeskSettings[:helpdesk_protocol, @project.id]), :onchange => "changeServerSettings(this)" %> +

        + + > + + > +

        + + <%= text_field_tag :helpdesk_host, HelpdeskSettings[:helpdesk_host, @project.id] %> +

        + +

        + + <%= text_field_tag :helpdesk_port, HelpdeskSettings[:helpdesk_port, @project.id] %> +

        +
        + +

        + + <%= text_field_tag :helpdesk_username, HelpdeskSettings[:helpdesk_username, @project.id] %> +

        + +

        + + <%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#helpdesk_password_field").show()' unless HelpdeskSettings[:helpdesk_username, @project.id].blank? %> + <%= content_tag 'span', :id => "helpdesk_password_field", :style => (HelpdeskSettings[:helpdesk_username, @project.id].blank? ? nil : 'display:none') do %> + <%= password_field_tag :helpdesk_password, '' %> + <% end %> +

        +

        id="helpdesk_use_ssl_field"> + + + <%= hidden_field_tag(:helpdesk_use_ssl, 0) %> + <%= check_box_tag :helpdesk_use_ssl, 1, HelpdeskSettings[:helpdesk_use_ssl, @project.id].to_i > 0 %> +

        + + > + + +

        + + <%= text_field_tag :helpdesk_imap_folder, HelpdeskSettings[:helpdesk_imap_folder, @project.id] %> +

        + +

        + + <%= text_field_tag :helpdesk_move_on_success, HelpdeskSettings[:helpdesk_move_on_success, @project.id] %> +

        + +

        + + <%= text_field_tag :helpdesk_move_on_failure, HelpdeskSettings[:helpdesk_move_on_failure, @project.id] %> +

        +
        + + > +

        + + + <%= hidden_field_tag(:helpdesk_apop, 0) %> + <%= check_box_tag :helpdesk_apop, 1, HelpdeskSettings[:helpdesk_apop, @project.id].to_i > 0 %> +

        + +

        + + + <%= hidden_field_tag(:helpdesk_delete_unprocessed, 0) %> + <%= check_box_tag :helpdesk_delete_unprocessed, 1, HelpdeskSettings[:helpdesk_delete_unprocessed, @project.id].to_i > 0 %> +

        +
        + +
        +
        + + <%= link_to l(:label_helpdesk_get_mail), + {}, + :remote => true, + :onclick => "updateCustomForm('#{url_for(:controller => 'helpdesk', :action => 'get_mail', :project_id => @project)}', $('#helpdesk_settings'))" %> + + +
        + +
        + +
        + +
        <%= l(:label_helpdesk_outgoing_mail_server) %> (experimental) + +

        + + <%= hidden_field_tag(:helpdesk_smtp_use_default_settings, 1) %> + <%= check_box_tag :helpdesk_smtp_use_default_settings, 0, HelpdeskSettings[:helpdesk_smtp_use_default_settings, @project.id].to_i == 0, :onchange => "$('.smtp-settings').toggle(); return false;" %> +

        + + 0 %>> + +

        + + <%= text_field_tag :helpdesk_smtp_server, HelpdeskSettings[:helpdesk_smtp_server, @project.id] %> +

        + +

        + + <%= text_field_tag :helpdesk_smtp_port, HelpdeskSettings[:helpdesk_smtp_port, @project.id] %> +

        + +

        + + <%= text_field_tag :helpdesk_smtp_domain, HelpdeskSettings[:helpdesk_smtp_domain, @project.id] %> +

        + +

        + + <%= select_tag :helpdesk_smtp_authentication, options_for_select([[l(:label_helpdesk_authentication_plain), "plain"], [l(:label_helpdesk_authentication_login), "login"], [l(:label_helpdesk_authentication_cram_md5), "cram_md5"]] , HelpdeskSettings[:helpdesk_smtp_authentication, @project.id]) %> +

        + +

        + + <%= text_field_tag :helpdesk_smtp_username, HelpdeskSettings[:helpdesk_smtp_username, @project.id] %> +

        + + +

        + + <%= link_to_function image_tag('edit.png'), '$(this).hide(); $("#helpdesk_smtp_password_field").show()' unless HelpdeskSettings[:helpdesk_smtp_username, @project.id].blank? %> + <%= content_tag 'span', :id => "helpdesk_smtp_password_field", :style => (HelpdeskSettings[:helpdesk_smtp_username, @project.id].blank? ? nil : 'display:none') do %> + <%= password_field_tag :helpdesk_smtp_password, '' %> + <% end %> +

        + +

        + + <%= hidden_field_tag(:helpdesk_smtp_ssl, 0) %> + <%= check_box_tag :helpdesk_smtp_ssl, 1, HelpdeskSettings[:helpdesk_smtp_ssl, @project.id].to_i > 0 %> +

        + +

        + + <%= hidden_field_tag(:helpdesk_smtp_tls, 0) %> + <%= check_box_tag :helpdesk_smtp_tls, 1, HelpdeskSettings[:helpdesk_smtp_tls, @project.id].to_i > 0 %> +

        + +
        + +
        + + + + diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_settings.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_settings.html.erb new file mode 100644 index 0000000..561d56a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_settings.html.erb @@ -0,0 +1,28 @@ +<%= error_messages_for 'helpdesk_settings' %> + +<% errors = [] %> +<% errors << l(:label_helpdesk_enable_modules) unless @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %> + +<% if errors.empty? %> + <%= form_tag({:controller => :helpdesk, :action => :save_settings, :project_id => @project, :tab => 'helpdesk'}, :method => :put, :class => "tabular", :multipart => true, :id => 'helpdesk_settings') do %> + +
        +

        <%=l(:label_helpdesk)%>

        + <%= render :partial => 'projects/settings/helpdesk_general' %> +
        + +
        +

        <%=l(:label_helpdesk_server_settings)%>

        + <%= render :partial => 'projects/settings/helpdesk_server' %> +
        + +
        + + <%= submit_tag l(:button_save) %> + + <% end %> + +<% else %> +

        <%= errors.join("
        ").html_safe %>

        +<% end %> + diff --git a/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_template.html.erb b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_template.html.erb new file mode 100644 index 0000000..bfe2bfe --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/projects/settings/_helpdesk_template.html.erb @@ -0,0 +1,64 @@ +<%= error_messages_for 'helpdesk_settings' %> + +<% if @project.module_enabled?(:contacts) && @project.module_enabled?(:issue_tracking) %> + <%= form_tag({:controller => :helpdesk, :action => :save_settings, :project_id => @project, :tab => 'helpdesk_template'}, :method => :put, :class => "tabular", :multipart => true, :id => 'helpdesk_template') do %> + +
        <%= l(:label_helpdesk_answer_template) %> + +

        + + <%= text_field_tag "helpdesk_answer_subject", HelpdeskSettings["helpdesk_answer_subject", @project.id], :style => "width:100%" %> +

        + +

        + + <%= text_area_tag "helpdesk_emails_header", HelpdeskSettings["helpdesk_emails_header", @project.id], :class => 'wiki-edit', :rows => 5 %> +

        + +

        + + <%= text_area_tag "helpdesk_emails_footer", HelpdeskSettings["helpdesk_emails_footer", @project.id], :class => 'wiki-edit', :rows => 5 %> +

        +
        + + +
        <%= l(:label_helpdesk_auto_answer_template) %> +

        + + + <%= hidden_field_tag("helpdesk_send_notification", 0) %> + <%= check_box_tag "helpdesk_send_notification", 1, ContactsSetting["helpdesk_send_notification", @project.id].to_i > 0 %> +

        + +

        + + <%= text_field_tag "helpdesk_first_answer_subject", HelpdeskSettings["helpdesk_first_answer_subject", @project.id], :style => "width:100%" %> +

        + +

        + + <%= text_area_tag "helpdesk_first_answer_template", HelpdeskSettings["helpdesk_first_answer_template", @project.id], :class => 'wiki-edit', :rows => 15 %> + <%= wikitoolbar_for 'helpdesk_first_answer_template' %> +

        + +
        + +
        <%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.join(', ')) %>
        + +
        + + <%= submit_tag l(:button_save) %> + + <% end %> + +<% else %> +

        <%= l(:label_helpdesk_enable_modules) %>

        +<% end %> + + +<% content_for :header_tags do %> + <%= javascript_include_tag :"tag-it", :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :"jquery.tagit.css", :plugin => 'redmine_contacts' %> + <%= javascript_include_tag :contacts, :plugin => 'redmine_contacts' %> + <%= stylesheet_link_tag :contacts, :plugin => 'redmine_contacts' %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_add_comment.html.erb b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_add_comment.html.erb new file mode 100644 index 0000000..8d24ae7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_add_comment.html.erb @@ -0,0 +1,8 @@ +<%= labelled_form_for @journal, :url => public_ticket_add_comment_path(:id => @ticket.id, :hash => @ticket.token) , :html => {:id => 'add_comment_form', :multipart => true} do |f| %> + <%= error_messages_for 'journal', 'journal_message' %> +
        + <%= f.text_area 'notes', :cols => 60, :rows => 10, :class => 'wiki-edit' %> + <%= wikitoolbar_for 'journal_notes' %> +
        + <%= submit_tag l(:button_submit) %> +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_attachment_links.html.erb b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_attachment_links.html.erb new file mode 100644 index 0000000..61aed61 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_attachment_links.html.erb @@ -0,0 +1,13 @@ +
        + <% for attachment in attachments %> +

        <%= link_to_attachment_with_hash attachment, :class => 'icon icon-attachment', :download => true -%> + <% if attachment.is_text? %> + <%= link_to image_tag('magnifier.png'), + :controller => 'attachments', :action => 'show', + :id => attachment, :filename => attachment.filename %> + <% end %> + <%= h(" - #{attachment.description}") unless attachment.description.blank? %> + (<%= number_to_human_size attachment.filesize %>) +

        + <% end %> +
        \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_history.html.erb b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_history.html.erb new file mode 100644 index 0000000..16b83bb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_history.html.erb @@ -0,0 +1,11 @@ +<% for journal in journals %> +
        +
        +

        <%= link_to "##{journal.indice}", {:anchor => "note-#{journal.indice}"}, :class => "journal-link" %> + <%= authoring_public journal, :label => :label_updated_time_by %>

        +
        + <%= textilizable(journal, :notes) unless journal.notes.blank? %> +
        +
        +
        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_sidebar_content.html.erb b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_sidebar_content.html.erb new file mode 100644 index 0000000..9f84819 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/_sidebar_content.html.erb @@ -0,0 +1,19 @@ +<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %> + +<% if (RedmineHelpdesk.public_spent_time? && @total_spent_hours.to_i > 0) || !@previous_tickets.blank? %> + <% content_for :sidebar do %> + <% if RedmineHelpdesk.public_spent_time? && @total_spent_hours.to_i > 0 %> +

        <%= l(:label_spent_time) %>

        +

        <%= l_hours(@total_spent_hours) %>

        + <% end %> + + <% unless @previous_tickets.empty? %> +

        + <%= l(:label_helpdesk_previous_tickets) %> +

        + <% @previous_tickets.each do |previous_ticket| %> +

        <%= link_to "##{previous_ticket.id} - #{previous_ticket.subject} (#{previous_ticket.status.name})", public_ticket_path(previous_ticket.helpdesk_ticket, :hash => previous_ticket.helpdesk_ticket.token), :class => previous_ticket.css_classes %>

        + <% end %> + <% end %> + <% end %> +<% end %> \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/public_tickets/show.html.erb b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/show.html.erb new file mode 100644 index 0000000..315c9fd --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/public_tickets/show.html.erb @@ -0,0 +1,79 @@ +

        <%= issue_heading(@issue) %>

        + +
        + <%= avatar(@ticket.from_address, :size => "50") %> + +
        +

        <%= @issue.subject %>

        +
        +

        + <%= l(:label_added_time_by, :author => mail_to(@ticket.from_address), :age => content_tag('acronym', distance_of_time_in_words(Time.now, @issue.created_on), :title => format_time(@issue.created_on))).html_safe %>. + <%# authoring @issue.created_on, mail_to(@ticket.from_address) %> + <% if @issue.created_on != @issue.updated_on %> + <%= l(:label_updated_time, ticket_time_tag(@issue.updated_on)).html_safe %>. + <% end %> +

        + +<<%= Redmine::VERSION.to_s > '3.2' ? 'div' : 'table' %> class="attributes"> +<%= issue_fields_rows do |rows| + rows.left l(:field_status), h(@issue.status.name), :class => 'status' + + unless @issue.disabled_core_fields.include?('assigned_to_id') + rows.left l(:field_assigned_to), (@issue.assigned_to ? @issue.assigned_to.name : "-"), :class => 'assigned-to' + end + unless @issue.disabled_core_fields.include?('done_ratio') + rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :width => '80px', :legend => "#{@issue.done_ratio}%"), :class => 'progress' + end + if RedmineHelpdesk.public_spent_time? + unless @issue.disabled_core_fields.include?('estimated_hours') + if RedmineHelpdesk.public_spent_time? && !@issue.estimated_hours.blank? + rows.right l(:field_estimated_hours), l_hours(@issue.estimated_hours), :class => 'estimated-hours' + end + end + rows.right l(:label_spent_time), (@issue.total_spent_hours > 0 ? l_hours(@issue.total_spent_hours) : "-"), :class => 'spent-time' + end +end %> +<%# render_custom_fields_rows(@issue) %> + '3.2' ? 'div' : 'table' %>> + +<% if @issue.description? || @issue.attachments.any? -%> +
        +<% if @issue.description? %> + +

        <%=l(:field_description)%>

        +
        + <%= textilizable @issue, :description, :attachments => @issue.attachments %> +
        +<% end %> +<% if @issue.attachments.any? %> + +<% end %> +<% end -%> + + +
        + +<% if @journals.present? %> +
        +

        <%=l(:label_history)%>

        +<%= render :partial => 'public_tickets/history', :locals => { :issue => @issue, :journals => @journals } %> +
        +<% end %> + +
        + +<% if RedmineHelpdesk.public_comments? %> +

        <%= toggle_link l(:label_comment_add), "update", :focus => "journal_notes" %>

        + +<% end %> + +<%= render :partial => 'sidebar_content' %> + diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk.html.erb new file mode 100644 index 0000000..fb1581e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk.html.erb @@ -0,0 +1,18 @@ +<% helpdesk_tabs = [ + {:name => 'general', :partial => 'settings/helpdesk_general', :label => :label_helpdesk_general}, + {:name => 'public', :partial => 'settings/helpdesk_public', :label => :label_helpdesk_settings_public}, + {:name => 'templates', :partial => 'settings/helpdesk_template', :label => :label_helpdesk_template}, + {:name => 'votes', :partial => 'settings/helpdesk_vote', :label => :label_helpdesk_vote}, + {:name => 'canned_responses', :partial => 'settings/helpdesk_canned_responses', :label => :label_helpdesk_canned_response_plural}, + {:name => 'widget', :partial => 'settings/helpdesk_widget', :label => :label_helpdesk_widget} + ] %> + +<% helpdesk_tabs.push({:name => 'hidden', :partial => 'settings/helpdesk_hidden', :label => :label_crm_contacts_hidden}) if params[:hidden] %> + +<%= render_tabs helpdesk_tabs %> + +<% html_title(l(:label_settings), l(:label_helpdesk)) -%> + +<% content_for(:header_tags) do %> + <%= javascript_include_tag :redmine_helpdesk, :plugin => 'redmine_contacts_helpdesk' %> +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_canned_responses.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_canned_responses.html.erb new file mode 100644 index 0000000..b41bd16 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_canned_responses.html.erb @@ -0,0 +1,2 @@ +<% @canned_responses = CannedResponse.all %> +<%= render :partial => 'canned_responses/index' %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_general.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_general.html.erb new file mode 100644 index 0000000..f207ef4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_general.html.erb @@ -0,0 +1,50 @@ +

        + + <%= text_field_tag 'settings[helpdesk_answer_from]', @settings["helpdesk_answer_from"], :size => "98%" %> + <%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::FROM_MACRO_LIST.join(', ')) %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_save_cc]', 1, @settings["helpdesk_save_cc"] %> +

        + +

        + + <%= check_box_tag 'settings[send_note_by_default]', 1, @settings["send_note_by_default"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_add_contact_notes]', 1, @settings["helpdesk_add_contact_notes"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_assign_contact_user]', 1, @settings["helpdesk_assign_contact_user"], :class => 'assign_contact_user' %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_create_private_tickets]', 1, @settings["helpdesk_create_private_tickets"], :disabled => true, :class => 'private_tikets' %> +

        + +

        + + <%= select_tag 'settings[helpdesk_autoclose_tickets_after]', options_for_select((1..24).map{ |h| [h, h] }, @settings["helpdesk_autoclose_tickets_after"]), + :onchange => "toggleStatusesForAutoclose(this); return false", :include_blank => true %> + <%= radio_button_tag 'settings[helpdesk_autoclose_tickets_time_unit]', 'day', RedmineHelpdesk.autoclose_time_unit_is?('day') || RedmineHelpdesk.autoclose_time_unit.nil? %> + <%= l(:label_helpdesk_days) %> + <%= radio_button_tag 'settings[helpdesk_autoclose_tickets_time_unit]', 'hour', RedmineHelpdesk.autoclose_time_unit_is?('hour') %> + <%= l(:label_helpdesk_hours) %> +

        +
        "> +

        + + <%= select_tag 'settings[helpdesk_autoclose_from_status]', options_for_select(IssueStatus.all.map{|st| [st.name, st.id]}, @settings["helpdesk_autoclose_from_status"]), :style => "width:150px" %> +

        +

        + + <%= select_tag 'settings[helpdesk_autoclose_to_status]', options_for_select(IssueStatus.all.map{|st| [st.name, st.id]}, @settings["helpdesk_autoclose_to_status"]), :style => "width:150px" %> +

        +
        diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_hidden.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_hidden.html.erb new file mode 100644 index 0000000..f2233d2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_hidden.html.erb @@ -0,0 +1,9 @@ +

        + + <%= check_box_tag 'settings[plain_text_mail]', 1, @settings["plain_text_mail"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_do_not_strip_tags]', 1, @settings["helpdesk_do_not_strip_tags"] %> +

        diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_public.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_public.html.erb new file mode 100644 index 0000000..f8a82ae --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_public.html.erb @@ -0,0 +1,20 @@ +

        + + <%= check_box_tag 'settings[helpdesk_public_tickets]', 1, @settings["helpdesk_public_tickets"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_public_show_spent_time]', 1, @settings["helpdesk_public_show_spent_time"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_public_comments]', 1, @settings["helpdesk_public_comments"] %> +

        + +

        + + <%= text_field_tag 'settings[helpdesk_public_title]', @settings["helpdesk_public_title"], :size => "98%" %> +

        + diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_template.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_template.html.erb new file mode 100644 index 0000000..be2bacb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_template.html.erb @@ -0,0 +1,45 @@ +
        <%= l(:label_helpdesk_answer_template) %> + +

        + + <%= text_field_tag 'settings[helpdesk_answer_subject]', @settings["helpdesk_answer_subject"], :style => "width:100%" %> +

        + + +

        + + <%= text_area_tag 'settings[helpdesk_emails_header]', @settings["helpdesk_emails_header"], :class => 'wiki-edit', :rows => 5 %> +

        + +

        + + <%= text_area_tag 'settings[helpdesk_emails_footer]', @settings["helpdesk_emails_footer"], :class => 'wiki-edit', :rows => 5 %> +

        + +
        + +
        <%= l(:label_helpdesk_auto_answer_template) %> + +

        + + <%= text_field_tag 'settings[helpdesk_first_answer_subject]', @settings["helpdesk_first_answer_subject"], :style => "width:100%" %> +

        + + <%= text_area_tag 'settings[helpdesk_first_answer_template]', @settings["helpdesk_first_answer_template"], :class => 'wiki-edit', :rows => 15 %> + +
        + +
        <%= l(:label_helpdesk_css) %> + <%= text_area_tag 'settings[helpdesk_helpdesk_css]', @settings["helpdesk_helpdesk_css"], :class => 'wiki-edit', :rows => 10 %> +
        + +<%= l(:text_helpdesk_answer_macros, :macro => HelpdeskSettings::MACRO_LIST.join(', ')) %> + +<% if params[:show_hidden] %> +
        +

        + + <%= check_box_tag 'settings[show_excerpt_tickets_list]', 1, @settings["show_excerpt_tickets_list"] %> +

        +
        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_vote.html.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_vote.html.erb new file mode 100644 index 0000000..024fca4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_vote.html.erb @@ -0,0 +1,9 @@ +

        + + <%= check_box_tag 'settings[helpdesk_vote_accept]', 1, @settings["helpdesk_vote_accept"] %> +

        + +

        + + <%= check_box_tag 'settings[helpdesk_vote_comment_accept]', 1, @settings["helpdesk_vote_comment_accept"] %> +

        \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_widget.erb b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_widget.erb new file mode 100644 index 0000000..7f128e7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/app/views/settings/_helpdesk_widget.erb @@ -0,0 +1,44 @@ +

        + + <%= check_box_tag 'settings[helpdesk_widget_enable]', 1, @settings["helpdesk_widget_enable"] %> +

        + +<% if @settings["helpdesk_widget_enable"].to_i > 0 %> +

        + + <% @helpdesk_projects = Project.visible.has_module('contacts_helpdesk') %> + <% if @helpdesk_projects.count > 0 %> + <% @helpdesk_projects.each do |project| %> + <%= check_box_tag 'settings[helpdesk_widget_available_projects][]', project.id, @settings["helpdesk_widget_available_projects"].try(:include?, project.id.to_s) %> + <%= project.name %> +
        + <% end %> + <% else %> + <%= l(:label_helpdesk_widget_no_available_projects) %> + <% end %> +

        + +

        + + <% IssueCustomField.visible.each do |cf| %> + <%= check_box_tag 'settings[helpdesk_widget_available_custom_fields][]', cf.id, @settings["helpdesk_widget_available_custom_fields"].try(:include?, cf.id.to_s) %> + <%= cf.name %> +
        + <% end %> +

        + +

        <%= l(:label_helpdesk_widget_activation_message) %>

        +
        +      
        +      <%= Redmine::SyntaxHighlighting.highlight_by_language(
        +      "
        +        
        + +
        ", "html").html_safe %> +
        +
        + +
        + +
        +<% end %> diff --git a/plugins/redmine_contacts_helpdesk/assets/images/arrow_divide.png b/plugins/redmine_contacts_helpdesk/assets/images/arrow_divide.png new file mode 100755 index 0000000000000000000000000000000000000000..61a7b1d995e4b97d08c784aaefc41bda7086b7e0 GIT binary patch literal 677 zcmV;W0$TlvP)O2Im`9qM4R5P3BW3krlN(mI5Z z(KV^C5HyzvLrS5djW#mEchkd0<@IzkP&T zje~)eD;&7qVC!q^K`R)J4I}kERX%bq_9+7Zgt9nQ1hjexG(6abiLr9H+N>B-7 z2>SBph(duFLM%ZMg!96nVfPZi`0RE{To_%f(_e%DKo9^(nOQLYoR{*7@^cGNURr@4 zzkZ(Ce|2}YP~a51^_3ecKqY3A9#W~)94RvZ5Lhx&%iTlGqzqJ)REFGQ7YYk+-h&8H zSWpD2LHy$fOifSwLOD8d@zwpm007W@bKhnf=_J{@wR~IV`r=Zk3O;}Rf{Doq5z1{E zIsfv~>-Wn(Sk20NF7Mj0E;nzqrn&}V6|v_rc**3>7Z)bS{%hr>c-THN=6y>p_+sGG zVAk^%RuiWM004bvt5qlzG?JaJFPIi!hzCPM5F-#u+HVZ3^__nMQKTs* zG$KrcK-C)nA;634?;v7tbkbr{XgS>*bPoiN+goL~D2Zi5Gq&{N*<5-4BFbwYC~uS^ z)f6=iI!z)y@dmHPyIt)*ZZ8lhrpBVpZ*TGXeF^V|^W>r3+_)dmMw%vK#jwbB!A!Qb z55`NSqVM_A6nBhuT|g`t!g?-?_0Mm3uA{5p=k^cAx4(}kbz&K&Db-?_NVwz^eUiiF zGe{NBt~}&Wg&OjWGLOw1t1}+sGaWOv=!P&Pd%FUrxebStuF`P~Hy_QwBAFBH)iAlgS_w zi5!Ueoiy%D-2>w@9p8do)6=DLQLP7#;B9^lp->3naQL5*)ftcRnT{JcE6T*?*K|G_ z>A=g>6qZ(AK~a-`%;V7-;Fi4a;Th7HjyH zRWPJ0e&I39dJPtv z1ER-^KsbhcZmS?J&aPVZ`Y5p&>YrAoU7L~=bwqkSnJk2&y^fx~VC&JCY`06Iu4_iI z_(Lts&E#C!G=B>XQB0A)-U5F2_OLUXP9U2e+THgW>4m0g53xP7-&Xf&=;DOnf5tBX Z1^{NCUF->RpQZo+002ovPDHLkV1lfxdDQ>_ literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/bullet_down.png b/plugins/redmine_contacts_helpdesk/assets/images/bullet_down.png new file mode 100644 index 0000000000000000000000000000000000000000..286073d64fa37712c016333b93fdf33013c66ca0 GIT binary patch literal 438 zcmV;n0ZIOeP)0#Ou&&vRzVH%;YGNg)R1E~KcW*$DXu#GlYF=pVFc5fm-kv9 zhvFQI?h_jt(9wX@(J?fA0-y+*hbL;J7d#CB1^9Tl1(<@A-O)1WX@FD`Bz-6uAb=Py zgV*>H3G$HH+=XFG!V`TkreJPwMV=SzuE99s&6lar3(nRVSK<@1jsrViK+mm12_HST z4m)3f(t_V38MiFel8)e2hWBBF^TFvAf|B+)7t(XLiN@V@~-M8D>86&tk} zsF6#PcX~jLT$*Yz$a@R?r#BQpM7T&HQ?fyBc9}{x g(R-MO{r@t30aH-2jZp#{R(K#S-pLRv<&Xc;YqUZG_n zw5v_fA_;<8go_X}x_CMqPky~Xn&PV24*VEq{-Y_S&`J?ntd;?;*Np9v$8yw*X(OKB zS2s;fFn`$5uCpvnhmt}v&b(y8~PL2%ESy^ z80M&dhGxUdD178G-k(4!vf3*N*`Bl(stZVBGwPebRB#EeP=@!~+Q|Mp8b5 z#ADZvX4G7wWNAJX?@)0nZ6p3Ul=h&KgD(a(iEgnJWyXXfuqeUHvnvB#=c|z zBr&-lG{QqMr*3zv?uM@*nITBPMGPhZon>=DlEe@iVW4vmOahW*b3r1Kbe=nb<=C(s h3%dLr(Ek53{Qw2VSM!1RC^G;6002ovPDHLkV1jz>uw(!L literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/email_error.png b/plugins/redmine_contacts_helpdesk/assets/images/email_error.png new file mode 100644 index 0000000000000000000000000000000000000000..8bdd3304de57761ba769f304df43ef97b4e6cc98 GIT binary patch literal 792 zcmV+z1LypSP)gn-s{Jsef@w|XbT!@L zKqWCIT8+A(7Sx3z9}!4Pu(d7F()NAMIIl$mh8>gK+?nLgnRCyXOBiF=&l0fDCEXmG zO1fK=fkMUbY^kO|IUx!Li*G1rXYru4dEO2NVi`tlvyl0*xG05#{;n&-JwDA~#O)Gy zp_Bjjq?zlQDb_VBdhw2?3i+}b*G)Mkej}RiH|=KYV#o;^MY)fCsb9d zsU|OsN4*FKTk!a82BnG)znj1#sVFUsLkabxPk!I1k)<6rR_jd$gsWp3CfpLm!B_y3 zsXVd;6|1uoP{IT7MFtSAy@Rth*I@QnS)ry`u-WLkiCn3Iu66|{;z1la6h!*_9BM0* z{Ph5=@-syHFF{yNfEpSa|Bn$Hz+|?+`69m@R^azIIYol*12T?FuMz1z52!OpJxahE zsKZrEL0x&ntu#0QWkR*Ix~y>@k9Eo{8Whm#vABR1lv)~Yvjinr1J#!7Vpv?v?xE{w z`;|a0UsB7zvrDA!!ri2^HGa~ics!CzBobHC!F9M6c_^MfJ51b;DNsGT-LnItXArps z0g2@NX5wr7GudmHX6_GN4fPJ+>*$gBt0~^Ej_Ief7#!&Z5&eLC4(SIs@{H0mdjh0J zPi6ZrCt{+o3}&v|YVDdoQ9g>?w=bA~e*1~E*Mymx?w%PNsdJ5Z>vTu3e;IaWtm6;T WSyNB7TX+uL$Nkc;* zP;zf(X>4Tx0C)kNmUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@${RdIBvPG*FL9}R9 z5mW>b6@)Ep(Kpt@m6i748=M;7jE>JY^FHtG+&2bBxbnd7zH>i*=lsrn=Q1fJwqtxd zWX07k-F!dYp2{4s>|n>b8*)K{1SBGI1gVP%f_ydKfCP(zpuTFEa|`oYcc)%BdtyZQ z0?FFjTK*BlHqBz62K8-tVN1h{`c7SW;_`AWGW<}myVE6@mJi31_*oG!s{+MUfl^JN zY)e>S? z_o+k_qWhL%b|+nK@A z@){c1q~b^oAq7z6%2x_N3PeCk2%ZfTjsig)L)R1VF1XDmpqzubjQH`(_Scq+OB8@M zlyrS}Q-KOrTLjX*fcYBU{CAv8O^592+qi7%eHn(nU$3oHtOawCi5o)^d{RNBNK(V| zV;shDVDSy+K2Ob#PRjA-jcqZi*SHzgNwzF+9h0*cF|&DnEC2u3=+h;Bd5Qg(<-sj- b{~0d>kmp9&fG9>#5EaEn(MD9Xvl47<{Rb2T3D!0$wpy4@P!Tnn zB0fxQ2!aPL_fZ@!=Wv9zmj8jnf5JO`<=MmZs@#@p z5O*{}9L5B6J+L7ORza<+9h)-B~<&cwIhUkd?cp4is-ziNrfY^u-7xdbFt=#%1534Oi8ajBBo}VZvCxd1 zTu*VtX+~P45)Om?dG5aS`PPW(%?lDPBwaz$6C^$8$_Q#plJTjpbgj;_rYi*?oK~m+ zDD`C->dkZKh0j6|Q^-0bJxIFHnHJOo$@r8{VNIUWU>31rp3@BVvYIZG2D0>Yuj2Y- zg?b?AObc>pK`oG+zkPsr3x_BTWXX8H*w}0@e;@>?SdsP&!-HMidhRn*4VkHi+<58} zTg6ae0~aSoNqRq(^;(g}0#Yiz)&q*YIg-f)mmeJA&3%nD3aJ$X-6L%1+fEQRIeTL* zS5K6|3cOaNtTtBowSZzzj!ZfM7L<|f+jtyp4ccf7mf6J$n}&ALkZlfLEYRoNvtG0Q zzJh|U~_Rdjkv}0kGt_?V}5$?sXN`N zTt|nOa)sx)IIf_-7XW**;!X9`USPjuUUsJrREk)6q*M9ZHEb zl2A!T3LBjrrTx74{FsUNcVA1zw%IWoiGNMw=s9P8>+qh!B*saR2}S literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/just_ok.png b/plugins/redmine_contacts_helpdesk/assets/images/just_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..16555c1f3143d7fc5aa499534d76f16bc68407d5 GIT binary patch literal 766 zcmVU)X z!O9|k#Fjt6T_J)VqKpV3dXIV#0{?(see!PzdZ`DY7as))!k|D9Ba2egTGud3Tir5t zox9uY?A*?|w&p`m4SXEVx!-r@{J2;9JPv3YU>M3;x{g5rLxc`OoKPTS2~$CUX(=Im zA0Ugi+Y%%@__`=KX4nJ$N7`>VM(mWOoApNE6@7nieZXCty!0#g>J9Nm37MLxmY^%p zq7NE-@AO$t%0@sD`U5-{x>}RJJW;Xk3+EAU((^m>kDpOH!|lUT7}Y8UBZ*|nip6cew1Z@- z7tq6qMX8MPrn1R_6cAKkMF_EkZ8j%U?za`5)udjjU?_f~r$rVvsN^DAFHhj+#8t@h zHl)~4mVJ)%IrEs$IYQc3eUHG{D+HP^0PPZSowNMPXd=V?lZx60$^6VKnHYEupKCOMt zuMs~K!n8^t1O&A0v;2l;*bWKRIB;_@3#O00Gl*OCO9TmS^xk507*qoM6N<$g1&fYZ2$lO literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/magnifier.png b/plugins/redmine_contacts_helpdesk/assets/images/magnifier.png new file mode 100644 index 0000000000000000000000000000000000000000..cf3d97f75e9cde9c143980d89272fe61fc2d64ee GIT binary patch literal 615 zcmV-t0+{`YP)gNuvOO$0ks zMIj=HnnBRUR?tKXG11rxCU4&7dG4NbuvR2_mEvc)n?Cow;~Wve|KR^>9@p5l)|QB+ z$jmun3q#x>;ss-PW_mnr2MHVzLAl1RW&0?VkixF*4t!St0YVb2wnKdU(kmOHiL;aW zK8Xte%(k>MVGG$E4no6dcNnb>BhVHHGD&1pv4YZ68kE2V03t5#PCEFm7=ad$6)+3B zTCmn*?A?=u(o~ET7~-7g0)ZB=6|lumi4}B}MLgy~Ysy6)Q5%Al7|05&1z3Jpu>cF8 z3?VXs*3<}%h3`5Wld)N2zJnk%Agw<~3k)sPTLFd=F5;d8-bj-09SkQuynfflNcZLN z!^_37fdZvzrq=9~mp*($%mcDRKC&qvaaZuX+C=AT6O*~tHl>0mcP<_q>-z%$xO(@! zYluq5a8VQI$S@4?r*v;gPo!QQ%pX3A#>xx4t=w-L6COWx?aj&`f+!YePsFtj=hOQR zP3=E2j@9L7s8;T^&s?u(Hdpu?CubjMrGn{t_37>9$|AD)QE08weJlKn8|OyjL~7oP zC8mPT`jzuH*Dh^I0048RGafUIT)4H~*m8m>egI0iH=(LB%b@@O002ovPDHLkV1lw0 B3nTN)c7hm#a-pu>H*?BW>MRK?U0)P)1TT2rdC3l?AO$ZT6gmuD{Cov-d z+kpqNxGmI^8?LeBPadwcFPyCd``)BY8zXFh3R)!?6svN@g@kVKO*tF zf*##(J&#?x-XBi%MFpq#`vEEk^jXwukhCp+TFvKPy9E3 z<)&CPT3Fx2%Jh5Kdp{9rk0JGT0pDjo!tc_3B4JB!ZEGr}R^#NpZom1Uu6a_920G4O zw3>p^)Z8Mz&dnoeS!fA`;biudWuN1G&OGL`j<2A(QjUku^~K!m4w@SpF!JIJ?i>!o z%@yEeca>$I<9yCM#V4IgP>1X`4a4&6L&$1XH71crz%3Qw?iZA0pBJAqkNK?Qn2$Cg zz3%$lx=SI!2;$jL5FR~#mpe_9BK-4jByicbP8l>R``%m`LLLkMd;Jn% Y0FP~Dgq+F&^Z)<=07*qoM6N<$f+qKQ2LJ#7 literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/reply.png b/plugins/redmine_contacts_helpdesk/assets/images/reply.png new file mode 100644 index 0000000000000000000000000000000000000000..3f40f9ddde439f3d76ab2e3ab96759df7de3ad75 GIT binary patch literal 565 zcmV-50?Pe~P)zk}+r#VHC&zcb8r*nyLtjT@;H-ut@23OCbqbITRc^ zI7mfAtq#E{cF26-(#6rClMoytcIc#W2v`bDvWSqFl!6s=$z9Wg-hE&0ySv=y(m-3V z)UE&NJ^1l|kN1DTu~T|ErKXot?~clc_BN@dSE}cG+W!m``$aN4r!t>u_io%*9N#&z zfdBwcGmlh35VYCJ#|m)?K7G%Y_AnfP382h5|FHP*ePN%$?BWAe5TQMZ&MCj!zaWQX z$kHGEo_7>^Y@*AIp{xPOs$5qa`HgzkieEYgm$}Qw40bqJQ5dshf zZ~?$29t3yflXBc8ZZo%#zq>Dz@tH9-mW&o-$!PJg=*DNp(tSqSjdB3MiALGbMhlsiB#&m1(6xp=2yHsp$QrvLs?qKl)=~X+%WyB7z<$ zrIwf)wn|%1%bX!)iBwwCx#iltZg;yo{W50S2j}1SeVlW?a|lF)nNXHM<}e_T^~dH# zaYMvHny0*GtUW$5jfjX-sw_#oqi%OWyW`$D`%u$$bXqN})A<{pJT#j3=Vll*lzYc&DxbUw{6Ft{W#1OC^X^g?Mst+~r9fkzaP3uj)K~;L!S2715n{ zIqa|<+6KYdXytT#bulfT3CE}`VP4J_>BPD`>4Zoq=D_&u6z_&}rp!8vwF`36@+I}R zYM&z__Pu?F7xjhSr7`ML=@Pjra8?~j6hF#V(!LS}qG2K4DAHa-L?8)=MXJ$iQvd+i z7zMOBv3~T&7el9Qa3F9$&scxW{XW~xahe6N41prVlb1nq`4mr{$P=`jcSE{1*!G-#sY-H5)KqV4yPMoVJuw$LySsC|5KaKx~ZqI(`Yx!rvfv|k3vhya2ec4EfE004iK zoN{ih08lHzW$y8LYCfit?RLwz?%{C@goT4tL_u1Fu;70Ns;unQ(NQu00B{0VfW8SI z2>|f+`&b8vL=Yb2gTjRH!qO0aS)edIDo(j8QFC?!ZC*n+TmkQ|5oo@8sfH^~G7*Rf zi}*ZQm3~f7W*8%L|NI3XVsoJ(WS+7Ag{djXY7u7e=z`^uOqx23p-52>JQXY$J@#Or zBuH2C@^4Ig)xqVCBg+^(qD9}=UbK$8v46yly_JWN-Z&R=S2ChOhqj{y-aYXm!=!G? zAMA_|J!m|6Xbb~jeNvCuScTYn0o%%phj+w>9t_kF|9)eZM4p&mT3jBNKEH_LXK5+0 zUfa8eqvqZp4Npz4ufJlL!9e~r0|0?P)vCi#|P&Xm-dkucwL z3)87{8iWe96huvPHfK`KOdC2Z({T6vJ9pwDx$D4>d(Pqff6w7Lmj{5i6;ZyPPpPN; zroaW=6d#@oL2Fa53F~$Su10(RG%K0p3VTuP3?Z=nBA8z$uq+XLUL^QrC74`bU|!e| zr>hK{)%Q!vdmIO5Z3JIvaOyjOX`X@c8-ua03`Q&)f&%p*{(A$q`ZTTjk%q_T7>v^J zu!R-a9fFLScYlKkNBP_Cob=9m9JLVoC-?c{)eOtMnh7qNN{ejy2sM{pS^mgFHJm@(buuM4>=<5Vr$&Kzw{B?uPr; z(1Yf=#g)zADkWnx=MR%ykl| z3Ui42k+O2{bCn)01-s5Sxp|z{G2di&KT(_M6;$EI zDL57JFf}cw4bP1P$pgTRKH$0@h|~aA>j`qZ2*kU5t2EVD5#~@VNhqx{vz8ethDD-=+1vnemftUBA zF;N!Q%PBB5B=KLB#QO(CHe?;R+-C8M?ppDW>R$5`cCPq@YpusFRTaH1i9Kv;l<>I( Ze*oTy+;kdDB`N>_002ovPDHLkV1l3CM+g7_ literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/images/world.png b/plugins/redmine_contacts_helpdesk/assets/images/world.png new file mode 100644 index 0000000000000000000000000000000000000000..68f21d30116710e48a8bf462cb32441e51fad5f6 GIT binary patch literal 923 zcmV;M17!S(P)A9)b<7tX~vT z$e)FfZ+`X4_uKyq#wJHC;J3lH{lhQkUc~Wid;*pnjhM12xe-bPByd^xuQ9zgeM^Mm z*tc)|P}LtTnHXr@Gkmmbkg^O2bqyhO>LP|qjIwW2@Di+4EuKm~&tOO2!N3o{128Hl z9v%fgerM0C#)7P|PMvxr*!Gf?eGA8f{OT6fS`9l>LQCg)p=~c$Zr|AT_0+_?F*JJk zlapOT2Q(wWx-LMq(TxXxLn+U;!LV)MhNp~ommdh+fo8T*&g-yQbbG&ze&=>tC(Ar=&^1xlA;Jc(6 zcCi_xs8k}-S&#ONOHm%e@#nGC7F++8C~r29Or!_{(QGQEG)+O^J1BCPmgM4JAzC8I z`jS9bO>|}Jq_#$IRzp0d34>)&3L%7MN)eTv!0B!^nn}f4z2*vFE@jv3dn zG>H)u>FR7_d2JcsjvfZ$vkP~xik@T^(_N)nx=tqJV+tQjQ`owJ83bf`zX6Ear*=Mhzn5QUuXE|v zR33Qyi8G!0{H2r##d#6R6YmYbZz4NTssT;cXiGb6lxO+k@{ba@2D~*hKDY6N;Bkh> xhhCRLejsJkAIT{5sICHcfU`5>bKmUb{{y)0nR3PMMxX!y002ovPDHLkV1nl+t-}BS literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/assets/javascripts/redmine_helpdesk.js b/plugins/redmine_contacts_helpdesk/assets/javascripts/redmine_helpdesk.js new file mode 100644 index 0000000..36c2dd4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/assets/javascripts/redmine_helpdesk.js @@ -0,0 +1,59 @@ +function togglePrivateTicketsOnChange() { + var checked = $(this).is(':checked'); + $('.private_tikets').attr('disabled', !checked); +} + +function togglePrivateTicketsInit() { + $('.assign_contact_user').each(togglePrivateTicketsOnChange); +} + +function toggleStatusesForAutoclose(node) { + if ($(node).val() == ''){ + $('#statuses_autoclose').hide(); + } else { + $('#statuses_autoclose').show(); + } +} + +function ccEmailTagResult (opt) { + if (opt.name){ + var formated_tag = $('' + opt.avatar + ' ' + opt.text + '');; + } else { + var formated_tag = opt.text; + } + return formated_tag +}; + +function ccEmailTagSelection (opt) { + if (opt.name){ + var formated_tag = opt.name + ' <' + opt.email + '>'; + } else { + var formated_tag = opt.text; + } + return formated_tag +}; + +function showWithSendAndScrollTo(id, focus) { + showAndScrollTo(id, focus); + send_mail_input = $('#helpdesk_is_send_mail'); + send_mail_input.prop( "checked", true ); + send_mail_input.checked = true; + toggleSendMail(send_mail_input); +} + +$(document).ready(function(){ + $('#content').on('change', '.assign_contact_user', togglePrivateTicketsOnChange); + togglePrivateTicketsInit(); + + $('#history .contextual a[title="Quote"]').click(function(){ + if ($('#ticket_data').length > 0) { + var journal_id = this.href.match(/journal_id=(\d*)/)[1]; + console.log(journal_id); + $.ajax({ + method: 'GET', + url: '/helpdesk/update_customer_email', + data: { journal_id: journal_id } + }); + } + }); +}); diff --git a/plugins/redmine_contacts_helpdesk/assets/stylesheets/helpdesk.css b/plugins/redmine_contacts_helpdesk/assets/stylesheets/helpdesk.css new file mode 100644 index 0000000..8c3f355 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/assets/stylesheets/helpdesk.css @@ -0,0 +1,370 @@ + +span.attachment img { + vertical-align: middle; +} + + +div#contacts_previous_issues { + margin-bottom: 20px; +} + +/**********************************************************************/ +/* TICKETS DATA SHOW ISSUE +/**********************************************************************/ + +span.helpdesk-message-date { + font-size: 0.9em; + color: #888; +} +/**********************************************************************/ +/* TICKETS DATA FORM +/**********************************************************************/ + +form#ticket_data_form { + background: + #ffffff; + display: block; + padding: 6px; + margin-bottom: 6px; + margin-right: 10px; + border: 1px solid + #d7d7d7; +} + +.contact_auto_complete > span { + font-weight: bold; +} + + +/**********************************************************************/ +/* TICKETS EXCERPT LIST +/**********************************************************************/ +table.list.issues td.helpdesk_ticket {white-space: normal; text-align: left;} +table.list.issues td.helpdesk_ticket .gravatar {float: left;} +table.list.issues td.helpdesk_ticket .ticket-data {margin-left: 45px;} +table.list.issues td.helpdesk_ticket .ticket-data .ticket-name {margin: 0px;font-weight: bold;} +table.list.issues td.helpdesk_ticket .ticket-status {float: left;} +table.list.issues td.last_message {vertical-align: top} + +/**********************************************************************/ +/* PUBCLIV TICKETS +/**********************************************************************/ +div.ticket-history div.journal { + overflow: visible; +} + +div.ticket-history div.journal.incoming, +div.ticket-history div.journal.outgoing { + padding: 6px; + margin-bottom: 6px; + border: 1px solid #d7d7d7; + position: relative; +} + +div.ticket-history div.journal.incoming { + background: #ececec; + margin-right: 30px; + margin-left: 4px; + +} +div.ticket-history div.journal.outgoing { + background: #f1faff; + margin-left: 30px; + margin-right: 4px; +} + +div.ticket-history div.journal.incoming:after { + content: ''; + display: block; + position: absolute; + top: 10px; + left:-7px; + width: 10px; + height: 10px; + background: #ececec; + border-left:1px solid #d7d7d7; + border-bottom:1px solid #d7d7d7; + -moz-transform:rotate(45deg); + -webkit-transform:rotate(45deg); +} + +div.ticket-history div.journal.outgoing:after { + content: ''; + display: block; + position: absolute; + top: 10px; + right:-7px; + width: 10px; + height: 10px; + background: #f1faff; + border-right:1px solid #d7d7d7; + border-bottom:1px solid #d7d7d7; + -moz-transform:rotate(-45deg); + -webkit-transform:rotate(-45deg); +} + +/**********************************************************************/ +/* TICKET DATA +/**********************************************************************/ +span.ticket-status { + padding: 3px 4px; + font-size: 10px; + white-space: nowrap; + margin-right: 4px; + color: white; +} + +div#ticket_data > div.contextual { + margin-top: initial; +} + +form.new_issue .email-template { + padding: 10px; +} + +span.ticket-status.status-1 {background-color: #CD5C5C;} +span.ticket-status.status-2 {background-color: deepSkyBlue;} +span.ticket-status.status-3 {background-color: green;} +span.ticket-status.status-4 {background-color: #FF8C00;} +span.ticket-status.status-5 {background-color: #AAA;} +span.ticket-status.status-6 {background-color: #8A2BE2;} + +/**********************************************************************/ +/* SHOW TICKET +/**********************************************************************/ + +#ticket-history .ticket-avatar {float: left;} +#ticket-history .ticket-note-content {margin-left: 50px;} +#ticket-history .ticket-note {border-top: 1px solid #d7d7d7; padding-top: 8px;} + +/**********************************************************************/ +/* SEND RESPONSE +/**********************************************************************/ + +.cc-list-edit .is-cc { + float: left; + min-width: 25px; + margin-top: 4px; +} + +.cc-list-edit { + margin-top: -8px; +} + +.cc-list-edit .select2 { + margin-left: 60px; + width: 90% !important; + -moz-border-radius: 0px; + -webkit-border-radius: 0px; + -khtml-border-radius: 0px; + border-radius: 0px; + background: white; + padding: 0px; +} +.cc-list-edit .select2 .select2-selection__choice { + background-color: #D7E7F9; + color: #000; +} + +.cc-list-edit ul.tagit li.tagit-choice .tagit-close .text-icon:hover { + color: black; +} + +.cc-list-edit ul.tagit li.tagit-new input { + font-size: 11px; + background: white; + margin-bottom: 2px; + margin-left: 2px; + width: 200px; +} + +.cc-list-edit ul.tagit li.tagit-new { + padding: 0px; +}*/ + +p#helpdesk_send_response { + margin-top: 5px; +} + + +/**********************************************************************/ +/* ISSUES LIST +/**********************************************************************/ + +tr.issue td.customer, tr.issue td.customer_company { + white-space: normal; + text-align: left; +} + +table.list.issue td.last_message img.gravatar{ + vertical-align: middle; + margin: 0 4px 2px 0; +} + +div.email-template { + background-color: #FFD; + border: 1px solid #E4E4E4; + padding-left: 10px; + padding-right: 10px; + margin-bottom: 5px; +} + +tr.issue td.last_message { + text-align: left; + white-space: normal; + padding: 5px; +} + +tr.issue td.last_message_date { + text-align: left; + white-space: normal; +} + +tr.issue.context-menu-selection td.last_message span.description { + color: inherit; +} + +/**********************************************************************/ +/* ICONS +/**********************************************************************/ + +#admin-menu a.helpdesk { background-image: url(../images/support.png)} +.icon-public-link { background-image: url(../../../images/external.png);padding-left: 14px;} +.icon-email-spam { background-image: url(../images/email_error.png); } +.icon-email-to { background-image: url(../images/email_go.png); } +.icon-email-from { background-image: url(../images/email_from.png); } +.icon-helpdesk { background-image: url(../images/user_comment.png); } +.icon-split { background-image: url(../images/arrow_divide.png); } +.icon-web { background-image: url(../images/world.png); } +.icon-support { background-image: url(../images/support.png); } +.icon-reply { background-image: url(../images/reply.png); } + +/**********************************************************************/ +/* VOTE +/**********************************************************************/ + +.vote_form { + text-align: center; +} + +.submit, #vote_comment { + margin-top: 5px; + width: 40%; + margin-left: auto; + margin-right: auto; + padding: 10px; +} + +.vote-value .icon { + padding-left: 20px; + margin-left: 5px; +} + +.icon-awesome { background-image: url(../images/awesome.png); } +.icon-justok { background-image: url(../images/just_ok.png); } +.icon-notgood { background-image: url(../images/not_good.png); } + +.error-text { color: red; } + +/**********************************************************************/ +/* CHARTS +/**********************************************************************/ + +.helpdesk_chart { text-align: center; width: 100%; } + +.center { text-align: center; } +.chart_table { margin: auto; border-collapse:collapse; text-align: center } +.chart_table .header { height: 50px; background-color: #eee } +.chart_table .header .column_data { border: 1px solid #c0c0c0; padding: 0px; } +.chart_table .header .column_data .issues_count { font-weight: bold; } +.chart_table .main_block { height: 200px } +.chart_table .main_block .column_data { vertical-align: bottom; width: 80px; border: 1px solid #c0c0c0; padding: 0px; } +.chart_table .main_block .column_data .percents { background-color: #BAE0BA; } +.chart_table .footer { height: 50px } +.chart_table .footer .column_data { font-weight: bold; padding: 0px; } + +/* Metrics*/ + +table.metrics { + border-collapse: separate; + table-layout: fixed; + width: 100%; +} + +.metrics.box { + background-color: #eee; + width: 250px; + margin: 15px; + border: 1px solid #c0c0c0; + display: inline-block; + padding: 20px 5px; +} + +.metrics td { + padding: 25px 0; + border-top: 1px solid #dbdde1; + text-align: left; +} + +.metrics .num { + color: #444; + font-size: 29px; + margin-right: 8px; + display: inline-block; +} + +.metrics .change { + display: inline-block; + color: #999; + font-size: 13px; + text-align: center; +} + +.metrics p { + color: #999; + margin: 2px; + line-height: 1.4em; +} + +.change>.caret { + border-width: 6px; + display: block; + margin: 0 auto 4px +} + +.change>.neg { + border-top-color: #ed5a5a +} + +.change>.pos { + border-top: 0; + border-bottom: 6px solid #43ac6d +} + +.change>.mirror_pos { + border-top: 0; + border-bottom: 6px solid #ed5a5a +} + +.change>.mirror_neg { + border-top-color: #43ac6d +} + +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-top: 4px solid #2b2b2b; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + content: "" +} + +.metrics .num>span { + font-size: 16px; +} + +#customer_previous_issues ul {margin: 0; padding: 0;} +#customer_previous_issues ul li {position: relative; margin-bottom: 10px} +#customer_previous_issues ul li .ticket-meta {color: #888; font-size: 0.9em;display: block} diff --git a/plugins/redmine_contacts_helpdesk/config/locales/de.yml b/plugins/redmine_contacts_helpdesk/config/locales/de.yml new file mode 100644 index 0000000..ef2ac09 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/de.yml @@ -0,0 +1,113 @@ +# encoding: utf-8 +# German strings go here for Rails i18n +de: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Vorlage für automatische E-Mail Antwort + text_helpdesk_answer_macros: Verfügbare Macros %{macro} + label_is_send_mail: Kommentar als E-Mail senden + label_send_auto_answer: Automatische Anwort senden + label_received_from: Erhalten von + label_sent_to: Gesendet an + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Kontakt antworten + permission_edit_helpdesk_settings: Kepldesk Einstellungen anpassen + label_not_create_contacts: Kontakte Whiteliste + field_created_contact_tags: Nach der Anlage Tags hinzufügen + label_helpdesk_save_as_attachment: E-Mails als Anhang anfügen + +#1.0.2 + label_helpdesk_assign_author: E-Mailabsender als Ticket Ersteller + +#1.0.3 + label_helpdesk_answered_status: Bearbeitungsstatus der Tickets (beantwortet/offen) + label_helpdesk_reopen_status: Ticket Status erneut öffnen + label_helpdesk_tracker: Ticket- Tracker/Typ + label_helpdesk_assigned_to: Ticket zuweisen an + label_helpdesk_lifetime: Ticket Lebensdauer + label_helpdesk_server_settings: E-Mail Server Einstellungen + label_helpdesk_protocol: Protokoll + label_helpdesk_host: Host + label_helpdesk_port: Port + label_helpdesk_username: Benutzername + label_helpdesk_password: Kennwort + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP Ordner + label_helpdesk_move_on_success: bei Erfolg verschieben + label_helpdesk_move_on_failure: bei Misserfolg verschieben + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: nicht verarbeitete Nachrichten löschen + label_helpdesk_first_answer_template: Vorlage + label_helpdesk_test_connection: Verbindungstest + label_helpdesk_get_mail: E-Mails abholen + label_helpdesk_get_mail_success: "Verabeitete %{count} E-Mail(s)" + label_helpdesk_css: E-Mail Stylesheet (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blacklist E-Mails + label_helpdesk_contact_activity: weitere Tickets des Kontakt + permission_view_helpdesk_tickets: Helpdesk Tickets anzeigen + label_helpdesk_last_message: Letzte Nachricht + label_helpdesk_enable_modules: Die Projektmodule Kontakte und Ticketverfolgung müssen aktiviert sein. + label_helpdesk_contact: Kontaktprofil + label_helpdesk_show_contact_card: Kontaktprofil anzeigen + +#2.0.0 + label_helpdesk_send_note_by_default: immer eine Nachricht schicken + label_helpdesk_ticket_attributes: Ticketattribute + label_helpdesk_to_address: An Adresse + label_helpdesk_from_address: Von Adresse + label_helpdesk_ticket_date: Ticketdatum + label_helpdesk_customer: Kunde/Kontakt + label_helpdesk_from: Von + label_email_cc: CC + label_email_bcc: BCC + label_helpdesk_ticket_plural: Helpdesk Tickets + label_helpdesk_template: Helpdesk Vorlagen + label_helpdesk_general: Allgemein + permission_edit_helpdesk_tickets: Ticket Info bearbeiten +#2.1.0 + label_helpdesk_spam: Junk Mail + label_helpdesk_add_cc: Cc/Bcc anfügen + label_helpdesk_add_contact_notes: Erste E-Mail als Notiz am Kontakt anhängen + text_helpdesk_customer_count: "%{count} Kontakt(e)" + text_helpdesk_ticket_count: "%{count} Ticket(s)" + label_helpdesk_tickets_report: Helpdesk-Ticketsbericht + label_helpdesk_staff_report: Helpdesk Teamreport + label_helpdesk_tickets_email: E-Mail + label_helpdesk_tickets_phone: Telefon + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Ticketquelle + label_helpdesk_ticket_time: Ticketzeit + label_helpdesk_save_cc: CC Adressaten als Kontakt ablegen + permission_view_helpdesk_reports: Berichte aufrufen + +#2.1.2 + label_helpdesk_answer_template: Antwortvorlage + +#2.1.3 + label_helpdesk_previous_tickets: Vorhergehende Tickets + label_helpdesk_public_tickets: Veröffentlichen von Tickets erlauben + label_helpdesk_public_title: Titel der öffentlichen Helpdesk Webseite + label_helpdesk_settings_public: Öffentliche Tickets + label_helpdesk_public_comments: Kommentare erlauben + label_helpdesk_public_show_spent_time: gebuchten Zeitaufwand anzeigen + label_helpdesk_public_link: Öffentliche URL + + +#2.1.4 + label_helpdesk_ticket_new: Neues Ticket + label_helpdesk_report_plural: Helpdesk reports + +#2.2.0 + label_helpdesk_contact_company: Kunde/Unternehmen + label_helpdesk_canned_response_plural: Textvorlagen + label_helpdesk_new_canned_response: Neue Textvorlage + label_helpdesk_canned_response: Textvorlage + permission_manage_public_canned_responses: Öffentliche Textvorlagen verwalten + permission_manage_canned_responses: Textvorlagen verwalten + +#2.2.1 + my_helpdesk_tickets: Meine Helpdesk Tickets + label_helpdesk_view_all_tickets: Alle Tickets anzeigen \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/en.yml b/plugins/redmine_contacts_helpdesk/config/locales/en.yml new file mode 100644 index 0000000..5ea2767 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/en.yml @@ -0,0 +1,247 @@ +# English strings go here for Rails i18n +en: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Auto answer email template + text_helpdesk_answer_macros: Avaliable macros %{macro} + label_is_send_mail: Send note + label_send_auto_answer: Send auto answer + label_received_from: Received from + label_sent_to: Sent to + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Send response to contact + permission_edit_helpdesk_settings: Edit hepldesk settings + label_not_create_contacts: Contacts whitelist + field_created_contact_tags: Add tags to contact + label_helpdesk_save_as_attachment: Save emails as attachments + +#1.0.2 + label_helpdesk_assign_author: Set issue author from sender + +#1.0.3 + label_helpdesk_answered_status: Answered ticket status + label_helpdesk_reopen_status: Reopen ticket status + label_helpdesk_tracker: Ticket tracker + label_helpdesk_assigned_to: Assign ticket to + label_helpdesk_lifetime: Ticket lifetime + label_helpdesk_server_settings: Mail server settings + label_helpdesk_protocol: Protocol + label_helpdesk_host: Host + label_helpdesk_port: Port + label_helpdesk_username: User name + label_helpdesk_password: Password + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP folder + label_helpdesk_move_on_success: Move on success + label_helpdesk_move_on_failure: Move on failure + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Delete unprocessed messages + label_helpdesk_first_answer_template: Template + label_helpdesk_test_connection: Test connection + label_helpdesk_get_mail: Get mail + label_helpdesk_get_mail_success: "Processed %{count} email(s)" + label_helpdesk_css: Email stylesheet (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blacklist emails + label_helpdesk_contact_activity: Contact activity + permission_view_helpdesk_tickets: View helpdesk tickets + label_helpdesk_last_message: Last message + label_helpdesk_enable_modules: Contacts and Issues modules should be enable for this project + label_helpdesk_contact: Helpdesk contact + label_helpdesk_show_contact_card: Show customer profiles + +#2.0.0 + label_helpdesk_send_note_by_default: Send note by default + label_helpdesk_ticket_attributes: Ticket attributes + label_helpdesk_to_address: To address + label_helpdesk_from_address: From address + label_helpdesk_ticket_date: Ticket date + label_helpdesk_customer: Customer + label_helpdesk_from: From + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: Helpdesk tickets + label_helpdesk_template: Helpdesk template + label_helpdesk_general: General + permission_edit_helpdesk_tickets: Edit ticket info + +#2.1.0 + label_helpdesk_ticket: Helpdesk ticket + label_helpdesk_spam: Junk mail + label_helpdesk_add_cc: Add Cc/Bcc + label_helpdesk_add_contact_notes: Add first email as contact note + text_helpdesk_customer_count: "%{count} customer(s)" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Helpdesk tickets report + label_helpdesk_staff_report: Helpdesk staff report + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Phone + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Ticket source + label_helpdesk_ticket_time: Ticket time + label_helpdesk_save_cc: Save CC as contacts + permission_view_helpdesk_reports: View reports + +#2.1.2 + label_helpdesk_answer_template: Answer template + +#2.1.3 + label_helpdesk_previous_tickets: Previous tickets + label_helpdesk_public_tickets: Enable public tickets + label_helpdesk_public_title: Public page title + label_helpdesk_settings_public: Public tickets + label_helpdesk_public_comments: Allow to add comments + label_helpdesk_public_show_spent_time: Show spent time + label_helpdesk_public_link: Public link + +#2.1.4 + label_helpdesk_ticket_new: New ticket + label_helpdesk_report_plural: Helpdesk reports + +#2.2.0 + label_helpdesk_contact_company: Helpdesk company + label_helpdesk_canned_response_plural: Canned responses + label_helpdesk_new_canned_response: New canned response + label_helpdesk_canned_response: Canned response + permission_manage_public_canned_responses: Manage public canned responses + permission_manage_canned_responses: Manage canned responses + +#2.2.1 + my_helpdesk_tickets: My helpdesk tickets + label_helpdesk_view_all_tickets: View all tickets + +#2.2.3 + label_helpdesk_tickets_conversation: Conversation + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contacts should not have the required custom fields + +#2.2.6 + label_helpdesk_last_message_date: Ticket updated + label_helpdesk_ago: "%{value} ago" + +#2.2.8 + label_helpdesk_to: To + label_helpdesk_send_as: Send as + label_helpdesk_not_send: "(Not send)" + label_helpdesk_send_as_notification: Send as notification + label_helpdesk_send_as_message: Send as initial message + +#2.2.9 + label_helpdesk_incoming_mail_server: Incoming mail server + label_helpdesk_outgoing_mail_server: Outgoing mail server + label_helpdesk_smtp_use_default_settings: Use default settings + label_helpdesk_smtp_server: SMTP server + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Authentication method + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Domain + + #2.2.10 + label_helpdesk_email_sending_problems: Email sending problems + + #2.3.0 + label_helpdesk_ticket_reaction_time: Time to reaction + label_helpdesk_ticket_first_response_time: Time to first response + label_helpdesk_ticket_resolve_time: Time to resolve + label_helpdesk_ticket_last_response_time: Wating response + field_helpdesk_ticket: Helpdesk ticket + text_helpdesk_to_address_cant_be_blank: To address can't be blank + text_helpdesk_message_body_cant_be_blank: Message body can't be blank + text_helpdesk_from_address_cant_be_blank: From address can't be blank + + #2.4.0 + label_helpdesk_vote: Vote + label_helpdesk_vote_settings: Allow vote + label_helpdesk_vote_comment_settings: Allow comment vote + label_helpdesk_mark: Please rate our work! + label_helpdesk_mark_awesome: Awesome + label_helpdesk_mark_justok: Just ok + label_helpdesk_mark_notgood: Not good + label_helpdesk_vote_comment_placeholder: Leave a comment + label_helpdesk_submit: Submit + label_helpdesk_vote_thank: Thank you for voting! + label_helpdesk_close_page: Close page + label_helpdesk_contact_vote: Contact vote + label_helpdesk_vote_comment: Vote comment + + field_vote: Vote + field_vote_comment: Vote comment + + # 3.0.2 + label_helpdesk_assign_contact_user: Assign contact user + label_helpdesk_create_private_tickets: Create private tickets + label_helpdesk_all: All + + label_helpdesk_widget: Widget + label_helpdesk_widget_enable: Enable widget + label_helpdesk_widget_available_projects: Avaliable projects + label_helpdesk_widget_no_available_projects: You should have at least one project with enabled Helpdesk module for widget activation + label_helpdesk_widget_custom_fields: Avaliable custom fields + label_helpdesk_widget_activation_message: Below code needs to add on page for widget activation + + label_helpdesk_widget_name: Your name + label_helpdesk_widget_email: Email address + label_helpdesk_widget_subject: Subject + label_helpdesk_widget_description: Ticket description + label_helpdesk_widget_create_ticket: Create ticket + label_helpdesk_widget_file_large: 'Sorry, too large' + label_helpdesk_widget_ticket_created: Ticket was created + label_helpdesk_widget_ticket_errors: "Can't add message " + label_helpdesk_widget_ticket_error_details: details + label_helpdesk_widget_ticket_error_description: Description cannot be empty + + label_helpdesk_reports: Helpdesk reports + label_helpdesk_first_response_time: First response time + label_helpdesk_report_names_first_response_time: First response time + label_helpdesk_filter_time_interval: Report time interval + label_helpdesk_first_response_time_interval_0_1h: '0 - 1h' + label_helpdesk_first_response_time_interval_1_2h: '1 - 2h' + label_helpdesk_first_response_time_interval_2_4h: '2 - 4h' + label_helpdesk_first_response_time_interval_4_8h: '4 - 8h' + label_helpdesk_first_response_time_interval_8_12h: '8 - 12h' + label_helpdesk_first_response_time_interval_12_24h: '12 - 24h' + label_helpdesk_first_response_time_interval_24_48h: '24 - 48h' + label_helpdesk_first_response_time_interval_48_0h: 'more 48h' + label_helpdesk_average_first_response_time: 'Average first response time' + label_helpdesk_average_responses_count: 'Average count of responses to close' + label_helpdesk_average_time_to_close: 'Average closing ticket time (automatically closed after 7 days)' + label_helpdesk_total_replies: Total replies + label_helpdesk_hour: "h" + label_helpdesk_minute: "m" + label_helpdesk_report_previous: 'Previous' + label_helpdesk_report_deviation: 'Deviation' + + label_helpdesk_busiest_time_of_day: Busiest time of day + label_helpdesk_report_names_busiest_time_of_day: Busiest time of day + label_helpdesk_busiest_time_of_day_interval_6_8h: '6 - 8h' + label_helpdesk_busiest_time_of_day_interval_8_10h: '8 - 10h' + label_helpdesk_busiest_time_of_day_interval_10_12h: '10 - 12h' + label_helpdesk_busiest_time_of_day_interval_12_14h: '12 - 14h' + label_helpdesk_busiest_time_of_day_interval_14_16h: '14 - 16h' + label_helpdesk_busiest_time_of_day_interval_16_18h: '16 - 18h' + label_helpdesk_busiest_time_of_day_interval_18_20h: '18 - 20h' + label_helpdesk_busiest_time_of_day_interval_20_22h: '20 - 22h' + label_helpdesk_busiest_time_of_day_interval_22_0h: '22 - 0h' + label_helpdesk_busiest_time_of_day_interval_0_2h: '0 - 2h' + label_helpdesk_busiest_time_of_day_interval_2_4h: '2 - 4h' + label_helpdesk_busiest_time_of_day_interval_4_6h: '4 - 6h' + label_helpdesk_busiest_time_of_day_new_tickets: New tickets + label_helpdesk_busiest_time_of_day_new_contacts: New contacts + label_helpdesk_number_of_tickets: Tickets count + label_helpdesk_open_tickets: Opened tickets + label_helpdesk_busiest_time_of_day_total_incoming: Total incoming + label_helpdesk_autoclose_tickets_after: Autoclose tickets after + label_helpdesk_autoclose_from_status: From status + label_helpdesk_autoclose_to_status: To status + label_helpdesk_hours: hours + label_helpdesk_days: days + label_helpdesk_autoclose_template: Autoclose template + label_helpdesk_autoclosed_ticket: Ticket was automatically closed + label_helpdesk_cc_address: CC addresses + + label_helpdesk_reply: Reply diff --git a/plugins/redmine_contacts_helpdesk/config/locales/es.yml b/plugins/redmine_contacts_helpdesk/config/locales/es.yml new file mode 100644 index 0000000..79f8384 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/es.yml @@ -0,0 +1,142 @@ +# English strings go here for Rails i18n +es: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Plantilla de autorespuesta por email + text_helpdesk_answer_macros: Macros disponibles %{macro} + label_is_send_mail: Enviar mensaje + label_send_auto_answer: Enviar autorespuesta + label_received_from: Recibido de + label_sent_to: Enviado a + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Enviar respuesta al contacto + permission_edit_helpdesk_settings: Editar ajustes del Helpdesk + label_not_create_contacts: Lista blanca de contactos + field_created_contact_tags: Añadir etiquetas tras creación + label_helpdesk_save_as_attachment: Guardar emails como adjuntos + +#1.0.2 + label_helpdesk_assign_author: Asignar al emisor del mensaje como autor del ticket + +#1.0.3 + label_helpdesk_answered_status: Estado de ticket respondido + label_helpdesk_reopen_status: Reabrir estado del ticket + label_helpdesk_tracker: Tipo de ticket + label_helpdesk_assigned_to: Asignar ticket a + label_helpdesk_lifetime: Validez del ticket + label_helpdesk_server_settings: Configuración del servidor de correo + label_helpdesk_protocol: Protocolo + label_helpdesk_host: Host + label_helpdesk_port: Puerto + label_helpdesk_username: Usuario + label_helpdesk_password: Contraseña + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: Carpeta IMAP + label_helpdesk_move_on_success: Mover tras acción correcta + label_helpdesk_move_on_failure: Mover tras fallo + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Eliminar mensajes no procesados + label_helpdesk_first_answer_template: Plantilla + label_helpdesk_test_connection: Conexión de prueba + label_helpdesk_get_mail: Recibir mensajes + label_helpdesk_get_mail_success: "Procesado(s) %{count} mensaje(s)" + label_helpdesk_css: Hoja de estilo del email (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Lista negra de emails + label_helpdesk_contact_activity: Tickets anteriores de clientes + permission_view_helpdesk_tickets: Ver tickets del helpdesk + label_helpdesk_last_message: Último mensaje + label_helpdesk_enable_modules: Los módulos de Contactos y de Tareas deben estar habilitados para este proyecto. + label_helpdesk_contact: Perfil de cliente + label_helpdesk_show_contact_card: Mostrar perfiles de clientes + +#2.0.0 + label_helpdesk_send_note_by_default: Enviar mensaje por defecto + label_helpdesk_ticket_attributes: Atributos del ticket + label_helpdesk_to_address: Dirección "A" + label_helpdesk_from_address: Dirección "De" + label_helpdesk_ticket_date: Fecha del ticket + label_helpdesk_customer: Cliente + label_helpdesk_from: De + label_helpdesk_cc: Cc + label_helpdesk_bcc: Cco + label_helpdesk_ticket_plural: Tickets de Helpdesk + label_helpdesk_template: Plantilla de Helpdesk + label_helpdesk_general: General + permission_edit_helpdesk_tickets: Editar información del ticket + +#2.1.0 + label_helpdesk_ticket: Ticket de Helpdesk + label_helpdesk_spam: Correo basura + label_helpdesk_add_cc: Añadir Cc/Cco + label_helpdesk_add_contact_notes: Añadir primer email como nota del contacto + text_helpdesk_customer_count: "%{count} cliente(s)" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Informe de tickets de Helpdesk + label_helpdesk_staff_report: Informe de personal de Helpdesk + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Teléfono + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Origen del ticket + label_helpdesk_ticket_time: Hora del ticket + label_helpdesk_save_cc: Guardar CC como contacto + permission_view_helpdesk_reports: Ver informes + +#2.1.2 + label_helpdesk_answer_template: Plantilla de respuesta + +#2.1.3 + label_helpdesk_previous_tickets: Tickets anteriores + label_helpdesk_public_tickets: Habilitar tickets públicos + label_helpdesk_public_title: Título de la página pública + label_helpdesk_settings_public: Tickets públicos + label_helpdesk_public_comments: Permitir añadir comentarios + label_helpdesk_public_show_spent_time: Mostrar tiempo invertido + label_helpdesk_public_link: Enlace público + +#2.1.4 + label_helpdesk_ticket_new: Nuevo ticket + label_helpdesk_report_plural: Informes del Helpdesk + +#2.2.0 + label_helpdesk_contact_company: Compañía cliente + label_helpdesk_canned_response_plural: Respuestas predefinidas + label_helpdesk_new_canned_response: Nueva respuesta predefinida + label_helpdesk_canned_response: Respuesta predefinida + permission_manage_public_canned_responses: Gestionar respuestas predefinidas públicas + permission_manage_canned_responses: Gestionar respuestas predefinidas + +#2.2.1 + my_helpdesk_tickets: Mis tickets del helpdesk + label_helpdesk_view_all_tickets: Ver todos los tickets + +#2.2.3 + label_helpdesk_tickets_conversation: Conversación + +#2.2.4 + label_helpdesk_required_custom_fields_error: Los contactos no deberían tener los campos personalizados requeridos + + #2.2.6 + label_helpdesk_last_message_date: Ticket actualizado + label_helpdesk_ago: "%{value} atrás" + +#2.2.8 + label_helpdesk_to: Para + label_helpdesk_send_as: Enviado como + label_helpdesk_not_send: "(No enviado)" + label_helpdesk_send_as_notification: Enviar como notificación + label_helpdesk_send_as_message: Enviar como mensaje inicial + +#2.2.9 + label_helpdesk_incoming_mail_server: Servidor de correo entrante + label_helpdesk_outgoing_mail_server: Servidor de correo saliente + label_helpdesk_smtp_use_default_settings: Usar opciones por default + label_helpdesk_smtp_server: Servidor SMTP + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Método de autenticación + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Dominio \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/fr.yml b/plugins/redmine_contacts_helpdesk/config/locales/fr.yml new file mode 100644 index 0000000..c0db9c1 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/fr.yml @@ -0,0 +1,196 @@ +# English strings go here for Rails i18n +fr: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Modèle de notification de réponse par email + text_helpdesk_answer_macros: "Macros disponibles : %{macro}" + label_is_send_mail: Envoyer cette note au contact + label_send_auto_answer: Envoyer la notification + label_received_from: Reçu de + label_sent_to: Envoyé à + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Envoyer la réponse au contact + permission_edit_helpdesk_settings: Editer les paramètres Helpdesk + label_not_create_contacts: Ne pas créer de nouveau contacts + field_created_contact_tags: Ajouter les tags après la création + label_helpdesk_save_as_attachment: Sauvegarder les emails en pièces jointes + +#1.0.2 + label_helpdesk_assign_author: Définir l'expéditeur comme auteur + +#1.0.3 + label_helpdesk_answered_status: État pour un ticket auquel il a été répondu + label_helpdesk_reopen_status: État pour réouverture d'un ticket + label_helpdesk_tracker: Tracker du ticket + label_helpdesk_assigned_to: Affecter le ticket à + label_helpdesk_lifetime: Durée de vie du ticket + label_helpdesk_server_settings: Paramètres du serveur d'emails + label_helpdesk_protocol: Protocole + label_helpdesk_host: Hôte + label_helpdesk_port: Port + label_helpdesk_username: Nom d'utilisateur + label_helpdesk_password: Mot de passe + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: Dossier IMAP + label_helpdesk_move_on_success: Si réussite, déplacer vers + label_helpdesk_move_on_failure: Si échec, déplacer vers + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Supprimer les messages non traités + label_helpdesk_first_answer_template: Modèle + label_helpdesk_test_connection: Tester la connexion + label_helpdesk_get_mail: Récupérer les emails + label_helpdesk_get_mail_success: "%{count} email(s) traités" + label_helpdesk_css: Feuille de style des emails (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blacklister les emails + label_helpdesk_contact_activity: Tickets précédents du client + permission_view_helpdesk_tickets: Afficher les tickets Helpdesk + label_helpdesk_last_message: Message précédent + label_helpdesk_enable_modules: Le module Contacts et demandes doit être activé pour ce projet + label_helpdesk_contact: Profil client + label_helpdesk_show_contact_card: Afficher les profils clients + +#2.0.0 + label_helpdesk_send_note_by_default: Par défaut, envoyer la note + label_helpdesk_ticket_attributes: Attributs du ticket + label_helpdesk_to_address: Destinataire + label_helpdesk_from_address: Expéditeur + label_helpdesk_ticket_date: Date du ticket + label_helpdesk_customer: Client + label_helpdesk_from: De + label_helpdesk_cc: Cc + label_helpdesk_bcc: Cci + label_helpdesk_ticket_plural: Tickets Helpdesk + label_helpdesk_template: Modèle Helpdesk + label_helpdesk_general: Général + permission_edit_helpdesk_tickets: Modifier les infos du ticket + +#2.1.0 + label_helpdesk_ticket: Ticket + label_helpdesk_spam: Spam + label_helpdesk_add_cc: Ajouter Cc/Cci + label_helpdesk_add_contact_notes: Ajouter le premier email aux notes du contact + text_helpdesk_customer_count: "%{count} client(s)" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Rapport tickets + label_helpdesk_staff_report: Rapport collaborateurs + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Téléphone + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Source du ticket + label_helpdesk_ticket_time: Heure du ticket + label_helpdesk_save_cc: Enregistrer les personnes en copie dans les contacts + permission_view_helpdesk_reports: Afficher les rapports + +#2.1.2 + label_helpdesk_answer_template: Modèle de réponse + +#2.1.3 + label_helpdesk_previous_tickets: Tickets précédents + label_helpdesk_public_tickets: Activer les tickets publics + label_helpdesk_public_title: Titre de la page publique + label_helpdesk_settings_public: Tickets publics + label_helpdesk_public_comments: Autoriser l'ajout de commentaires + label_helpdesk_public_show_spent_time: Afficher le temps passé + label_helpdesk_public_link: Lien public + +#2.1.4 + label_helpdesk_ticket_new: Nouveau ticket + label_helpdesk_report_plural: Rapports tickets + +#2.2.0 + label_helpdesk_contact_company: Entreprise du client + label_helpdesk_canned_response_plural: Textes prédéfinis + label_helpdesk_new_canned_response: Nouveau texte prédéfini + label_helpdesk_canned_response: Texte prédéfini + permission_manage_public_canned_responses: Gérer les réponses prédéfinies publiques + permission_manage_canned_responses: Gérer les réponses prédéfinies + +#2.2.1 + my_helpdesk_tickets: Mes tickets + label_helpdesk_view_all_tickets: Afficher tous les tickets + +#2.2.3 + label_helpdesk_tickets_conversation: Conversation + +#2.2.4 + label_helpdesk_required_custom_fields_error: Les contacts ne devraient pas avoir les champs personnalisés requis + +#2.2.6 + label_helpdesk_last_message_date: Ticket mis à jour + label_helpdesk_ago: "il y a %{value}" + +#2.2.8 + label_helpdesk_to: À + label_helpdesk_send_as: Envoyer en tant que + label_helpdesk_not_send: "(Non envoyé)" + label_helpdesk_send_as_notification: Envoyer en tant que notification + label_helpdesk_send_as_message: Envoyer en tant que premier message + +#2.2.9 + label_helpdesk_incoming_mail_server: Serveur de mails entrants + label_helpdesk_outgoing_mail_server: Serveur de mails sortants + label_helpdesk_smtp_use_default_settings: Utiliser les paramètres par défaut + label_helpdesk_smtp_server: Serveur SMTP + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Méthode d'authentification + label_helpdesk_authentication_plain: Simple + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Domaine + + #2.2.10 + label_helpdesk_email_sending_problems: Une erreur est survenue lors de l'envoi d'un email + + #2.3.0 + label_helpdesk_ticket_reaction_time: Temps de réaction + label_helpdesk_ticket_first_response_time: Temps pour la première réponse + label_helpdesk_ticket_resolve_time: Temps de résolution + label_helpdesk_ticket_last_response_time: Attente de réponse + field_helpdesk_ticket: Ticket Helpdesk + text_helpdesk_to_address_cant_be_blank: Le champ À ne peut pas être laissé vide + text_helpdesk_message_body_cant_be_blank: Le corps du message ne peut pas être laissé vide + text_helpdesk_from_address_cant_be_blank: Le champ De ne peut pas être laissé vide + + #2.4.0 + label_helpdesk_vote: Vote + label_helpdesk_vote_settings: Autoriser les votes + label_helpdesk_vote_comment_settings: Autoriser les commentaires de votes + label_helpdesk_mark: Merci pour votre évaluation de notre travail ! + label_helpdesk_mark_awesome: Parfait + label_helpdesk_mark_justok: Satisfaisant + label_helpdesk_mark_notgood: Non satisfaisant + label_helpdesk_vote_comment_placeholder: Laisser un commentaire + label_helpdesk_submit: Envoyer + label_helpdesk_vote_thank: Merci pour votre vote ! + label_helpdesk_close_page: Fermer la page + label_helpdesk_contact_vote: Vote du contact + label_helpdesk_vote_comment: Commentaire de vote + + field_vote: Vote + field_vote_comment: Commentaire de vote + + # 3.0.2 + label_helpdesk_assign_contact_user: Affecter l'utilisateur au contact + label_helpdesk_create_private_tickets: Créer des tickets privés + label_helpdesk_all: tous + + label_helpdesk_widget: Widget + label_helpdesk_widget_enable: Activer le widget + label_helpdesk_widget_available_projects: Projets disponibles + label_helpdesk_widget_no_available_projects: Vous devez avoir au moins un projet sur lequel Helpdesk est activé pour l'activation du module widget + label_helpdesk_widget_custom_fields: Champs personnalisés disponibles + label_helpdesk_widget_activation_message: Le code ci-dessous doit être ajouté à la page pour l'activation du widget + + label_helpdesk_widget_name: Votre nom + label_helpdesk_widget_email: Adresse e-mail + label_helpdesk_widget_subject: Sujet + label_helpdesk_widget_description: Description du ticket + label_helpdesk_widget_create_ticket: Créer le ticket + label_helpdesk_widget_file_large: 'Désolé, trop gros' + label_helpdesk_widget_ticket_created: Le ticket a été créé + label_helpdesk_widget_ticket_errors: "Impossible d'ajouter le message " + label_helpdesk_widget_ticket_error_details: détails + label_helpdesk_widget_ticket_error_description: La description ne peut pas être laissée vide diff --git a/plugins/redmine_contacts_helpdesk/config/locales/it.yml b/plugins/redmine_contacts_helpdesk/config/locales/it.yml new file mode 100644 index 0000000..8f2dbc7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/it.yml @@ -0,0 +1,154 @@ +it: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Modello per la risposta automatica + text_helpdesk_answer_macros: Macro disponibile %{macro} + label_is_send_mail: Scrivi una nota + label_send_auto_answer: Manda risposta automatica + label_received_from: Ricevuto da + label_sent_to: Spedita a + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Manda risposta al contatto + permission_edit_helpdesk_settings: Modifica le impostazioni helpdesk + label_not_create_contacts: whitelist contatti + field_created_contact_tags: Aggiungi i tags dopo la crezione + label_helpdesk_save_as_attachment: Salva email come allegati + +#1.0.2 + label_helpdesk_assign_author: Imposta l'autore della segnalazione in base al mittente + +#1.0.3 + label_helpdesk_answered_status: Stato ticket risposto + label_helpdesk_reopen_status: Stato ticket riaperto + label_helpdesk_tracker: Ticket tracker + label_helpdesk_assigned_to: Assegna ticket a + label_helpdesk_lifetime: Tempo di vita del Ticket + label_helpdesk_server_settings: Mail server settings + label_helpdesk_protocol: Protocol + label_helpdesk_host: Host + label_helpdesk_port: Port + label_helpdesk_username: User name + label_helpdesk_password: Password + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP folder + label_helpdesk_move_on_success: Move on success + label_helpdesk_move_on_failure: Move on failure + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Delete unprocessed messages + label_helpdesk_first_answer_template: Template + label_helpdesk_test_connection: Test connection + label_helpdesk_get_mail: Get mail + label_helpdesk_get_mail_success: "Processed %{count} email(s)" + label_helpdesk_css: Email stylesheet (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blacklist emails + label_helpdesk_contact_activity: tickets precedenti cliente + permission_view_helpdesk_tickets: Visualizza ticket dell'helpdesk + label_helpdesk_last_message: Ultimo messaggio + label_helpdesk_enable_modules: I moduli dei Contatti e delle Segnalazioni dovrebbero essere abilitati per questo progetto + label_helpdesk_contact: Profilo clienti + label_helpdesk_show_contact_card: Mostra profilo del cliente + +#2.0.0 + label_helpdesk_send_note_by_default: Manda una nota in automatico + label_helpdesk_ticket_attributes: Attributi dei Ticket + label_helpdesk_to_address: Indirizzo Destinatario + label_helpdesk_from_address: Indirizzo Mittente + label_helpdesk_ticket_date: Data del Ticket + label_helpdesk_customer: Cliente + label_helpdesk_from: Da + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: Helpdesk tickets + label_helpdesk_template: Helpdesk modello + label_helpdesk_general: Generale + permission_edit_helpdesk_tickets: Modifica ticket info + +#2.1.0 + label_helpdesk_ticket: Helpdesk ticket + label_helpdesk_spam: SPAM mail + label_helpdesk_add_cc: Aggiungi Cc/Bcc + label_helpdesk_add_contact_notes: Aggiungi la prima mail come nota del contatto + text_helpdesk_customer_count: "%{count} Cliente" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Helpdesk tickets report + label_helpdesk_staff_report: Helpdesk staff report + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Telefono + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Sorgente Ticket + label_helpdesk_ticket_time: Data Ticket + label_helpdesk_save_cc: Salva CC come contatto + permission_view_helpdesk_reports: Guarda reports + +#2.1.2 + label_helpdesk_answer_template: Modello delle risposte + +#2.1.3 + label_helpdesk_previous_tickets: Ticket precedenti + label_helpdesk_public_tickets: Abilita i tickets pubblici + label_helpdesk_public_title: Titolo Pagina ticket pubblici + label_helpdesk_settings_public: Ticket Pubblici + label_helpdesk_public_comments: Permetti l'aggiunta di commenti + label_helpdesk_public_show_spent_time: Mostra tempo impiegato + label_helpdesk_public_link: Link Pubblico + +#2.1.4 + label_helpdesk_ticket_new: Nuovo Ticket + label_helpdesk_report_plural: Helpdesk reports + +#2.2.0 + label_helpdesk_contact_company: Azienda Cliente + label_helpdesk_canned_response_plural: Risposte Tipiche + label_helpdesk_new_canned_response: Nuova Risposta Tipica + label_helpdesk_canned_response: Risposta Tipica + permission_manage_public_canned_responses: Gestisci le Risposte Tipiche pubbliche + permission_manage_canned_responses: Gestisci le Risposte Tipiche + +#2.2.1 + my_helpdesk_tickets: I miei ticket dell'helpdesk + label_helpdesk_view_all_tickets: Guarda tutti i ticket + +#2.2.3 + label_helpdesk_tickets_conversation: Discussione + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contacts should not have the required custom fields + +#2.2.6 + label_helpdesk_last_message_date: Data Aggiornamento + label_helpdesk_ago: "%{value} fa" + +#2.2.8 + label_helpdesk_to: A + label_helpdesk_send_as: Manda Come + label_helpdesk_not_send: "(Non Mandare)" + label_helpdesk_send_as_notification: Manda come Notifica + label_helpdesk_send_as_message: Manda come messaggio iniziale + +#2.2.9 + label_helpdesk_incoming_mail_server: Server posta in arrivo + label_helpdesk_outgoing_mail_server: Server posta in uscita + label_helpdesk_smtp_use_default_settings: Usa impostazioni di default + label_helpdesk_smtp_server: server SMTP + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Metodo di autenticazione + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Dominio SMTP + +#2.2.10 + label_helpdesk_email_sending_problems: Problemi invio Email + +#2.3.0 + label_helpdesk_ticket_reaction_time: Tempo Reazione + label_helpdesk_ticket_first_response_time: Tempo Prima Risposta + label_helpdesk_ticket_resolve_time: tempo Risoluzione + label_helpdesk_ticket_last_response_time: Tempo Ultima attesa + field_helpdesk_ticket: Ticket Helpdesk + text_helpdesk_to_address_cant_be_blank: "L'indirizzo di destinazione non può essere vuoto" + text_helpdesk_message_body_cant_be_blank: "Il corpo del messaggio non può essere vuoto" + text_helpdesk_from_address_cant_be_blank: "L'indirizzo del mittente non può essere vuoto" \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/nl.yml b/plugins/redmine_contacts_helpdesk/config/locales/nl.yml new file mode 100644 index 0000000..9e5927d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/nl.yml @@ -0,0 +1,145 @@ +# English strings go here for Rails i18n +nl: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Sjabloon voor automatisch bericht + text_helpdesk_answer_macros: Beschikbare macros %{macro} + label_is_send_mail: Verzenden naar contact + label_send_auto_answer: Verzend automatisch antwoord + label_received_from: Ontvangen van + label_sent_to: Verzonden naar + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Zend antwoord naar contact + permission_edit_helpdesk_settings: Bewerk hepldesk eigenschappen + label_not_create_contacts: Toegelaten contacten + field_created_contact_tags: Tags toevoegen na creatie + label_helpdesk_save_as_attachment: Bewaar de emails als bijlagen + +#1.0.2 + label_helpdesk_assign_author: Zet auteur op basis van contact + +#1.0.3 + label_helpdesk_answered_status: Status voor een beantwoord ticket + label_helpdesk_reopen_status: Status bij reactie van klant + label_helpdesk_tracker: Tracker + label_helpdesk_assigned_to: Ticket toekennen aan + label_helpdesk_lifetime: Levensduur van een ticket + label_helpdesk_server_settings: Eigenschappen van de mail server + label_helpdesk_protocol: Protocol + label_helpdesk_host: Server + label_helpdesk_port: Poort + label_helpdesk_username: Gebruiker + label_helpdesk_password: Wachtwoord + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP folder + label_helpdesk_move_on_success: Indien gelukt, verplaatsen naar + label_helpdesk_move_on_failure: Indien mislukt, verplaatsen naar + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Verwijder niet verwerkte boodschappen + label_helpdesk_first_answer_template: Sjlaboon + label_helpdesk_test_connection: Test connectie + label_helpdesk_get_mail: Mails ophalen + label_helpdesk_get_mail_success: "%{count} email(s) verwerkt" + label_helpdesk_css: Email stylesheet (CSS) + label_helpdesk_original: Origineel + +#1.0.4 + label_helpdesk_blacklist: Zwarte lijst van email adressen + label_helpdesk_previous_issues: Voorgaande tickets van het contact + permission_view_helpdesk_tickets: De helpdesk tickets bekijken + label_helpdesk_last_message: Voorgaande boodschap + label_helpdesk_enable_modules: De module contacten en issues moet geactiveerd zijn voor dit project + label_helpdesk_customer_profile: Profiel van het contact + label_helpdesk_show_contact_card: Profielen van de contacten tonen + +#2.0.0 + label_helpdesk_send_note_by_default: De nota altijd verzenden + label_helpdesk_ticket_attributes: Ticket eigenschappen + label_helpdesk_to_address: Geadresseerde + label_helpdesk_from_address: Verzender + label_helpdesk_ticket_date: Datum van ticket + label_helpdesk_customer: Klant + label_helpdesk_from: Van + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: Helpdesk tickets + label_helpdesk_template: Helpdesk sjabloon + label_helpdesk_general: Algemeen + permission_edit_helpdesk_tickets: Ticket info bewerken + +#2.1.0 + label_helpdesk_ticket: Helpdesk ticket + label_helpdesk_spam: Spam + label_helpdesk_add_cc: Cc/Bcc toevoegen + label_helpdesk_add_contact_notes: Voeg eerste email aan nota's van de klant + text_helpdesk_customer_count: "%{count} klant(en)" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Rapport van tickets + label_helpdesk_staff_report: Rapport van medewerkers + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Telefoon + label_helpdesk_tickets_web: Website + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Ticket bron + label_helpdesk_ticket_time: Tijdstip van het ticket + label_helpdesk_save_cc: Bewaar CC als contacten + permission_view_helpdesk_reports: Bekijk rapporten + +#2.1.2 + label_helpdesk_answer_template: Antwoord sjabloon + +#2.1.3 + label_helpdesk_previous_tickets: Voorgaande tickets + label_helpdesk_public_tickets: Activeer publieke tickets + label_helpdesk_public_title: Titel van de publieke pagina + label_helpdesk_settings_public: Publieke tickets + label_helpdesk_public_comments: Het toevoegen van commentaar toelaten + label_helpdesk_public_show_spent_time: Toon gespendeerde tijd + label_helpdesk_public_link: Publieke link + +#2.1.4 + label_helpdesk_ticket_new: Nieuw ticket + label_helpdesk_report_plural: Helpdesk rapporten + +#2.2.0 + label_helpdesk_customer_company: Klant bedrijf + label_helpdesk_canned_response_plural: Standaard antwoorden + label_helpdesk_new_canned_response: Nieuw standaard antwoord + label_helpdesk_canned_response: Standaard antwoord + permission_manage_public_canned_responses: Beheer publieke standaard antwoorden + permission_manage_canned_responses: Beheer standaard antwoorden + +#2.2.1 + my_helpdesk_tickets: Mijn helpdesk tickets + label_helpdesk_view_all_tickets: Bekijk alle tickets + +#2.2.3 + label_helpdesk_tickets_conversation: Conversatie + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contacten zouden geen verplichte niet-standaard eigenschappen hebben + +#2.2.6 + label_helpdesk_last_message_date: Ticket aangepast op + label_helpdesk_ago: "%{value} geleden" + +#2.2.8 + label_helpdesk_to: Naar + label_helpdesk_send_as: Verzend als + label_helpdesk_not_send: "(Niet verzonden)" + label_helpdesk_send_as_notification: Verzonden als notificatie + label_helpdesk_send_as_message: Verzonden als eerste boodschap + +#2.2.9 + label_helpdesk_incoming_mail_server: Binnenkomende mail server + label_helpdesk_outgoing_mail_server: Uitgaande mail server + label_helpdesk_smtp_use_default_settings: Gebruik standaard eigenschappen + label_helpdesk_smtp_server: SMTP server + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Authenticatie methode + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Domein + + #2.2.10 + label_helpdesk_email_sending_problems: Een fout is opgetreden tijdens het verzenden van een email \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/pl.yml b/plugins/redmine_contacts_helpdesk/config/locales/pl.yml new file mode 100644 index 0000000..cbea392 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/pl.yml @@ -0,0 +1,123 @@ +# Polish strings go here for Rails i18n +pl: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Auto answer email template + text_helpdesk_answer_macros: Avaliable macros %{macro} + label_is_send_mail: Send note + label_send_auto_answer: Send auto answer + label_received_from: Received from + label_sent_to: Sent to + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Send response to contact + permission_edit_helpdesk_settings: Edit hepldesk settings + label_not_create_contacts: Contacts whitelist + field_created_contact_tags: Add tags after create + label_helpdesk_save_as_attachment: Save emails as attachments + +#1.0.2 + label_helpdesk_assign_author: Set issue author from sender + +#1.0.3 + label_helpdesk_answered_status: Answered ticket status + label_helpdesk_reopen_status: Reopen ticket status + label_helpdesk_tracker: Ticket tracker + label_helpdesk_assigned_to: Przypisz zgÅ‚oszenie do + label_helpdesk_lifetime: Ticket lifetime + label_helpdesk_server_settings: Ustawienia serwera pocztowego + label_helpdesk_protocol: Protokół + label_helpdesk_host: Host + label_helpdesk_port: Port + label_helpdesk_username: Użytkownik + label_helpdesk_password: HasÅ‚o + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: folder IMAP + label_helpdesk_move_on_success: Move on success + label_helpdesk_move_on_failure: Move on failure + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Delete unprocessed messages + label_helpdesk_first_answer_template: Szablon + label_helpdesk_test_connection: Przetestuj połączenie + label_helpdesk_get_mail: Pobierz pocztÄ™ + label_helpdesk_get_mail_success: "Processed %{count} email(s)" + label_helpdesk_css: Email stylesheet (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blacklist emails + label_helpdesk_contact_activity: Customer previous tickets + permission_view_helpdesk_tickets: View helpdesk tickets + label_helpdesk_last_message: Last message + label_helpdesk_enable_modules: Contacts and Issues modules should be enable for this project + label_helpdesk_contact: Customer profile + label_helpdesk_show_contact_card: Show customer profiles + +#2.0.0 + label_helpdesk_send_note_by_default: Send note by default + label_helpdesk_ticket_attributes: Atrybuty zgÅ‚oszenia + label_helpdesk_to_address: To address + label_helpdesk_from_address: From address + label_helpdesk_ticket_date: Ticket date + label_helpdesk_customer: Klient + label_helpdesk_from: Od + label_helpdesk_cc: Dw + label_helpdesk_bcc: Udw + label_helpdesk_ticket_plural: ZgÅ‚oszenia Helpdesk + label_helpdesk_template: Szablon Helpdesk + label_helpdesk_general: General + permission_edit_helpdesk_tickets: Edytuj informacje zgÅ‚oszenia + +#2.1.0 + label_helpdesk_ticket: ZgÅ‚oszenie Helpdesk + label_helpdesk_spam: SPAM + label_helpdesk_add_cc: Dodaj DW/UDW + label_helpdesk_add_contact_notes: Add first email as contact note + text_helpdesk_customer_count: "%{count} klient(ów)" + text_helpdesk_ticket_count: "%{count} zgÅ‚oszeÅ„" + label_helpdesk_tickets_report: Helpdesk tickets report + label_helpdesk_staff_report: Helpdesk staff report + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Telefon + label_helpdesk_tickets_web: WWW + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Ticket source + label_helpdesk_ticket_time: Ticket time + label_helpdesk_save_cc: Zapisz DW jako kontakty + permission_view_helpdesk_reports: Zobacz raporty + +#2.1.2 + label_helpdesk_answer_template: Szablon odpowiedzi + +#2.1.3 + label_helpdesk_previous_tickets: Poprzednie zgÅ‚oszenia + label_helpdesk_public_tickets: Enable public tickets + label_helpdesk_public_title: Public page title + label_helpdesk_settings_public: ZgÅ‚oszenia publiczne + label_helpdesk_public_comments: Pozwól dodawać koementarze + label_helpdesk_public_show_spent_time: Show spent time + label_helpdesk_public_link: Link publiczny + +#2.1.4 + label_helpdesk_ticket_new: Nowe zgÅ‚oszenie + label_helpdesk_report_plural: Helpdesk reports + +#2.2.0 + label_helpdesk_contact_company: Customer company + label_helpdesk_canned_response_plural: Canned responses + label_helpdesk_new_canned_response: New canned response + label_helpdesk_canned_response: Canned response + permission_manage_public_canned_responses: Manage public canned responses + permission_manage_canned_responses: Manage canned responses + +#2.2.1 + my_helpdesk_tickets: Moje zgÅ‚oszenia + label_helpdesk_view_all_tickets: Zobacz wszystkie zgÅ‚oszenia + +#2.2.3 + label_helpdesk_tickets_conversation: Konwersacja + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contacts should not have the required custom fields + +#2.2.6 + label_helpdesk_last_message_date: ZgÅ‚oszenie zaktualizowane + label_helpdesk_ago: "%{value} temu" \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/pt-BR.yml b/plugins/redmine_contacts_helpdesk/config/locales/pt-BR.yml new file mode 100644 index 0000000..5f377d5 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/pt-BR.yml @@ -0,0 +1,174 @@ +pt-BR: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Template do email de resposta automática + text_helpdesk_answer_macros: Macros disponíveis %{macro} + label_is_send_mail: Enviar notas + label_send_auto_answer: Enviar resposta automática + label_received_from: Recebido de + label_sent_to: Enviar para + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Enviar resposta para o contato + permission_edit_helpdesk_settings: Editar configurações de hepldesk + label_not_create_contacts: Lista de contatos confiáveis + field_created_contact_tags: Tags adicionadas após a inclusão + label_helpdesk_save_as_attachment: Salvar emails como anexo + +#1.0.2 + label_helpdesk_assign_author: Definir autor da tarefa como o remetente + +#1.0.3 + label_helpdesk_answered_status: Status do ticket respondido + label_helpdesk_reopen_status: Status do ticket reaberto + label_helpdesk_tracker: Rastrear ticket + label_helpdesk_assigned_to: Atribuir ticket para + label_helpdesk_lifetime: Tempo de vida do ticket + label_helpdesk_server_settings: Configurações do servidor de email + label_helpdesk_protocol: Protocolo + label_helpdesk_host: Servidor + label_helpdesk_port: Porta + label_helpdesk_username: Usuário + label_helpdesk_password: Senha + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: Pasta IMAP + label_helpdesk_move_on_success: Quando bem-sucedido mover para + label_helpdesk_move_on_failure: Quando falhar mover para + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Excluir mensagens não processadas + label_helpdesk_first_answer_template: Template + label_helpdesk_test_connection: Testar conexão + label_helpdesk_get_mail: Buscar email + label_helpdesk_get_mail_success: "Processado %{count} email(s)" + label_helpdesk_css: Estilo do email (CSS) + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Lista negra + label_helpdesk_contact_activity: Customer previous tickets + permission_view_helpdesk_tickets: Visualizar tickets de helpdesk + label_helpdesk_last_message: Última mensagem + label_helpdesk_enable_modules: Contatos e Tarefas devem ser habilitados para este projeto + label_helpdesk_contact: Perfil do cliente + label_helpdesk_show_contact_card: Visualizar perfil do cliente + +#2.0.0 + label_helpdesk_send_note_by_default: Enviar notas por padrão + label_helpdesk_ticket_attributes: Atributos do ticket + label_helpdesk_to_address: Para + label_helpdesk_from_address: De + label_helpdesk_ticket_date: Data do ticket + label_helpdesk_customer: Cliente + label_helpdesk_from: De + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: Helpdesk tickets + label_helpdesk_template: Helpdesk template + label_helpdesk_general: Geral + permission_edit_helpdesk_tickets: Alterar informações do ticket + +#2.1.0 + label_helpdesk_ticket: Helpdesk ticket + label_helpdesk_spam: Lixo eletrônico + label_helpdesk_add_cc: Add Cc/Bcc + label_helpdesk_add_contact_notes: Adicionar primeiro email como nota + text_helpdesk_customer_count: "%{count} cliente(s)" + text_helpdesk_ticket_count: "%{count} ticket(s)" + label_helpdesk_tickets_report: Relatório de tickets de Helpdesk + label_helpdesk_staff_report: Relatório da equipe de Helpdesk + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Telefone + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Origem do ticket + label_helpdesk_ticket_time: Duração do ticket + label_helpdesk_save_cc: Salvar CC como contato + permission_view_helpdesk_reports: Mostrar relatórios + +#2.1.2 + label_helpdesk_answer_template: Template de resposta + +#2.1.3 + label_helpdesk_previous_tickets: Tickets anteriores + label_helpdesk_public_tickets: Habilitar tickets públicos + label_helpdesk_public_title: Título da página pública + label_helpdesk_settings_public: Tickets públicos + label_helpdesk_public_comments: Permite adicionar comentários + label_helpdesk_public_show_spent_time: Mostrar tempo + label_helpdesk_public_link: Link público + +#2.1.4 + label_helpdesk_ticket_new: Novo ticket + label_helpdesk_report_plural: Relatórios de Helpdesk + +#2.2.0 + label_helpdesk_contact_company: Empresa do cliente + label_helpdesk_canned_response_plural: Respostas pré-definidas + label_helpdesk_new_canned_response: Nova resposta pré-definida + label_helpdesk_canned_response: Resposta pré-definida + permission_manage_public_canned_responses: Administrar respostas pré-definida públicas + permission_manage_canned_responses: Administrar respostas pré-definida + +#2.2.1 + my_helpdesk_tickets: Meus tickets + label_helpdesk_view_all_tickets: Mostar todos os tickets + +#2.2.3 + label_helpdesk_tickets_conversation: Conversa + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contatos não devem possuir campos requeridos + +#2.2.6 + label_helpdesk_last_message_date: Ticket atualizado + label_helpdesk_ago: "%{value} atrás" + + +#2.2.8 + label_helpdesk_to: Para + label_helpdesk_send_as: Enviar como + label_helpdesk_not_send: "(Não enviado)" + label_helpdesk_send_as_notification: Enviar como notificação + label_helpdesk_send_as_message: Enviar como menssagem inicial + +#2.2.9 + label_helpdesk_incoming_mail_server: Caixa de entrada do servidor + label_helpdesk_outgoing_mail_server: Caixa de saída do servidor + label_helpdesk_smtp_use_default_settings: Usar configurações padrões + label_helpdesk_smtp_server: Servidor SMTP + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Método de autenticação + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 nas respostas pré-definidas + label_helpdesk_smtp_domain: Domínio + + #2.2.10 + label_helpdesk_email_sending_problems: Problemas ao enviar email + + #2.3.0 + label_helpdesk_ticket_reaction_time: Tempo para reação + label_helpdesk_ticket_first_response_time: Tempo para resposta + label_helpdesk_ticket_resolve_time: Tempo para resolução + label_helpdesk_ticket_last_response_time: Aguardando resposta + field_helpdesk_ticket: Helpdesk ticket + text_helpdesk_to_address_cant_be_blank: Destinatário não pode ser nulo + text_helpdesk_message_body_cant_be_blank: Menssagem não pode ser nula + text_helpdesk_from_address_cant_be_blank: Remetente não pode ser nulo + + #2.4.0 + label_helpdesk_vote: Votação + label_helpdesk_vote_settings: Permitir votar + label_helpdesk_vote_comment_settings: Permitir comentar o voto + label_helpdesk_mark: Por favor avalie nosso trabalho! + label_helpdesk_mark_awesome: Muito bom + label_helpdesk_mark_justok: Regular + label_helpdesk_mark_notgood: Ruim + label_helpdesk_vote_comment_placeholder: Deixe um comentário + label_helpdesk_submit: Enviar + label_helpdesk_vote_thank: Obrigado pelo seu voto! + label_helpdesk_close_page: Fechar página + label_helpdesk_customer_vote: Voto do cliente + label_helpdesk_vote: Votar + label_helpdesk_vote_comment: Comentário do voto + + field_vote: Votar + field_vote_comment: Comentário do voto \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/config/locales/ru.yml b/plugins/redmine_contacts_helpdesk/config/locales/ru.yml new file mode 100644 index 0000000..5202e90 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/ru.yml @@ -0,0 +1,249 @@ +# Russian strings go here for Rails i18n +ru: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Шаблон авто-ответа + text_helpdesk_answer_macros: МакроÑÑ‹ %{macro} + label_is_send_mail: Отправить заметку + label_send_auto_answer: ОтправлÑть авто-ответ + label_received_from: Получено от + label_sent_to: Отправлено + project_module_contacts_helpdesk: Helpdesk + permission_send_response: ОтправлÑть заметки контактам + permission_edit_helpdesk_settings: Редактирование наÑтроек + label_not_create_contacts: Ðе Ñоздавать новые контакты + field_created_contact_tags: ДобавлÑть Ñ‚Ñги к Ñозданному контакту + label_helpdesk_save_as_attachment: СохранÑть email как вложение + +#1.0.2 + label_helpdesk_assign_author: Ðазначать автора из Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð¸Ñ‚ÐµÐ»Ñ + +#1.0.3 + label_helpdesk_answered_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚Ð°Ð½Ð½Ð¾Ð³Ð¾ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_reopen_status: Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð´Ð»Ñ Ð¿Ñ€Ð¸ повторном обращении + label_helpdesk_tracker: ТрÑкер Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_assigned_to: Ðазначать обращение + label_helpdesk_lifetime: Создавать новое обращение поÑле + label_helpdesk_server_settings: ÐаÑтройки почтового Ñервера + label_helpdesk_protocol: Протокол + label_helpdesk_host: ХоÑÑ‚ + label_helpdesk_port: Порт + label_helpdesk_username: Логин + label_helpdesk_password: Пароль + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP папка + label_helpdesk_move_on_success: Перемещать в папку при удаче + label_helpdesk_move_on_failure: Перемещать в папку при ошибке + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: УдалÑть необработанные ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_first_answer_template: Шаблон + label_helpdesk_test_connection: Проверить подключение + label_helpdesk_get_mail: Получить почту + label_helpdesk_get_mail_success: "Обработано %{count} Ñообщений" + label_helpdesk_original: Оригинал + +#1.0.4 + label_helpdesk_blacklist: Черный ÑпиÑок email + label_helpdesk_contact_activity: ÐктивноÑть контакта + permission_view_helpdesk_tickets: ПроÑмотр обращений + label_helpdesk_last_message: ПоÑледнее Ñообщение + label_helpdesk_enable_modules: Ð”Ð»Ñ Ñтого проекта должны быть включены модули Контакты и Задачи + label_helpdesk_contact: Контакт Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_show_contact_card: Показывать профили контактов + +#2.0.0 + label_helpdesk_send_note_by_default: ОтправлÑть заметку по умолчанию + label_helpdesk_ticket_attributes: Ðтрибуты Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_to_address: ÐÐ´Ñ€ÐµÑ Ð¿Ð¾Ð»ÑƒÑ‡Ð°Ñ‚ÐµÐ»Ñ + label_helpdesk_from_address: ÐÐ´Ñ€ÐµÑ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð¸Ñ‚ÐµÐ»ÑŒ + label_helpdesk_ticket_date: Дата Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_customer: Клиент + label_helpdesk_from: От + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: ÐžÐ±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Helpdesk + label_helpdesk_template: Helpdesk шаблоны + label_helpdesk_general: ОÑновные + permission_edit_helpdesk_tickets: Редактирование реквизитов Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + +#2.1.0 + label_helpdesk_ticket: Обращение + label_helpdesk_spam: Удалить как Ñпам + label_helpdesk_add_cc: Добавить Cc/Bcc + label_helpdesk_add_contact_notes: ДобавлÑть первое обращение в заметки контакта + text_helpdesk_customer_count: "%{count} клиент(ов)" + text_helpdesk_ticket_count: "%{count} обращение(й)" + label_helpdesk_tickets_report: Отчет по обращениÑм + label_helpdesk_staff_report: Отчет по Ñотрудникам + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Телефон + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Канал Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_ticket_time: Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_save_cc: СохранÑть CC как контакты + permission_view_helpdesk_reports: ПроÑмотр отчетов + +#2.1.2 + label_helpdesk_answer_template: Шаблон ответа + +#2.1.3 + label_helpdesk_previous_tickets: Предыдущие Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_public_tickets: Публичные Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_public_title: Заголовок публичной Ñтраницы + label_helpdesk_settings_public: Публичные Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_public_comments: Добавление комментариев + label_helpdesk_public_show_spent_time: Показывать затраченное Ð²Ñ€ÐµÐ¼Ñ + label_helpdesk_public_link: ÐŸÑƒÐ±Ð»Ð¸Ñ‡Ð½Ð°Ñ ÑÑылка + +#2.1.4 + label_helpdesk_ticket_new: Ðовое обращение + label_helpdesk_report_plural: Отчеты по обращениÑм + +#2.2.0 + label_helpdesk_contact_company: ÐšÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚Ð° Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_canned_response_plural: Шаблоны ответов + label_helpdesk_new_canned_response: Ðовый шаблон ответа + label_helpdesk_canned_response: Шаблон ответа + permission_manage_public_canned_responses: Управление обшими шаблонами ответов + permission_manage_canned_responses: Управление шаблонами ответов + +#2.2.1 + my_helpdesk_tickets: Мои Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_view_all_tickets: Показать вÑе Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + +#2.2.3 + label_helpdesk_tickets_conversation: БеÑеда + +#2.2.4 + label_helpdesk_required_custom_fields_error: Контакты не должны иметь обÑзательные наÑтраиваемые Ð¿Ð¾Ð»Ñ + +#2.2.6 + label_helpdesk_last_message_date: Обращение обновлено + label_helpdesk_ago: "%{value} назад" + +#2.2.8 + label_helpdesk_to: Кому + label_helpdesk_send_as: Отправить как + label_helpdesk_not_send: "(Ðе отправлÑть)" + label_helpdesk_send_as_notification: Отправить как извещение + label_helpdesk_send_as_message: Отправить как новое Ñообщение + +#2.2.9 + label_helpdesk_incoming_mail_server: Сервер входÑщей почты + label_helpdesk_outgoing_mail_server: Сервер иÑходÑщей почты + label_helpdesk_smtp_use_default_settings: ÐаÑтройки по умолчанию + label_helpdesk_smtp_server: SMTP Ñервер + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Метод аунтификации + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Домен + + #2.2.10 + label_helpdesk_email_sending_problems: Проблемы при отправке email + + #2.3.0 + label_helpdesk_ticket_reaction_time: Ð’Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð¹ реакции + label_helpdesk_ticket_first_response_time: Ð’Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ ответа + label_helpdesk_ticket_resolve_time: Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ решение + label_helpdesk_ticket_last_response_time: Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð¶Ð¸Ð´Ð°Ð½Ð¸Ñ + field_helpdesk_ticket: Обращение + text_helpdesk_to_address_cant_be_blank: ÐÐ´Ñ€ÐµÑ Ð¿Ð¾Ð»ÑƒÑ‡Ð°Ñ‚ÐµÐ»Ñ Ð½Ðµ может быть пуÑтым + text_helpdesk_message_body_cant_be_blank: Содержание Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð½Ðµ может быть пуÑтым + text_helpdesk_from_address_cant_be_blank: ÐÐ´Ñ€ÐµÑ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð¸Ñ‚ÐµÐ»Ñ Ð½Ðµ может быть пуÑтым + + #2.4.0 + label_helpdesk_vote: Оценка + label_helpdesk_vote_settings: Разрешить оценки + label_helpdesk_vote_comment_settings: Разрешить комментировать оценки + label_helpdesk_mark: Оцените нашу работу! + label_helpdesk_mark_awesome: Отлично + label_helpdesk_mark_justok: Ðормально + label_helpdesk_mark_notgood: Плохо + label_helpdesk_vote_comment_placeholder: ОÑтавьте коментарий + label_helpdesk_submit: Отправить + label_helpdesk_vote_thank: СпаÑибо за вашу оценку! + label_helpdesk_close_page: Закрыть + label_helpdesk_contact_vote: Оценка контакта + label_helpdesk_vote_comment: Коментарий к отзыву + + field_vote: Оценка + field_vote_comment: Коментарий к оценке + + # 3.0.2 + label_helpdesk_assign_contact_user: Ðазначать ответÑтвенного Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ + label_helpdesk_create_private_tickets: Создавать приватные Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_all: Ð’Ñе + + label_helpdesk_widget: Виджет + label_helpdesk_widget_enable: Ðктивировать виджет + label_helpdesk_widget_available_projects: ДоÑтупные проекты + label_helpdesk_widget_no_available_projects: Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ виджета необходим Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ один проект Ñо влюченным модулем Helpdesk + label_helpdesk_widget_custom_fields: ДоÑтупные доп. Ð¿Ð¾Ð»Ñ + label_helpdesk_widget_activation_message: Ð”Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ виджета необходимо добавить на Ñтраницу код ниже + + label_helpdesk_widget_name: Ваше Ð¸Ð¼Ñ + label_helpdesk_widget_email: Email + label_helpdesk_widget_subject: Тема Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_widget_description: Содержание Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_widget_file_large: Файл Ñлишком большой + label_helpdesk_widget_ticket_created: Обращение Ñоздано + label_helpdesk_widget_ticket_errors: "Ðе могу добавить обращение " + label_helpdesk_widget_ticket_error_details: подробнее + label_helpdesk_widget_ticket_error_description: ОпиÑание не может быть пуÑтым + + label_helpdesk_widget_create_ticket: Создать обращение + + label_helpdesk_reports: Helpdesk отчеты + label_helpdesk_first_response_time: Ð’Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ ответа + label_helpdesk_report_names_first_response_time: Ð’Ñ€ÐµÐ¼Ñ Ð¿ÐµÑ€Ð²Ð¾Ð³Ð¾ ответа + label_helpdesk_filter_time_interval: Интервал времени + label_helpdesk_first_response_time_interval_0_1h: '0 - 1ч' + label_helpdesk_first_response_time_interval_1_2h: '1 - 2ч' + label_helpdesk_first_response_time_interval_2_4h: '2 - 4ч' + label_helpdesk_first_response_time_interval_4_8h: '4 - 8ч' + label_helpdesk_first_response_time_interval_8_12h: '8 - 12ч' + label_helpdesk_first_response_time_interval_12_24h: '12 - 24ч' + label_helpdesk_first_response_time_interval_24_48h: '24 - 48ч' + label_helpdesk_first_response_time_interval_48_0h: 'более 48ч' + label_helpdesk_average_first_response_time: 'СреднÑÑ ÑкороÑть первого ответа' + label_helpdesk_average_responses_count: 'Среднее количеÑтво ответов Ð´Ð»Ñ Ð·Ð°ÐºÑ€Ñ‹Ñ‚Ð¸Ñ' + label_helpdesk_average_time_to_close: 'СреднÑÑ ÑкороÑть Ð·Ð°ÐºÑ€Ñ‹Ñ‚Ð¸Ñ Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ð¹' + label_helpdesk_total_replies: Общее количеÑтво ответов + label_helpdesk_hour: "ч" + label_helpdesk_minute: "м" + label_helpdesk_report_previous: 'Предыдущее значение' + label_helpdesk_report_deviation: 'Отклонение' + +# 3.0.5 + + label_helpdesk_busiest_time_of_day: Загрузка в течение Ð´Ð½Ñ + label_helpdesk_report_names_busiest_time_of_day: Загрузка в течение Ð´Ð½Ñ + label_helpdesk_busiest_time_of_day_interval_6_8h: '6 - 8ч' + label_helpdesk_busiest_time_of_day_interval_8_10h: '8 - 10ч' + label_helpdesk_busiest_time_of_day_interval_10_12h: '10 - 12ч' + label_helpdesk_busiest_time_of_day_interval_12_14h: '12 - 14ч' + label_helpdesk_busiest_time_of_day_interval_14_16h: '14 - 16ч' + label_helpdesk_busiest_time_of_day_interval_16_18h: '16 - 18ч' + label_helpdesk_busiest_time_of_day_interval_18_20h: '18 - 20ч' + label_helpdesk_busiest_time_of_day_interval_20_22h: '20 - 22ч' + label_helpdesk_busiest_time_of_day_interval_22_0h: '22 - 0ч' + label_helpdesk_busiest_time_of_day_interval_0_2h: '0 - 2ч' + label_helpdesk_busiest_time_of_day_interval_2_4h: '2 - 4ч' + label_helpdesk_busiest_time_of_day_interval_4_6h: '4 - 6ч' + label_helpdesk_busiest_time_of_day_new_tickets: Созданных задач + label_helpdesk_busiest_time_of_day_new_contacts: Созданных контактов + label_helpdesk_number_of_tickets: КоличеÑтво обращений + label_helpdesk_open_tickets: ЕÑть открытые Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ + label_helpdesk_busiest_time_of_day_total_incoming: Ð’Ñего входÑщих + label_helpdesk_autoclose_tickets_after: ÐвтоматичеÑки закрывать Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ð¾Ñле + label_helpdesk_autoclose_from_status: ÐаходащиеÑÑ Ð² ÑтатуÑе + label_helpdesk_autoclose_to_status: Переводить в ÑÑ‚Ð°Ñ‚ÑƒÑ + label_helpdesk_hours: дней + label_helpdesk_days: чаÑов + label_helpdesk_autoclose_template: Шаблон пиÑьма об автозакрытии + label_helpdesk_autoclosed_ticket: Обращение было автоматичеÑки закрыто + label_helpdesk_cc_address: CC адреÑа + + label_helpdesk_reply: Ответить diff --git a/plugins/redmine_contacts_helpdesk/config/locales/sk.yml b/plugins/redmine_contacts_helpdesk/config/locales/sk.yml new file mode 100644 index 0000000..70c6c40 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/sk.yml @@ -0,0 +1,113 @@ +# English strings go here for Rails i18n +sk: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Å ablóna automatickej odpovede + text_helpdesk_answer_macros: Dostupné makrá %{macro} + label_is_send_mail: PoslaÅ¥ poznámku + label_send_auto_answer: PoslaÅ¥ auto odpoveÄ + label_received_from: Prijaté od + label_sent_to: Poslané na + project_module_contacts_helpdesk: Helpdesk + permission_send_response: PoslaÅ¥ odpoveÄ kontaktu + permission_edit_helpdesk_settings: UpraviÅ¥ helpdesk nastavenia + label_not_create_contacts: Povolené kontakty + field_created_contact_tags: PridaÅ¥ Å¡títok po vytvorení + label_helpdesk_save_as_attachment: UložiÅ¥ vÅ¡etky prílohy + +#1.0.2 + label_helpdesk_assign_author: Odosielateľ ako autor úlohy + +#1.0.3 + label_helpdesk_answered_status: Stav odpovede tiketu + label_helpdesk_reopen_status: Stav znovu-otvorenia tiketu + label_helpdesk_tracker: Tiket sledovaÄ + label_helpdesk_assigned_to: PriradiÅ¥ tiket + label_helpdesk_lifetime: ŽivotnosÅ¥ tiketu + label_helpdesk_server_settings: Nastavenia mail serveru + label_helpdesk_protocol: Protokol + label_helpdesk_host: Server + label_helpdesk_port: Port + label_helpdesk_username: Užívateľské meno + label_helpdesk_password: Heslo + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP adresár + label_helpdesk_move_on_success: Presunuté úspeÅ¡ne + label_helpdesk_move_on_failure: Presun zlyhal + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: VymazaÅ¥ nevybavené správy + label_helpdesk_first_answer_template: Å ablóna + label_helpdesk_test_connection: Test spojenia + label_helpdesk_get_mail: PrevziaÅ¥ mail + label_helpdesk_get_mail_success: "Spracovaný(ch) %{count} email(ov)" + label_helpdesk_css: Å tyly emailu (CSS) + label_helpdesk_original: Originál + +#1.0.4 + label_helpdesk_blacklist: Nepovolené emaili + label_helpdesk_contact_activity: Zákaznikov predoÅ¡lí tiket + permission_view_helpdesk_tickets: ZobraziÅ¥ helpdesk tikety + label_helpdesk_last_message: Posledná správa + label_helpdesk_enable_modules: Modul Kontakty a Úlohy musia byÅ¥ povolené pre tento projekt + label_helpdesk_contact: Zákazníkov profil + label_helpdesk_show_contact_card: ZobraziÅ¥ zákazníkov profil + +#2.0.0 + label_helpdesk_send_note_by_default: Å tandartne poslaÅ¥ poznámku + label_helpdesk_ticket_attributes: Atribúty tiketu + label_helpdesk_to_address: Na adresu + label_helpdesk_from_address: Od adresy + label_helpdesk_ticket_date: Dátum tiketu + label_helpdesk_customer: Zákazník + label_helpdesk_from: Od + label_helpdesk_cc: Cc + label_helpdesk_bcc: Bcc + label_helpdesk_ticket_plural: Helpdesk tikety + label_helpdesk_template: Helpdesk Å¡ablóna + label_helpdesk_general: Hlavná + permission_edit_helpdesk_tickets: UpraviÅ¥ tiket info + +#2.1.0 + label_helpdesk_ticket: Helpdesk tiket + label_helpdesk_spam: Nevyžiadaný mail + label_helpdesk_add_cc: PridaÅ¥ Cc/Bcc + label_helpdesk_add_contact_notes: PridaÅ¥ prvý email ako poznámka kontaktu + text_helpdesk_customer_count: "%{count} zákazník(ov)" + text_helpdesk_ticket_count: "%{count} tiket(ov)" + label_helpdesk_tickets_report: Helpdesk report tiketov + label_helpdesk_staff_report: Helpdesk report pracovníkov + label_helpdesk_tickets_email: Email + label_helpdesk_tickets_phone: Telefón + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Zdroj tiketu + label_helpdesk_ticket_time: ÄŒas tiketu + label_helpdesk_save_cc: UložiÅ¥ CC ako kontakty + permission_view_helpdesk_reports: ZobraziÅ¥ reporty + +#2.1.2 + label_helpdesk_answer_template: Å ablóna odpovede + +#2.1.3 + label_helpdesk_previous_tickets: PredoÅ¡lý tiket + label_helpdesk_public_tickets: PovoliÅ¥ verejné tikety + label_helpdesk_public_title: Stránka verejných tiketov + label_helpdesk_settings_public: Verejné tikety + label_helpdesk_public_comments: PovoliÅ¥ pridaÅ¥ komentár + label_helpdesk_public_show_spent_time: ZobraziÅ¥ strávený Äas + label_helpdesk_public_link: Verejná linka + +#2.1.4 + label_helpdesk_ticket_new: Nový tiket + label_helpdesk_report_plural: Helpdesk reporty + +#2.2.0 + label_helpdesk_contact_company: Zakazníkova spoloÄnosÅ¥ + label_helpdesk_canned_response_plural: Predlohy odpovede + label_helpdesk_new_canned_response: Nová predloha odpovede + label_helpdesk_canned_response: Predloha odpovede + permission_manage_public_canned_responses: Manažment verejných predloh odpovedí + permission_manage_canned_responses: Manažment predloh odpovedí + +#2.2.1 + my_helpdesk_tickets: Moje helpdesk tikety + label_helpdesk_view_all_tickets: ZobraziÅ¥ vÅ¡etky tikety diff --git a/plugins/redmine_contacts_helpdesk/config/locales/sr.yml b/plugins/redmine_contacts_helpdesk/config/locales/sr.yml new file mode 100644 index 0000000..7c63e23 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/sr.yml @@ -0,0 +1,145 @@ +# Serbian strings go here for Rails i18n +sr: + label_helpdesk: ХелпдеÑк + label_helpdesk_auto_answer_template: Шаблон аутоматÑког одговарања мејлом + text_helpdesk_answer_macros: ДоÑтупни macros %{macro} + label_is_send_mail: ПоÑлати обавјештење + label_send_auto_answer: ÐутоматÑко Ñлање одговора + label_received_from: Примљено од + label_sent_to: ПоÑлано + project_module_contacts_helpdesk: ХелппдеÑк + permission_send_response: ПоÑлати одговор контакту + permission_edit_helpdesk_settings: Уредити подешавања за хелпдеÑк + label_not_create_contacts: ВајтлиÑÑ‚ контакта + field_created_contact_tags: Додати обавјештења након креирања + label_helpdesk_save_as_attachment: Счувати мејлове као прилоге/датотеке + +#1.0.2 + label_helpdesk_assign_author: Пошиљалц поÑтавља предмет аутора + +#1.0.3 + label_helpdesk_answered_status: Одговорити на ÑÑ‚Ð°Ñ‚ÑƒÑ Ñ‚Ð¸ÐºÐµÑ‚Ð° + label_helpdesk_reopen_status: Поново отворити тикет ÑÑ‚Ð°Ñ‚ÑƒÑ + label_helpdesk_tracker: Претраживач тикета + label_helpdesk_assigned_to: Додјелти тикет + label_helpdesk_lifetime: Трајање тикета + label_helpdesk_server_settings: Подешавања мејла + label_helpdesk_protocol: Протокол + label_helpdesk_host: ХоÑÑ‚ + label_helpdesk_port: Порт + label_helpdesk_username: КориÑничко име + label_helpdesk_password: Лозинка + label_helpdesk_ssl: ССЛ + label_helpdesk_imap_folder: ИМÐП фолдер + label_helpdesk_move_on_success: Прећи на уÑпјех + label_helpdesk_move_on_failure: Прећи на пад + label_helpdesk_apop: ÐПОП + label_helpdesk_delete_unprocessed: ИзбриÑати не прегледане/не обрађене поруке + label_helpdesk_first_answer_template: Шаблон + label_helpdesk_test_connection: ТеÑÑ‚ конекције + label_helpdesk_get_mail: Примити мејл + label_helpdesk_get_mail_success: "Прегледан %{count} мејл(ова)" + label_helpdesk_css: Образац имејла (ЦСС) + label_helpdesk_original: Оригинал + +#1.0.4 + label_helpdesk_blacklist: БлеклиÑÑ‚ мејлови + label_helpdesk_contact_activity: Тикети претходног купца + permission_view_helpdesk_tickets: ПриÑтуп кориÑним информацијама о тикету + label_helpdesk_last_message: ПоÑљедња порука + label_helpdesk_enable_modules: Контакти и предемт модули би требали бити омогућени за овај пројекат + label_helpdesk_contact: Профил купца + label_helpdesk_show_contact_card: Приказ профила купца + +#2.0.0 + label_helpdesk_send_note_by_default: ПоÑалти обавјештење + label_helpdesk_ticket_attributes: Обиљежија тикета + label_helpdesk_to_address: Унијети адреÑу + label_helpdesk_from_address: ÐдреÑа Ñа које је поÑлано + label_helpdesk_ticket_date: Датум тикета + label_helpdesk_customer: Купац + label_helpdesk_from: Од + label_helpdesk_cc: Цц + label_helpdesk_bcc: Ð’ÑÑ + label_helpdesk_ticket_plural: Тикети хелпдеÑка + label_helpdesk_template: Шаблон хелпдеÑка + label_helpdesk_general: Општи + permission_edit_helpdesk_tickets: Уредити информације о тикету + +#2.1.0 + label_helpdesk_ticket: Тикет хелпдеÑка + label_helpdesk_spam: Ðеважан мејл + label_helpdesk_add_cc: Додати Cc/Bcc + label_helpdesk_add_contact_notes: Додати први мејл као обавјештење контакта + text_helpdesk_customer_count: "%{count} купац(и)" + text_helpdesk_ticket_count: "%{count} тикет(и)" + label_helpdesk_tickets_report: ХелпдеÑк тикет извјештај + label_helpdesk_staff_report: ХелпдеÑк извјештај + label_helpdesk_tickets_email: Имејл + label_helpdesk_tickets_phone: Телефон + label_helpdesk_tickets_web: Веб + label_helpdesk_tickets_twitter: Твитер + label_helpdesk_ticket_source: Извор тикета + label_helpdesk_ticket_time: Вријема тикета + label_helpdesk_save_cc: Сачувати CC као контакте + permission_view_helpdesk_reports: Преглед извјештаја + +#2.1.2 + label_helpdesk_answer_template: Шаблон за одговор + +#2.1.3 + label_helpdesk_previous_tickets: Претходни тикети + label_helpdesk_public_tickets: Омогућити јавне тикете + label_helpdesk_public_title: ÐаÑлов јавне Ñтранице + label_helpdesk_settings_public: Јавни тикети + label_helpdesk_public_comments: Дозволити додатне коментаре + label_helpdesk_public_show_spent_time: Приказ потрошеног времена + label_helpdesk_public_link: Линк + +#2.1.4 + label_helpdesk_ticket_new: Ðови тикет + label_helpdesk_report_plural: Извјештаји хелпдеÑка + +#2.2.0 + label_helpdesk_contact_company: Компанија купца + label_helpdesk_canned_response_plural: Механички одговор + label_helpdesk_new_canned_response: Ðови механички одговор + label_helpdesk_canned_response: Механички одговор + permission_manage_public_canned_responses: Управљати јавним механичкким одговорима + permission_manage_canned_responses: Управљати механичким одговорима + +#2.2.1 + my_helpdesk_tickets: Моји хелпдеÑк тикети + label_helpdesk_view_all_tickets: Видјети Ñве тикете + +#2.2.3 + label_helpdesk_tickets_conversation: Конверзација + +#2.2.4 + label_helpdesk_required_custom_fields_error: Контакти не би требали захтјевати додатна поља + +#2.2.6 + label_helpdesk_last_message_date: Ðпдејт тикета + label_helpdesk_ago: "%{value} од прије" + +#2.2.8 + label_helpdesk_to: За + label_helpdesk_send_as: ПоÑлано као + label_helpdesk_not_send: "(Ðије поÑлано)" + label_helpdesk_send_as_notification: ПоÑлано као обавјештење + label_helpdesk_send_as_message: ПоÑлано као почетна порука + +#2.2.9 + label_helpdesk_incoming_mail_server: Сервер долазећих имејлова + label_helpdesk_outgoing_mail_server: Сервер одлазећих имјелова + label_helpdesk_smtp_use_default_settings: КориÑти подразумјевана подешавања. + label_helpdesk_smtp_server: СМТП Ñервер + label_helpdesk_smtp_tls: ТСЛ + label_helpdesk_authentication: Метод аутентикације + label_helpdesk_authentication_plain: Очигледно + label_helpdesk_authentication_login: Пријавити Ñе + label_helpdesk_authentication_cram_md5: МД5 Изазов-Одговор + label_helpdesk_smtp_domain: Домен + + #2.2.10 + label_helpdesk_email_sending_problems: Проблеми Ñа Ñлањем имејла diff --git a/plugins/redmine_contacts_helpdesk/config/locales/sv.yml b/plugins/redmine_contacts_helpdesk/config/locales/sv.yml new file mode 100644 index 0000000..5406833 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/sv.yml @@ -0,0 +1,148 @@ +# Swedish translation based on RedmineCRM +# Helpdesk plugin version 2.2.13 +# Created by Khedron Wilk (khedron.wilk@gmail.com) 2014 Dec 09 + +sv: + label_helpdesk: Helpdesk + label_helpdesk_auto_answer_template: Autosvarsmall + text_helpdesk_answer_macros: Tillgängliga makron %{macro} + label_is_send_mail: Skicka anteckning + label_send_auto_answer: Skicka autosvar + label_received_from: Mottagen frÃ¥n + label_sent_to: Skickad till + project_module_contacts_helpdesk: Helpdesk + permission_send_response: Skicka svar till kontakt + permission_edit_helpdesk_settings: Redigera Helpdesk-inställningar + label_not_create_contacts: Vitlista för kontakter + field_created_contact_tags: Lägg till etiketter efter skapa + label_helpdesk_save_as_attachment: Spara e-post som bilaga + +#1.0.2 + label_helpdesk_assign_author: Gör avsändare till författare av ärende + +#1.0.3 + label_helpdesk_answered_status: Status för besvarat ärende + label_helpdesk_reopen_status: Status för Ã¥teröppnat ärende + label_helpdesk_tracker: Ärendetyp + label_helpdesk_assigned_to: Tilldela ärende till + label_helpdesk_lifetime: Giltighetstid för ärende + label_helpdesk_server_settings: Serverinställningar e-posts + label_helpdesk_protocol: Protokoll + label_helpdesk_host: Värd + label_helpdesk_port: Port + label_helpdesk_username: Användarnamn + label_helpdesk_password: Lösenord + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP-folder + label_helpdesk_move_on_success: Flytta till vid framgÃ¥ng + label_helpdesk_move_on_failure: Flytta till vid misslyckande + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: Radera obearbetade meddelanden + label_helpdesk_first_answer_template: Mall + label_helpdesk_test_connection: Test förbindelse + label_helpdesk_get_mail: Hämta e-post + label_helpdesk_get_mail_success: "Bearbetat %{count} brev" + label_helpdesk_css: Stylesheet (CSS) för e-post + label_helpdesk_original: Original + +#1.0.4 + label_helpdesk_blacklist: Blockeringslista för e-postmeddelanden + label_helpdesk_contact_activity: Kundens tidigare ärenden + permission_view_helpdesk_tickets: Visa Helpdesk-ärenden + label_helpdesk_last_message: Senaste meddelandet + label_helpdesk_enable_modules: Modulerna Kontakter och Ärenden bör vara aktiverade för detta projekt + label_helpdesk_contact: Kundprofil + label_helpdesk_show_contact_card: Visa kundprofiler + +#2.0.0 + label_helpdesk_send_note_by_default: Skicka anteckning som standard + label_helpdesk_ticket_attributes: Ärendeegenskaper + label_helpdesk_to_address: Till-adress + label_helpdesk_from_address: FrÃ¥n-adresss + label_helpdesk_ticket_date: Ärendedatum + label_helpdesk_customer: Kund + label_helpdesk_from: FrÃ¥n + label_helpdesk_cc: Kopia + label_helpdesk_bcc: Dold kopia + label_helpdesk_ticket_plural: Helpdesk-ärenden + label_helpdesk_template: Helpdesk-mall + label_helpdesk_general: Allmänt + permission_edit_helpdesk_tickets: Redigera ärendeinfo + +#2.1.0 + label_helpdesk_ticket: Helpdesk-ärende + label_helpdesk_spam: Skräppost + label_helpdesk_add_cc: Lägg till Kopia/Dold kopiac + label_helpdesk_add_contact_notes: Lägg till första e-post som kontakt + text_helpdesk_customer_count: "%{count} kund(er)" + text_helpdesk_ticket_count: "%{count} ärende(n)" + label_helpdesk_tickets_report: Rapport Helpdesk-ärenden + label_helpdesk_staff_report: Rapport medarbetare Helpdesk-ärenden + label_helpdesk_tickets_email: E-post + label_helpdesk_tickets_phone: Telefon + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Ärendekälla + label_helpdesk_ticket_time: Ärendetid + label_helpdesk_save_cc: Spara Kopia som kontakter + permission_view_helpdesk_reports: Visa rapporter + +#2.1.2 + label_helpdesk_answer_template: Svarsmall + +#2.1.3 + label_helpdesk_previous_tickets: Tidigare ärenden + label_helpdesk_public_tickets: Aktivera publika ärenden + label_helpdesk_public_title: Titel publik sida + label_helpdesk_settings_public: Publika ärenden + label_helpdesk_public_comments: TillÃ¥t kommentarer + label_helpdesk_public_show_spent_time: Visa använd tid + label_helpdesk_public_link: Publik länk + +#2.1.4 + label_helpdesk_ticket_new: Nytt ärende + label_helpdesk_report_plural: Helpdesk-rapporter + +#2.2.0 + label_helpdesk_contact_company: Kundföretag + label_helpdesk_canned_response_plural: Svarsmallar + label_helpdesk_new_canned_response: Ny svarsmall + label_helpdesk_canned_response: Svarsmall + permission_manage_public_canned_responses: Hantera publika svarsmallar + permission_manage_canned_responses: Hantera svarsmallar + +#2.2.1 + my_helpdesk_tickets: Mina Helpdesk-ärenden + label_helpdesk_view_all_tickets: Visa alla ärenden + +#2.2.3 + label_helpdesk_tickets_conversation: Konversation + +#2.2.4 + label_helpdesk_required_custom_fields_error: Kontakter bör inte ha de anpassade obligatoriska fälten + +#2.2.6 + label_helpdesk_last_message_date: Ärendet uppdaterat + label_helpdesk_ago: "%{value} sen" + +#2.2.8 + label_helpdesk_to: Till + label_helpdesk_send_as: Skicka som + label_helpdesk_not_send: "(Inte skickad)" + label_helpdesk_send_as_notification: Skicka som anteckning + label_helpdesk_send_as_message: Skicka som intialt meddelande + +#2.2.9 + label_helpdesk_incoming_mail_server: Inkommande e-postserver + label_helpdesk_outgoing_mail_server: UtgÃ¥ende e-postserver + label_helpdesk_smtp_use_default_settings: Använd standardinställningar + label_helpdesk_smtp_server: SMTP-server + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Verifieringsmetod + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Domän + + #2.2.10 + label_helpdesk_email_sending_problems: Problem att skicka e-post diff --git a/plugins/redmine_contacts_helpdesk/config/locales/tr.yml b/plugins/redmine_contacts_helpdesk/config/locales/tr.yml new file mode 100644 index 0000000..c90b4eb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/tr.yml @@ -0,0 +1,174 @@ +# English strings go here for Rails i18n +tr: + label_helpdesk: Yardım Masası + label_helpdesk_auto_answer_template: Otomatik yanıt eposta taslağı + text_helpdesk_answer_macros: Kullanılabilir makrolar %{macro} + label_is_send_mail: Not gönder + label_send_auto_answer: Otomatik yanıt gönder + label_received_from: Received from + label_sent_to: Sent to + project_module_contacts_helpdesk: Yardım Masası + permission_send_response: KiÅŸilere yanıt gönderme + permission_edit_helpdesk_settings: Hepldesk ayarlarını düzenleme + label_not_create_contacts: Yeni kiÅŸi kabul etme + field_created_contact_tags: oluÅŸturulan kayda eklenecek etiketler + label_helpdesk_save_as_attachment: Orjinal epostayı ek olarak kaydet + +#1.0.2 + label_helpdesk_assign_author: Epostayı göndereni iÅŸi raporlayan yap + +#1.0.3 + label_helpdesk_answered_status: Yanıtlanan kaydın durumu + label_helpdesk_reopen_status: Yeniden açılan kaydın durumu + label_helpdesk_tracker: Kayıt iÅŸ türü + label_helpdesk_assigned_to: Kaydı ata + label_helpdesk_lifetime: Kayıt ömrü + label_helpdesk_server_settings: Eposta sunucusu ayarları + label_helpdesk_protocol: Protokol + label_helpdesk_host: Sunucu + label_helpdesk_port: Port + label_helpdesk_username: giriÅŸ adı + label_helpdesk_password: Åžifre + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP klasörü + label_helpdesk_move_on_success: BaÅŸarılı olanları taşı + label_helpdesk_move_on_failure: BaÅŸarısız olanları taşı + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: iÅŸlenemeyen mesajları sil + label_helpdesk_first_answer_template: Taslak + label_helpdesk_test_connection: BaÄŸlantı test et + label_helpdesk_get_mail: Eposta al + label_helpdesk_get_mail_success: "%{count} eposta iÅŸlendi" + label_helpdesk_css: Eposta stili (CSS) + label_helpdesk_original: Orjinal + +#1.0.4 + label_helpdesk_blacklist: Engellenen eposta adresleri + label_helpdesk_contact_activity: Müşterinin önceki kayıtları + permission_view_helpdesk_tickets: Yardım Masası kayıtlarını görme + label_helpdesk_last_message: Son mesaj + label_helpdesk_enable_modules: Contacts ve İş takibi modülleri aktif olmalı bu proje için + label_helpdesk_contact: Müşteri profili + label_helpdesk_show_contact_card: Müşteri profilini göster + +#2.0.0 + label_helpdesk_send_note_by_default: varsayılan olarak notu yolla + label_helpdesk_ticket_attributes: Kayıt nitelikleri + label_helpdesk_to_address: Kime adresi + label_helpdesk_from_address: Kimden adresi + label_helpdesk_ticket_date: Kayıt adresi + label_helpdesk_customer: Müşteri + label_helpdesk_from: Kimden + label_helpdesk_cc: Bilgi + label_helpdesk_bcc: Gizli + label_helpdesk_ticket_plural: Yardım Masası kayıtları + label_helpdesk_template: Yardım Masası ÅŸablonu + label_helpdesk_general: Genel + permission_edit_helpdesk_tickets: Kayıt bilgilerini düzenle + +#2.1.0 + label_helpdesk_ticket: Yardım Masası kaydı + label_helpdesk_spam: İstenmeyen eposta + label_helpdesk_add_cc: Cc/Bcc leri ekle + label_helpdesk_add_contact_notes: İlk epostayı kiÅŸilere kaydet + text_helpdesk_customer_count: "%{count} müşteri" + text_helpdesk_ticket_count: "%{count} kayıt" + label_helpdesk_tickets_report: Yardım Masası kayıtlar raporu + label_helpdesk_staff_report: Yardım Masası sorumlu raporu + label_helpdesk_tickets_email: Eposta + label_helpdesk_tickets_phone: Telefon + label_helpdesk_tickets_web: Web + label_helpdesk_tickets_twitter: Twitter + label_helpdesk_ticket_source: Kayıt kaynağı + label_helpdesk_ticket_time: Kayıt zamanı + label_helpdesk_save_cc: CC dekileri kiÅŸilere kaydet + permission_view_helpdesk_reports: Raporları görme + +#2.1.2 + label_helpdesk_answer_template: Yanıt ÅŸablonu + +#2.1.3 + label_helpdesk_previous_tickets: Önceki kayıtlar + label_helpdesk_public_tickets: Umumi sayfaları aç + label_helpdesk_public_title: Umumi sayfa baÅŸlığı + label_helpdesk_settings_public: Umumi Sayfa + label_helpdesk_public_comments: Not eklemeye izin ver + label_helpdesk_public_show_spent_time: Harcanan zamanı göster + label_helpdesk_public_link: Umumi sayfa baÄŸlantısı + +#2.1.4 + label_helpdesk_ticket_new: Yeni kayıt + label_helpdesk_report_plural: Yardım Masası raporları + +#2.2.0 + label_helpdesk_contact_company: Müşteri ÅŸirketi + label_helpdesk_canned_response_plural: Sık kullanılan yanıtlar + label_helpdesk_new_canned_response: Yeni sık kullanılan yanıt + label_helpdesk_canned_response: Sık kullanılan yanıt + permission_manage_public_canned_responses: Genel sık kullanılan yanıtları yönet + permission_manage_canned_responses: Sık kullanılan yanıtları yönet + +#2.2.1 + my_helpdesk_tickets: Benim Yardım Masası kayıtlarım + label_helpdesk_view_all_tickets: Tüm kayıtları göster + +#2.2.3 + label_helpdesk_tickets_conversation: Görüşme + +#2.2.4 + label_helpdesk_required_custom_fields_error: Contacts should not have the required custom fields + +#2.2.6 + label_helpdesk_last_message_date: Kayıt güncellendi + label_helpdesk_ago: "%{value} önce" + +#2.2.8 + label_helpdesk_to: Kime + label_helpdesk_send_as: Gönderme biçimi + label_helpdesk_not_send: "(gönderme)" + label_helpdesk_send_as_notification: Bilgilendirme olarak gönder + label_helpdesk_send_as_message: BaÅŸlangıç mesajı olarak gönder + +#2.2.9 + label_helpdesk_incoming_mail_server: Gelen posta sunucusu + label_helpdesk_outgoing_mail_server: Giden posta sunucusu + label_helpdesk_smtp_use_default_settings: Varsayılanı kullan + label_helpdesk_smtp_server: SMTP sunucu + label_helpdesk_smtp_tls: TLS + label_helpdesk_authentication: Kimlik denetim metodu + label_helpdesk_authentication_plain: Plain + label_helpdesk_authentication_login: Login + label_helpdesk_authentication_cram_md5: MD5 Challenge-Response + label_helpdesk_smtp_domain: Domain + + #2.2.10 + label_helpdesk_email_sending_problems: Eposta gönderme problemleri + + #2.3.0 + label_helpdesk_ticket_reaction_time: Tepki süresi + label_helpdesk_ticket_first_response_time: İlk yanıt süresi + label_helpdesk_ticket_resolve_time: Çözülme süresi + label_helpdesk_ticket_last_response_time: Cevap bekleme + field_helpdesk_ticket: Yardım masası bilet + text_helpdesk_to_address_cant_be_blank: Kime adresi boÅŸ olamaz + text_helpdesk_message_body_cant_be_blank: Mesaj içeriÄŸi boÅŸ olamaz + text_helpdesk_from_address_cant_be_blank: Kimden adresi boÅŸ olamaz + + #2.4.0 + label_helpdesk_vote: Oylama + label_helpdesk_vote_settings: Oylamaya izin ver + label_helpdesk_vote_comment_settings: Yorum yapılmasına izin ver + label_helpdesk_mark: Lütfen bu iÅŸi derecelendirin! + label_helpdesk_mark_awesome: MüthiÅŸ + label_helpdesk_mark_justok: İyi + label_helpdesk_mark_notgood: İyi deÄŸil + label_helpdesk_vote_comment_placeholder: yorum bırakma + label_helpdesk_submit: Gönder + label_helpdesk_vote_thank: Oylama için teÅŸekkürler! + label_helpdesk_close_page: Sayfayı kapat + label_helpdesk_contact_vote: Müşteri oylaması + label_helpdesk_vote: Oylama + label_helpdesk_vote_comment: Oy yorumu + + field_vote: Oylama + field_vote_comment: Oy yorumu diff --git a/plugins/redmine_contacts_helpdesk/config/locales/zh.yml b/plugins/redmine_contacts_helpdesk/config/locales/zh.yml new file mode 100644 index 0000000..1f2edd9 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/locales/zh.yml @@ -0,0 +1,233 @@ +# English strings go here for Rails i18n +zh: + label_helpdesk: 支æŒä¸­å¿ƒ + label_helpdesk_auto_answer_template: 自动回å¤é‚®ä»¶æ¨¡æ¿ + text_helpdesk_answer_macros: å¯ç”¨çš„å® %{macro} + label_is_send_mail: å‘é€å¤‡å¿˜ + label_send_auto_answer: å‘é€è‡ªåŠ¨å›žå¤ + label_received_from: 接å—自 + label_sent_to: å‘é€åˆ° + project_module_contacts_helpdesk: 支æŒä¸­å¿ƒ + permission_send_response: ç»™è”系人å‘é€å›žåº” + permission_edit_helpdesk_settings: 编辑支æŒä¸­å¿ƒè®¾ç½® + label_not_create_contacts: è”系人白åå• + field_created_contact_tags: 添加标签åŽåˆ›å»º + label_helpdesk_save_as_attachment: ä¿å­˜é‚®ä»¶é™„ä»¶ + +#1.0.2 + label_helpdesk_assign_author: 设置å‘件人为问题的作者 + +#1.0.3 + label_helpdesk_answered_status: 回答工å•çŠ¶æ€ + label_helpdesk_reopen_status: 釿–°æ‰“开工å•çŠ¶æ€ + label_helpdesk_tracker: å·¥å•追踪 + label_helpdesk_assigned_to: 分é…å·¥å•ç»™ + label_helpdesk_lifetime: å·¥å•生命周期 + label_helpdesk_server_settings: 邮件æœåŠ¡å™¨è®¾ç½® + label_helpdesk_protocol: åè®® + label_helpdesk_host: 主机 + label_helpdesk_port: ç«¯å£ + label_helpdesk_username: 用户å + label_helpdesk_password: å¯†ç  + label_helpdesk_ssl: SSL + label_helpdesk_imap_folder: IMAP 文件夹 + label_helpdesk_move_on_success: 移动æˆåŠŸ + label_helpdesk_move_on_failure: 移动失败 + label_helpdesk_apop: APOP + label_helpdesk_delete_unprocessed: 删除未处ç†çš„邮件 + label_helpdesk_first_answer_template: æ¨¡æ¿ + label_helpdesk_test_connection: 测试连接 + label_helpdesk_get_mail: 获å–邮件 + label_helpdesk_get_mail_success: "å·²å¤„ç† %{count} 邮件(s)" + label_helpdesk_css: é‚®ä»¶æ ·å¼ (CSS) + label_helpdesk_original: 原 + +#1.0.4 + label_helpdesk_blacklist: 邮件黑åå• + label_helpdesk_contact_activity: è”系人活动 + permission_view_helpdesk_tickets: 查看支æŒä¸­å¿ƒå·¥å• + label_helpdesk_last_message: 最åŽçš„ä¿¡æ¯ + label_helpdesk_enable_modules: è”系人和问题模å—应该在这个项目å¯ç”¨ + label_helpdesk_contact: 支æŒä¸­å¿ƒè”系人 + label_helpdesk_show_contact_card: 显示客户档案 + +#2.0.0 + label_helpdesk_send_note_by_default: 默认å‘é€å¤‡å¿˜ + label_helpdesk_ticket_attributes: å·¥å•属性 + label_helpdesk_to_address: é‚®å¯„åœ°å€ + label_helpdesk_from_address: å¯„ä¿¡åœ°å€ + label_helpdesk_ticket_date: 工啿—¥æœŸ + label_helpdesk_customer: 客户 + label_helpdesk_from: æ¥è‡ª + label_helpdesk_cc: æŠ„é€ + label_helpdesk_bcc: å¯†ä»¶æŠ„é€ + label_helpdesk_ticket_plural: 支æŒä¸­å¿ƒå·¥å• + label_helpdesk_template: 支æŒä¸­å¿ƒæ¨¡æ¿ + label_helpdesk_general: 通用 + permission_edit_helpdesk_tickets: 编辑工å•ä¿¡æ¯ + +#2.1.0 + label_helpdesk_ticket: 支æŒä¸­å¿ƒå·¥å• + label_helpdesk_spam: 垃圾邮件 + label_helpdesk_add_cc: 添加抄é€/å¯†é€ + label_helpdesk_add_contact_notes: 首先添加电å­é‚®ä»¶ä½œä¸ºè”系人备注 + text_helpdesk_customer_count: "%{count} 客户(s)" + text_helpdesk_ticket_count: "%{count} å·¥å•(s)" + label_helpdesk_tickets_report: 支æŒä¸­å¿ƒå·¥å•报告 + label_helpdesk_staff_report: 支æŒä¸­å¿ƒå·¥ä½œäººå‘˜æŠ¥å‘Š + label_helpdesk_tickets_email: 电å­é‚®ä»¶ + label_helpdesk_tickets_phone: ç”µè¯ + label_helpdesk_tickets_web: 网站 + label_helpdesk_tickets_twitter: å¾®åš + label_helpdesk_ticket_source: 工啿º + label_helpdesk_ticket_time: 工啿—¶é—´ + label_helpdesk_save_cc: ä¿å­˜CC为è”系人 + permission_view_helpdesk_reports: 查看报告 + +#2.1.2 + label_helpdesk_answer_template: å›žç­”æ¨¡æ¿ + +#2.1.3 + label_helpdesk_previous_tickets: è¿‡å¾€å·¥å• + label_helpdesk_public_tickets: å¯ç”¨å…¬å¼€å·¥å• + label_helpdesk_public_title: 公开工啿 ‡é¢˜ + label_helpdesk_settings_public: å…¬å¼€å·¥å• + label_helpdesk_public_comments: å…许添加注释 + label_helpdesk_public_show_spent_time: 显示所花费的时间 + label_helpdesk_public_link: 公开链接 + +#2.1.4 + label_helpdesk_ticket_new: æ–°å·¥å• + label_helpdesk_report_plural: 支æŒä¸­å¿ƒæŠ¥å‘Š + +#2.2.0 + label_helpdesk_contact_company: 支æŒä¸­å¿ƒå…¬å¸ + label_helpdesk_canned_response_plural: 标准化回应 + label_helpdesk_new_canned_response: 新标准化回应 + label_helpdesk_canned_response: 标准化回应 + permission_manage_public_canned_responses: 管ç†å…¬å¼€çš„æ ‡å‡†åŒ–回应 + permission_manage_canned_responses: ç®¡ç†æ ‡å‡†åŒ–回应 + +#2.2.1 + my_helpdesk_tickets: 我的支æŒä¸­å¿ƒå·¥å• + label_helpdesk_view_all_tickets: æŸ¥çœ‹æ‰€æœ‰å·¥å• + +#2.2.3 + label_helpdesk_tickets_conversation: ä¼šè¯ + +#2.2.4 + label_helpdesk_required_custom_fields_error: è”系人ä¸åº”该有必è¦çš„自定义字段 + +#2.2.6 + label_helpdesk_last_message_date: æ›´æ–°å·¥å• + label_helpdesk_ago: "%{value} 之å‰" + +#2.2.8 + label_helpdesk_to: 到 + label_helpdesk_send_as: å‘é€ + label_helpdesk_not_send: "(ä¸å‘é€)" + label_helpdesk_send_as_notification: å‘é€é€šçŸ¥ + label_helpdesk_send_as_message: å‘é€åˆå§‹ä¿¡æ¯ + +#2.2.9 + label_helpdesk_incoming_mail_server: 接收邮件æœåС噍 + label_helpdesk_outgoing_mail_server: å‘é€é‚®ä»¶æœåС噍 + label_helpdesk_smtp_use_default_settings: 使用默认设置 + label_helpdesk_smtp_server: SMTPæœåС噍 + label_helpdesk_smtp_tls: TLS加密 + label_helpdesk_authentication: èº«ä»½éªŒè¯æ–¹æ³• + label_helpdesk_authentication_plain: 明文 + label_helpdesk_authentication_login: 登录 + label_helpdesk_authentication_cram_md5: MD5 质询-å“应 + label_helpdesk_smtp_domain: 域 + + #2.2.10 + label_helpdesk_email_sending_problems: 电å­é‚®ä»¶å‘é€é—®é¢˜ + + #2.3.0 + label_helpdesk_ticket_reaction_time: å应时间 + label_helpdesk_ticket_first_response_time: 首次å“应时间 + label_helpdesk_ticket_resolve_time: 解决时间 + label_helpdesk_ticket_last_response_time: 等待å“应时间 + field_helpdesk_ticket: 支æŒä¸­å¿ƒå·¥å• + text_helpdesk_to_address_cant_be_blank: 地å€ä¸èƒ½ä¸ºç©º + text_helpdesk_message_body_cant_be_blank: 消æ¯ä¿¡æ¯ä¸èƒ½ä¸ºç©º + text_helpdesk_from_address_cant_be_blank: æ¥è‡ªåœ°å€ä¸èƒ½ä¸ºç©º + + #2.4.0 + label_helpdesk_vote: 投票 + label_helpdesk_vote_settings: å…许投票 + label_helpdesk_vote_comment_settings: å…许评论投票 + label_helpdesk_mark: 请给我们的工作打分! + label_helpdesk_mark_awesome: 挺ä¸é”™ + label_helpdesk_mark_justok: è¿˜å‡‘åˆ + label_helpdesk_mark_notgood: ä¸å¤ªè¡Œ + label_helpdesk_vote_comment_placeholder: å‘表评论 + label_helpdesk_submit: æäº¤ + label_helpdesk_vote_thank: 感谢您的投票! + label_helpdesk_close_page: å…³é—­é¡µé¢ + label_helpdesk_contact_vote: è”系人投票 + label_helpdesk_vote_comment: 投票评论 + + field_vote: 投票 + field_vote_comment: 投票评论 + + # 3.0.2 + label_helpdesk_assign_contact_user: 分é…è”系人用户 + label_helpdesk_create_private_tickets: åˆ›å»ºç§æœ‰å·¥å• + label_helpdesk_all: 所有 + + label_helpdesk_widget: å°å·¥å…· + label_helpdesk_widget_enable: å¼€å¯å°å·¥å…· + label_helpdesk_widget_available_projects: å¯ç”¨é¡¹ç›® + label_helpdesk_widget_no_available_projects: 你应该至少有一个项目å¯ç”¨æ¡Œé¢å°å·¥å…·æ¨¡å—用以激活å°å·¥å…· + label_helpdesk_widget_custom_fields: å¯è‡ªå®šä¹‰å­—段 + label_helpdesk_widget_activation_message: 下é¢çš„代ç éœ€è¦æ·»åŠ åœ¨é¡µé¢ä¸Šç”¨ä»¥æ¿€æ´»å°å·¥å…· + + label_helpdesk_widget_name: ä½ çš„åå­— + label_helpdesk_widget_email: é‚®ä»¶åœ°å€ + label_helpdesk_widget_subject: 主题 + label_helpdesk_widget_description: å·¥å•æè¿° + label_helpdesk_widget_create_ticket: åˆ›å»ºå·¥å• + label_helpdesk_widget_file_large: '抱歉, 太长了!' + label_helpdesk_widget_ticket_created: å·¥å•已创建 + label_helpdesk_widget_ticket_errors: "ä¸èƒ½æ·»åŠ ä¿¡æ¯ " + label_helpdesk_widget_ticket_error_details: 细节 + label_helpdesk_widget_ticket_error_description: æè¿°ä¸èƒ½ä¸ºç©º + + label_helpdesk_reports: 支æŒä¸­å¿ƒæŠ¥å‘Š + label_helpdesk_first_response_time: 第一次å“应时间 + label_helpdesk_report_names_first_response_time: 第一次å“应时间 + label_helpdesk_filter_time_interval: 报告时间间隔 + label_helpdesk_first_response_time_interval_0_1h: '0 - 1æ—¶' + label_helpdesk_first_response_time_interval_1_2h: '1 - 2æ—¶' + label_helpdesk_first_response_time_interval_2_4h: '2 - 4æ—¶' + label_helpdesk_first_response_time_interval_4_8h: '4 - 8æ—¶' + label_helpdesk_first_response_time_interval_8_12h: '8 - 12æ—¶' + label_helpdesk_first_response_time_interval_12_24h: '12 - 24æ—¶' + label_helpdesk_first_response_time_interval_24_48h: '24 - 48æ—¶' + label_helpdesk_first_response_time_interval_48_0h: '大于48æ—¶' + label_helpdesk_average_first_response_time: 'å¹³å‡ç¬¬ä¸€æ¬¡å“应时间' + label_helpdesk_average_responses_count: “关闭的å“应的平å‡è®¡æ•°â€ + label_helpdesk_average_time_to_close: 'å¹³å‡å·¥å•关闭时间' + label_helpdesk_total_replies: å›žå¤æ€»æ•° + label_helpdesk_hour: "å°æ—¶" + label_helpdesk_minute: "分钟" + label_helpdesk_report_previous: '上一页' + label_helpdesk_report_deviation: 'åå·®' + + label_helpdesk_busiest_time_of_day: 一天中最忙的时间 + label_helpdesk_report_names_busiest_time_of_day: 一天中最忙的时间 + label_helpdesk_busiest_time_of_day_interval_15_18h: '15 - 18æ—¶' + label_helpdesk_busiest_time_of_day_interval_18_21h: '18 - 21æ—¶' + label_helpdesk_busiest_time_of_day_interval_21_0h: '21 - 0æ—¶' + label_helpdesk_busiest_time_of_day_interval_0_3h: '0 - 3æ—¶' + label_helpdesk_busiest_time_of_day_interval_3_7h: '3 - 7æ—¶' + label_helpdesk_busiest_time_of_day_interval_7_10h: '7 - 10æ—¶' + label_helpdesk_busiest_time_of_day_interval_10_13h: '10 - 13æ—¶' + label_helpdesk_busiest_time_of_day_interval_13_15h: '13 - 15æ—¶' + label_helpdesk_busiest_time_of_day_new_tickets: æ–°å·¥å• + label_helpdesk_busiest_time_of_day_new_contacts: æ–°è”系人 + label_helpdesk_number_of_tickets: 工啿•° + label_helpdesk_open_tickets: å¼€å¯çš„å·¥å• + label_helpdesk_busiest_time_of_day_total_incoming: 传入总数 diff --git a/plugins/redmine_contacts_helpdesk/config/routes.rb b/plugins/redmine_contacts_helpdesk/config/routes.rb new file mode 100644 index 0000000..988fcc6 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/config/routes.rb @@ -0,0 +1,52 @@ +#custom routes for this plugin +resources :helpdesk_tickets, :only => [:edit, :destroy, :update] + +match "helpdesk_search" => "helpdesk_search#usage", :via => [:get] +match "helpdesk_search/issues" => "helpdesk_search#usage", :via => [:get] +match "helpdesk_search/contacts" => "helpdesk_search#usage", :via => [:get] +match "helpdesk_search/issues/:issue_id" => "helpdesk_search#ticket_by_issue", :via => [:get] +match "helpdesk_search/issues/:issue_id/ticket" => "helpdesk_search#ticket_by_issue", :via => [:get] +match "helpdesk_search/contacts/:contact_id/issues" => "helpdesk_search#issues_by_contact", :via => [:get] +match "helpdesk_search/issues/:issue_id/messages" => "helpdesk_search#messages_by_issue", :via => [:get] +match "helpdesk_search/contacts/:contact_id/timeline" => "helpdesk_search#contact_timeline", :via => [:get] + +resources :projects do + resources :canned_responses, :only => [:new, :create] +end + +resources :canned_responses do + collection do + post :add + end +end + +match "helpdesk_mailer" => "helpdesk_mailer#index",:via => [:get, :post] +match "helpdesk_mailer/get_mail" => "helpdesk_mailer#get_mail", :via => [:get, :post, :put] +match "helpdesk/save_settings" => "helpdesk#save_settings", :via => [:get, :post, :put ] +match "helpdesk/get_mail" => "helpdesk#get_mail", :via => [:get, :post, :put] +match "helpdesk/update_customer_email" => "helpdesk#update_customer_email", :via => [:get] +match "helpdesk/delete_spam" => "helpdesk#delete_spam", :via => [:delete] +match "helpdesk/email_note.:format" => "helpdesk#email_note", :via => [:get, :post] +match "helpdesk/create_ticket.:format" => "helpdesk#create_ticket", :via => [:get, :post] +match "helpdesk/show_original" => "helpdesk#show_original", :via => [:get, :post] +match '/projects/:project_id/helpdesk/reports/:report', :to => 'helpdesk_reports#show', :as => 'project_helpdesk_reports', :via => [:get] +match '/projects/:project_id/helpdesk/render_chart', :to => 'helpdesk_reports#render_chart', :as => 'project_helpdesk_render_chart', :via => [:get] +match 'helpdesk_widget/widget.:format' => 'helpdesk_widget#widget', :via => [:get], :constraints => { :only_ajax => true } +match 'helpdesk_widget/iframe.:format' => 'helpdesk_widget#iframe', :via => [:get], :constraints => { :only_ajax => true } +match 'helpdesk_widget/load_form.:format' => 'helpdesk_widget#load_form', :via => [:get], :constraints => { :only_ajax => true } +match 'helpdesk_widget/load_custom_fields' => 'helpdesk_widget#load_custom_fields', :via => [:get], :constraints => { :only_ajax => true } +match 'helpdesk_widget/avatar/:login' => 'helpdesk_widget#avatar', :via => [:get] +match 'helpdesk_widget/create_ticket' => 'helpdesk_widget#create_ticket', :via => [:post], :constraints => { :only_ajax => true } + +get "mail_fetcher/receive_imap" => "mail_fetcher#receive_imap" +get "mail_fetcher/receive_pop3" => "mail_fetcher#receive_pop3" + +match 'tickets/:id/:hash' => 'public_tickets#show', :as => :public_ticket, :via => [:get, :post] +match 'tickets/:id/add_comment/:hash' => 'public_tickets#add_comment', :as => :public_ticket_add_comment, :via => [:get, :post] + +match 'vote/:id/:hash' => 'helpdesk_votes#show', :via => :get, :as => 'helpdesk_votes_show' +match 'vote/:id/:hash' => 'helpdesk_votes#vote', :via => :post, :as => 'helpdesk_votes_vote' +match 'vote/:id/:vote/:hash' => 'helpdesk_votes#fast_vote', :via => :get, :as => 'helpdesk_votes_fast_vote' + +get 'attachments/:id/:ticket_id/:hash/:filename', :to => 'attachments#show', :id => /\d+/, :filename => /.*/, :as => 'hashed_named_attachment' +get 'attachments/download_hashed/:id/:ticket_id/:hash/:filename', :to => 'attachments#download', :id => /\d+/, :filename => /.*/, :as => 'hashed_download_named_attachment' diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/001_create_contact_journals.rb b/plugins/redmine_contacts_helpdesk/db/migrate/001_create_contact_journals.rb new file mode 100644 index 0000000..9d0d809 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/001_create_contact_journals.rb @@ -0,0 +1,17 @@ +class CreateContactJournals < ActiveRecord::Migration + def self.up + create_table :contact_journals do |t| + t.references :contact + t.references :journal + t.string :email + t.boolean :is_incoming + t.timestamps :null => false + end + add_index :contact_journals, [:journal_id, :contact_id] + end + + def self.down + drop_table :contact_journals + end +end + diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/002_create_helpdesk_tickets.rb b/plugins/redmine_contacts_helpdesk/db/migrate/002_create_helpdesk_tickets.rb new file mode 100644 index 0000000..4d942ba --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/002_create_helpdesk_tickets.rb @@ -0,0 +1,52 @@ +class CreateHelpdeskTickets < ActiveRecord::Migration + def self.up + create_table :helpdesk_tickets do |t| + t.references :contact + t.references :issue + t.integer :source, :null => false, :default => HelpdeskTicket::HELPDESK_EMAIL_SOURCE + t.string :from_address + t.string :to_address + t.datetime :ticket_date + end + add_index :helpdesk_tickets, [:issue_id, :contact_id] + + remove_index :contact_journals, [:journal_id, :contact_id] + rename_table :contact_journals, :journal_messages + add_index :journal_messages, [:journal_id, :contact_id] + + change_table :journal_messages do |t| + t.remove :created_at, :updated_at + t.integer :source, :null => false, :default => HelpdeskTicket::HELPDESK_EMAIL_SOURCE + t.string :from_address + t.string :to_address + t.string :bcc_address + t.string :cc_address + t.datetime :message_date + end + + JournalMessage.where(:is_incoming => true).update_all("from_address = email") + JournalMessage.where(:is_incoming => false).update_all("to_address = email") + + remove_column :journal_messages, :email + Attachment.where(:container_type => 'ContactJournal').update_all(:container_type => 'JournalMessage') + + end + + def self.down + Attachment.where(:container_type => 'JournalMessage').update_all(:container_type => 'ContactJournal') + drop_table :helpdesk_tickets + add_column :journal_messages, :email, :string + + JournalMessage.where(:is_incoming => true).update_all("email = from_address") + JournalMessage.where(:is_incoming => false).update_all("email = to_address") + + change_table :journal_messages do |t| + t.timestamps + t.remove :source, :from_address, :to_address, :bcc_address, :cc_address, :message_date + end + + rename_table :journal_messages, :contact_journals + + end +end + diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/003_add_cc_and_message_id_to_helpdesk_tickets.rb b/plugins/redmine_contacts_helpdesk/db/migrate/003_add_cc_and_message_id_to_helpdesk_tickets.rb new file mode 100644 index 0000000..e861dba --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/003_add_cc_and_message_id_to_helpdesk_tickets.rb @@ -0,0 +1,10 @@ +class AddCcAndMessageIdToHelpdeskTickets < ActiveRecord::Migration + def change + add_column :helpdesk_tickets, :cc_address, :string + add_column :helpdesk_tickets, :message_id, :string + add_index :helpdesk_tickets, [:message_id] + + add_column :journal_messages, :message_id, :string + add_index :journal_messages, [:message_id] + end +end diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/004_create_canned_responses.rb b/plugins/redmine_contacts_helpdesk/db/migrate/004_create_canned_responses.rb new file mode 100644 index 0000000..601effd --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/004_create_canned_responses.rb @@ -0,0 +1,11 @@ +class CreateCannedResponses < ActiveRecord::Migration + def change + create_table :canned_responses do |t| + t.string :name + t.text :content + t.integer :project_id + t.integer :user_id + t.boolean :is_public + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/005_add_is_incoming_to_helpdesk_tickets.rb b/plugins/redmine_contacts_helpdesk/db/migrate/005_add_is_incoming_to_helpdesk_tickets.rb new file mode 100644 index 0000000..f90d939 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/005_add_is_incoming_to_helpdesk_tickets.rb @@ -0,0 +1,5 @@ +class AddIsIncomingToHelpdeskTickets < ActiveRecord::Migration + def change + add_column :helpdesk_tickets, :is_incoming, :boolean, :default => true + end +end diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/006_add_metrics_to_helpdesk_tickets.rb b/plugins/redmine_contacts_helpdesk/db/migrate/006_add_metrics_to_helpdesk_tickets.rb new file mode 100644 index 0000000..9f828bf --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/006_add_metrics_to_helpdesk_tickets.rb @@ -0,0 +1,9 @@ +class AddMetricsToHelpdeskTickets < ActiveRecord::Migration + def change + add_column :helpdesk_tickets, :reaction_time, :integer + add_column :helpdesk_tickets, :first_response_time, :integer + add_column :helpdesk_tickets, :resolve_time, :integer + add_column :helpdesk_tickets, :last_agent_response_at, :datetime + add_column :helpdesk_tickets, :last_customer_response_at, :datetime + end +end diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/007_populate_helpdesk_tickets_metrics.rb b/plugins/redmine_contacts_helpdesk/db/migrate/007_populate_helpdesk_tickets_metrics.rb new file mode 100644 index 0000000..ce8d450 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/007_populate_helpdesk_tickets_metrics.rb @@ -0,0 +1,9 @@ +class PopulateHelpdeskTicketsMetrics < ActiveRecord::Migration + def up + HelpdeskTicket.joins(:issue).readonly(false).each(&:save) + end + + def down + #none + end +end diff --git a/plugins/redmine_contacts_helpdesk/db/migrate/008_add_vote_to_helpdesk_tickets.rb b/plugins/redmine_contacts_helpdesk/db/migrate/008_add_vote_to_helpdesk_tickets.rb new file mode 100644 index 0000000..1118f96 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/db/migrate/008_add_vote_to_helpdesk_tickets.rb @@ -0,0 +1,6 @@ +class AddVoteToHelpdeskTickets < ActiveRecord::Migration + def change + add_column :helpdesk_tickets, :vote, :integer, :default => nil + add_column :helpdesk_tickets, :vote_comment, :string + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/doc/CHANGELOG b/plugins/redmine_contacts_helpdesk/doc/CHANGELOG new file mode 100644 index 0000000..211e882 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/doc/CHANGELOG @@ -0,0 +1,311 @@ +== Redmine Helpdesk plugin changelog + +Redmine Helpdesk plugin - helpdesk plugin (email support) for redmine +Copyright (C) 2011-2017 RedmineUP +https://www.redmineup.com/ + +== 2017-08-17 v3.0.9 + +* Added Reply button +* Fixed settings saving bug +* Just mail link hidden +* New public ticket stytes +* Custom fields hidded for public tickets +* Fixed my page context menu bug +* Fixed email emoji problem + +== 2017-07-07 v3.0.8 + +* Autoclose tickets setting +* Added CC field edit on the contact card +* Added translation option for attachments in widget +* Apply macros for initial message send +* Fixed ignored attachment bug +* Redmine 3.4 support fixes +* Changed filters for select2 +* Always send CC if present +* Reports fixes + +== 2017-02-24 v3.0.7 + +* Canned responses visibility fixes +* Fixed bug with customer response +* Reports fixes +* Fixed win1251 subject encoding +* Fixed widget styles param + +== 2016-12-02 v3.0.6 + +* Added From address macro to use agent name +* Fixed bug with Widget API avatar +* Fixed bug with single attachment part emails +* Fixed {{send_file()}} macro + +== 2016-11-21 v3.0.5 + +* User avatar Widget API +* "Busiest time of day" report +* Predefined fields hidden from Widget +* Canned responses tab in global settings +* Cleanup Helpdesk project settings +* Send notications for tickets created using Widget +* Widget API fixes +* Email dropbox for tickets admin+1234@redmine.app.com + +== 2016-09-01 v3.0.4 + +* First response report +* Email dropbox for redmine issues admin+123@redmine.com +* Helpdesk widget API (translation and ) +* Macro tag {%response.author.last_name%} (Alexey Dvoryanchikov) +* Fixed destination for outgoing emails with CC list +* Chinese translation (Zuofeng Zhang) + +== 2016-07-01 v3.0.3 + +* French translation update (Olivier Houdas) +* Fixed tracker_id bug for Redmine 3.3 + +== 2016-05-26 v3.0.2 + +* Helpdesk widget +* Assign tickets to contact assignee +* Added allow_override option for redmine:email:helpdesk rake tasks +* Activated TLS for outgoing mails +* Show delivery errors on email sending +* Previous ticket new styles +* Banner plugin compatibility +* CKEEditor and canned responses compatibility +* Required fields ignoring on ticket creating +* Redmine 3.2 UI compatibility +* Portuguese (Brasil) translation update (Leandro Gehlen) +* French translation update (Olivier Houdas) +* Dutch translation (Philippe Schottey) +* Fixed bug with manual emails receiving (Unable to determine @target_project) +* Fixed bug with ticket status changing on public link response +* Fixed but with reversed comments in public tickets +* Fixed API bug with send_note +* Fixed bug with showing global canned responses list +* Fixed email's subject decoding bug +* Fixed bug with wrong token for public tickets +* Fixed bug customer company filter with connection variable + +== 2015-08-24 v3.0.1 + +* Customer profile saving bug fixed +* Fetching mail bug fixed + +== 2015-06-15 v3.0.0 + +* Redmine 3 support +* Turkish translation update (Adnan Topcu) + +== 2015-03-25 v2.4.0 + +* Satisfaction Ratings +* Select first project tracker for "All" setting +* Macro tag {%response.author.custom_field: Custom field name%} +* French translation update (Olivier Houdas) +* Swedish translation update (Khedron Wilk) +* Ticket contact company time report grouping (Marcelo A. Fernandes) +* Ignoring emails with "X-Auto-Response-Suppress: all" header +* Ignoring attachments from Redmine global settings list +* redirect_on_success param for create_ticket API method +* fixes n+1 on issues list +* New redmine_helpdesk.log for Helpdesk messages +* Italian translation update (Nicola Mondinelli) + +== 2014-11-10 v2.3.0 + +* Ticket reaction metrics (reaction time, first response time, resolve time) +* Contact selection fixes +* Setting for disable striping HTML tags +* Serbian translation (Radenko) + +== 2014-10-02 v2.2.13 + +* Spent time reports for customer +* Hide private issues and notes from public tickets view +* New styles for public tickets incoming replies +* Permission check for filters and issue list columns +* Hide avatars on public tickets view +* Fixed bug with contacts without projects + +== 2014-09-06 v2.2.12 + +* Added macro @{%ticket.start_date%}@ +* SMTP Authentication method fixes (Markus Plutka) +* Added support for "cross-project contacts relations" setting +* Turkish translation (Adnan Topcu) +* CKEditor support for HTML part of sent mails +* Show project on global canned responses list +* Links for adding macros in canned responses +* Reverse name if "," was used in Last, First notation. +* Company assignment from brackets in sender name string +* Fixed binary files corruption +* Fixed iso-2022-jp encoded messages +* Fixed bug with empty contact on new ticker form + +== 2014-05-03 v2.2.11 + +* French translation update by Olivier Houdas +* Set default start date on issues created by email if settings +* Fixed bug with deleting related attachments + +== 2014-04-07 v2.2.10 + +* Added macro @{%contact.email%} +* Multiline encoded subject fixes +* Canned responses columns "Public", "For all projects" +* Fixed canned responses visibility +* Fixed global canned responses update/delete +* Post image from clipboard compatibility +* Time out 60 sec for sending email +* Spanish translation update (Leandro Russo) + +== 2014-02-25 v2.2.9 + +* Polish translation (Szymon Anders) +* Fixed bug with attachments in create_ticket API +* Send autoresponse to CC list by default +* Custom SMTP server settings (experimental) + +== 2014-02-11 v2.2.8 + +* Encoding fixes (Tillmann Steinbrecher) +* Prevent attachments duplication (hash comparing) +* Canned responses global list +* Editable To address in send note (forwarding emails) +* Sending initial message to customer +* Added macros @{%ticket.closed_on%}@ and @{%ticket.due_date%}@ + +== 2014-01-23 v2.2.7 + +* Ticket history macro +* Issues customer company filter with Equal condition +* Brazil translation (Tiago O Baptistetti) +* Spanish translation (Luis Blasco) +* Italian translation (Nicola Mondinelli) + +== 2013-12-23 v2.2.6 + +* New last messate date column (x hours ago) for tickets (issues) list +* Set ticket date from email date (was current date) +* New issues table column "Helpdeks ticket" +* API changes for create_ticket method +* Fixed bug with attachments for puplic tickets + +== 2013-09-25 v2.2.5 + +* Fixed bug with CRM plugin >=3.2.5 compatibility +* Required CRM plugin >=3.2.5 + +== 2013-09-23 v2.2.4 + +* REST API for adding attachemnts to the new ticket +* Assign unassigned ticket to the current user if "Send email" checked +* New email body HTML layout + +== 2013-08-13 v2.2.3 + +* Show employees tickets for companies +* Replace "Anonymous wrote:" in quoted reply +* Added new ticket source type "conversation" +* Slovak locale (Martin Bucko) +* Send auto answer checkbox on ticket creation from 'New issue' tab +* Auto assign ticket to a first responder user for response via email +* Auto assign answered status for response via email +* Set "Reopen status" after add comments to public tickets +* Fixed bug with send notes for invalid issues + +== 2013-05-04 v2.2.2 + +* My page tickets +* Contact tab for helpdesk tickets +* Contact context menu to add helpdesk ticket + +== 2013-05-04 v2.2.1 + +* Tickets grouping for activity page +* Do not strip tags from plain text part + +== 2013-04-05 v2.2.0 + +* Canned responses +* Customer company field and filter on issues list +* Ticket source field and filter on issues list +* Bug fixes for rails 3.2.13 +* Fixed bug with attachments in redmine 2.3 + +== 2013-02-26 v2.1.3 + +* Public links for helpdesk tickets +* Redmine mail fetcher by REST API +* Fixed bug with duplicating responses +* Fixed bug with ticket status reset + +== 2013-02-04 v2.1.2 + +* Redmine 2.3 trunk support +* New macros for issue status, % done, estimated hours, priority +* Template for answer subject +* Better email threading + +== 2013-01-21 v2.1.1 + +* Redmine 2.2.2 support + +== 2013-01-20 v2.1.0 + +* Redmine 2.2.1 support +* Add contacts from CC addresses when receiving issues +* Select customer and ticket source chanel on issue creation form +* Cc and Bcc for sending response +* Delete Junk mail button +* Filter by issues with/without tickets +* Link response by "In-Reply-To" field by message-Id +* Autocomplete for Helpdesk ticket customer select +* Selection To address for response +* Rake task changed from redmine:email:helpdesk: to redmine:email:helpdesk: + +== 2012-11-01 v2.0.0 + +* Feature: Redmine 2.1 support +* Feature: Ruby 1.9 support +* Feature: issues list for each contact +* Feature: Preview for email original in issue notes +* Feature: New macros %%QUOTED_ISSUE_DESCRIPTION%%, %%SUBJECT%%, %%NOTE_AUTHOR%%, %%NOTE_AUTHOR.FIRST_NAME%% +* Feature: Applying macros for sent issues notes +* Feature: email Blacklist Filtering +* Feature: REST API for getting mails from all projects +* Feature: Last message from/to contact in issue list +* Feature: Contact profile (card) on issue show page +* Feature: Previous messages from contact on issue show page +* Feature: Send mails as plain text option +* Feature: Autoresponse with Noreply-Email +* Feature: Last message column for issues list +* Feature: Show message header and footer in issue edit form +* Feature: Contact profile card on the issue sidebar +* Feature: Answer to customer from email +* Feature: Add line breaks for html only message +* Feature: Activity providers for helpdesk tickets and responses +* Feature: Send flag default settings +* Bug: SSL always on with IMAP and project mail settings +* Bug: IMAP Folder does not work +* Bug: Journals connections does not copy after contacts merge +* Bug: move on failure label +* Bug: "Add tags after create" doesn't save +* Bug: Macro %%UPDATER%% not working +* Bug: Email subject format is not used for replies (rather than auto-notifications) + + +== 2012-03-07 v1.0.3 + +* Feature: Store incoming emails as attachments to issue/journal +* Feature: use mail Header to track email to issue id +* Feature: Store email server settings in project settings tab +* Feature: Multiple Recipients from issue contacts +* Feature: Create new issue if respond lifetime expired +* Feature: CSS styles for emails +* Feature: Change issue status in note add form +* Feature: API for sending response to issue diff --git a/plugins/redmine_contacts_helpdesk/doc/COPYING b/plugins/redmine_contacts_helpdesk/doc/COPYING new file mode 100644 index 0000000..82fa1da --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/doc/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugins/redmine_contacts_helpdesk/doc/LICENSE b/plugins/redmine_contacts_helpdesk/doc/LICENSE new file mode 100644 index 0000000..e2e50b5 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/doc/LICENSE @@ -0,0 +1,26 @@ +LICENSING + +RedmineUP Licencing + +This End User License Agreement is a binding legal agreement between you and RedmineUP. Purchase, installation or use of RedmineUP Extensions provided on redmineup.com signifies that you have read, understood, and agreed to be bound by the terms outlined below. + +RedmineUP GPL Licencing + +All Redmine Extensions produced by RedmineUP are released under the GNU General Public License, version 2 (http://www.gnu.org/licenses/gpl-2.0.html). Specifically, the Ruby code portions are distributed under the GPL license. If not otherwise stated, all images, manuals, cascading style sheets, and included JavaScript are NOT GPL, and are released under the RedmineUP Proprietary Use License v1.0 (See below) unless specifically authorized by RedmineUP. Elements of the extensions released under this proprietary license may not be redistributed or repackaged for use other than those allowed by the Terms of Service. + +RedmineUP Proprietary Use License (v1.0) + +The RedmineUP Proprietary Use License covers any images, cascading stylesheets, manuals and JavaScript files in any extensions produced and/or distributed by redmineup.com. These files are copyrighted by redmineup.com (RedmineUP) and cannot be redistributed in any form without prior consent from redmineup.com (RedmineUP) + +Usage Terms + +You are allowed to use the Extensions on one or many "production" domains, depending on the type of your license +You are allowed to make any changes to the code, however modified code will not be supported by us. + +Modification Of Extensions Produced By RedmineUP. + +You are authorized to make any modification(s) to RedmineUP extension Ruby code. However, if you change any Ruby code and it breaks functionality, support may not be available to you. + +In accordance with the RedmineUP Proprietary Use License v1.0, you may not release any proprietary files (modified or otherwise) under the GPL license. The terms of this license and the GPL v2 prohibit the removal of the copyright information from any file. + +Please contact us if you have any requirements that are not covered by these terms. \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/extra/rdm-helpdesk-mailhandler.rb b/plugins/redmine_contacts_helpdesk/extra/rdm-helpdesk-mailhandler.rb new file mode 100644 index 0000000..f5f49d7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/extra/rdm-helpdesk-mailhandler.rb @@ -0,0 +1,166 @@ +#!/usr/bin/env ruby +# Redmine - project management software +# Copyright (C) 2006-2015 Jean-Philippe Lang +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +require 'net/http' +require 'net/https' +require 'uri' +require 'optparse' + +module Net + class HTTPS < HTTP + 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 + http = new(url.host, url.port) + http.use_ssl = (url.scheme == 'https') + if options[:certificate_bundle] + http.ca_file = options[:certificate_bundle] + end + if options[:no_check_certificate] + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + http.start {|h| h.request(request) } + end + end +end + +class RedmineMailHandler + VERSION = '0.2' + + attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check, + :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification + + def initialize + self.issue_attributes = {} + + optparse = OptionParser.new do |opts| + opts.banner = "Usage: rdm-mailhandler.rb [options] --url= --key=" + opts.separator("") + opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.") + opts.separator("") + opts.separator("Required arguments:") + opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v} + opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v} + opts.separator("") + opts.separator("General options:") + opts.on("--key-file FILE", "full path to a file that contains your Redmine", + "API key (use this option instead of --key if", + "you don't want the key to appear in the command", + "line)") {|v| read_key_from_file(v)} + opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true} + opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v} + opts.on("-h", "--help", "show this help") {puts opts; exit 1} + opts.on("-v", "--verbose", "show extra information") {self.verbose = true} + opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit} + opts.separator("") + opts.separator("Ticket attributes control options:") + opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v} + opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v} + opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v} + opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v} + opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v} + opts.on( "--private", "create new ticket as private") {|v| self.issue_attributes['is_private'] = '1'} + opts.on( "--due-date DUE_DATE", "due date for ticket") {|v| self.issue_attributes['due_date'] = v} + opts.on( "--assigned_to ASSIGNED_LOGIN", "assigned user login") {|v| self.issue_attributes['assigned_to'] = v} + opts.on("-o", "--allow-override ATTRS", "allow email content to override attributes", + "specified by previous options", + "ATTRS is a comma separated list of attributes") {|v| self.allow_override = v} + + opts.separator("") + opts.separator("Example:") + opts.separator(" rdm-helpdesk-mailhandler.rb --url http://redmine.domain.foo --key secret --project test-project\\") + opts.separator(" --category foo \\") + opts.separator(" --tracker bug \\") + opts.separator(" --allow-override tracker,project") + + opts.summary_width = 27 + end + optparse.parse! + + unless url && key + puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help." + exit 1 + end + end + + def submit(email) + uri = url.gsub(%r{/*$}, '') + '/helpdesk_mailer' + + headers = { 'User-Agent' => "Redmine helpdesk mail handler/#{VERSION}" } + + data = { 'key' => key, 'email' => email, + 'allow_override' => allow_override, + 'unknown_user' => unknown_user, + 'default_group' => default_group, + 'no_account_notice' => no_account_notice, + 'no_notification' => no_notification, + 'no_permission_check' => no_permission_check} + issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } + + debug "Posting to #{uri}..." + begin + response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle) + rescue SystemCallError, IOError => e # connection refused, etc. + warn "An error occured while contacting your Redmine server: #{e.message}" + return 75 # temporary failure + end + debug "Response received: #{response.code}" + + case response.code.to_i + when 403 + warn "Request was denied by your Redmine server. " + + "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key." + return 77 + when 422 + warn "Request was denied by your Redmine server. " + + "Possible reasons: email is sent from an invalid email address or is missing some information." + return 77 + when 400..499 + warn "Request was denied by your Redmine server (#{response.code})." + return 77 + when 500..599 + warn "Failed to contact your Redmine server (#{response.code})." + return 75 + when 201 + debug "Proccessed successfully" + return 0 + else + return 1 + end + end + + private + + def debug(msg) + puts msg if verbose + end + + def read_key_from_file(filename) + begin + self.key = File.read(filename).strip + rescue Exception => e + $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}" + exit 1 + end + end +end + +handler = RedmineMailHandler.new +exit(handler.submit(STDIN.read)) diff --git a/plugins/redmine_contacts_helpdesk/init.rb b/plugins/redmine_contacts_helpdesk/init.rb new file mode 100644 index 0000000..8de91f4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/init.rb @@ -0,0 +1,50 @@ +require 'redmine' + +Redmine::Plugin.register :redmine_contacts_helpdesk do + name "Redmine Helpdesk plugin (PRO version)" + author 'RedmineUP' + description 'This is a Helpdesk plugin for Redmine' + version '3.0.9' + url 'https://www.redmineup.com/pages/plugins/helpdesk' + author_url 'mailto:support@redmineup.com' + + requires_redmine :version_or_higher => '2.3' + + begin + requires_redmine_plugin :redmine_contacts, :version_or_higher => '4.0.9' + rescue Redmine::PluginNotFound => e + raise "Please install redmine_contacts plugin" + end + + settings :default => { + "helpdesk_answer_from" => '', + "helpdesk_add_contact_notes" => '1', + "helpdesk_answer_subject" => 'Re: {%ticket.subject%} [{%ticket.tracker%} #{%ticket.id%}]', + "helpdesk_first_answer_subject" => '{%ticket.project%} support message [{%ticket.tracker%} #{%ticket.id%}]', + "helpdesk_first_answer_template" => "Hello, {%contact.first_name%}\n\nWe hereby confirm that we have received your message.\n\nWe will handle your request and get back to you as soon as possible.\n\nYour request has been assigned the following case ID #\{%ticket.id%}.", + "helpdesk_assign_contact_user" => 0, + "helpdesk_create_private_tickets" => 0, + "helpdesk_autoclose_tickets_time_unit" => 'day' + }, :partial => 'settings/helpdesk' + + project_module :contacts_helpdesk do + permission :view_helpdesk_tickets, :helpdesk => [:show_original], + :helpdesk_search => [:usage, :ticket_by_issue, :issues_by_contact, :messages_by_issue, :contact_timeline], + :canned_responses => [:add] + permission :view_helpdesk_reports, :helpdesk_reports => [:show, :render_chart] + permission :send_response, :issues => [:send_helpdesk_response, :email_note], + :helpdesk => [:show_original, :create_ticket, :delete_spam] + permission :edit_helpdesk_settings, :helpdesk => [:save_settings, :get_mail] + permission :edit_helpdesk_tickets, :helpdesk_tickets => [:update, :edit, :destroy] + # Canned responses + permission :manage_public_canned_responses, {:canned_responses => [:new, :create, :edit, :update, :destroy]}, :require => :member + permission :manage_canned_responses, {:canned_responses => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin + end + + menu :admin_menu, :helpdesk, {:controller => 'settings', :action => 'plugin', :id => "redmine_contacts_helpdesk"}, :caption => :label_helpdesk, :param => :project_id, :html => {:class => 'icon'} + + activity_provider :helpdesk_tickets, :default => false, :class_name => ['HelpdeskTicket', 'JournalMessage'] + +end + +require 'redmine_helpdesk' diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb new file mode 100644 index 0000000..25474da --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk.rb @@ -0,0 +1,135 @@ +ActionDispatch::Callbacks.to_prepare do + require 'redmine_helpdesk/patches/issues_controller_patch' + require 'redmine_helpdesk/patches/journals_controller_patch' + require 'redmine_helpdesk/patches/attachments_controller_patch' + require 'redmine_helpdesk/patches/issue_patch' + require 'redmine_helpdesk/patches/journal_patch' + require 'redmine_helpdesk/patches/contact_patch' + require 'redmine_helpdesk/patches/issue_query_patch' + require 'redmine_helpdesk/patches/time_report_patch' + require 'redmine_helpdesk/patches/queries_helper_patch' + require 'redmine_helpdesk/patches/projects_helper_patch' + require 'redmine_helpdesk/patches/contacts_helper_patch' + require 'redmine_helpdesk/patches/application_helper_patch' + require 'redmine_helpdesk/patches/mail_handler_patch' + require 'redmine_helpdesk/patches/compatibility_patch' + require 'redmine_helpdesk/patches/contact_query_patch' + + require 'redmine_helpdesk/hooks/view_layouts_hook' + require 'redmine_helpdesk/hooks/view_issues_hook' + require 'redmine_helpdesk/hooks/view_projects_hook' + require 'redmine_helpdesk/hooks/view_contacts_hook' + require 'redmine_helpdesk/hooks/view_journals_hook' + require 'redmine_helpdesk/hooks/controller_contacts_duplicates_hook' + require 'redmine_helpdesk/hooks/helper_issues_hook' + require 'redmine_helpdesk/hooks/issues_controller_hook' + + require 'redmine_helpdesk/wiki_macros/helpdesk_wiki_macro' +end + +class HelpdeskLogFormatter < Logger::Formatter + def call(severity, time, progname, msg) + "[%s] - %s - %s\n" % [severity, time.to_s(:short), msg2str(msg)] unless msg2str(msg).blank? + end +end + +HelpdeskLogger = Logger.new(Rails.root.join('log/redmine_helpdesk.log')) +HelpdeskLogger.formatter = HelpdeskLogFormatter.new + +class HelpdeskSettings + MACRO_LIST = %w({%contact.first_name%} {%contact.name%} {%contact.company%} {%contact.last_name%} + {%contact.middle_name%} {%date%} {%ticket.assigned_to%} {%ticket.id%} {%ticket.tracker%} + {%ticket.project%} {%ticket.subject%} {%ticket.quoted_description%} {%ticket.history%} {%ticket.status%} + {%ticket.priority%} {%ticket.estimated_hours%} {%ticket.done_ratio%} {%ticket.public_url%} {%ticket.closed_on%} {%ticket.due_date%} + {%ticket.start_date%} {%ticket.voting%} {%ticket.voting.good%} {%ticket.voting.okay%} {%ticket.voting.bad%} + {%response.author%} {%response.author.first_name%} {%response.author.last_name%}) + + FROM_MACRO_LIST = %w({%response.author%} {%response.author.first_name%}) + + # Returns the value of the setting named name + def self.[](name, project_id) + project_id = project_id.id if project_id.is_a?(Project) + ContactsSetting[name.to_s, project_id].present? ? ContactsSetting[name.to_s, project_id] : RedmineHelpdesk.settings[name.to_s] + end +end + +module RedmineHelpdesk + module ContactUserMethods + def name(formatter = nil) + f = self.class.name_formatter(formatter) + if formatter + eval('"' + f[:string] + '"') + else + @name ||= eval('"' + f[:string] + '"') + end + end + end + + def self.settings() Setting[:plugin_redmine_contacts_helpdesk] ? Setting[:plugin_redmine_contacts_helpdesk] : {} end + + def self.public_title + universal_setting("helpdesk_public_title") + end + + def self.public_tickets? + universal_setting("helpdesk_public_tickets").to_i > 0 + end + + def self.vote_allow? + universal_setting("helpdesk_vote_accept").to_i > 0 + end + def self.vote_comment_allow? + universal_setting("helpdesk_vote_comment_accept").to_i > 0 + end + + def self.strip_tags? + universal_setting("helpdesk_do_not_strip_tags").to_i <= 0 + end + + def self.public_comments? + universal_setting("helpdesk_public_comments").to_i > 0 + end + + def self.public_spent_time? + universal_setting("helpdesk_public_show_spent_time").to_i > 0 + end + + def self.save_log? + universal_setting(:helpdesk_vote_save_log).to_i > 0 + end + + def self.autoclose_tickets_after + universal_setting("helpdesk_autoclose_tickets_after").to_i + end + + def self.autoclose_from_status + universal_setting("helpdesk_autoclose_from_status").to_i + end + + def self.autoclose_to_status + universal_setting("helpdesk_autoclose_to_status").to_i + end + + def self.autoclose_time_unit + universal_setting("helpdesk_autoclose_tickets_time_unit") + end + + def self.autoclose_time_unit_is?(value) + autoclose_time_unit == value + end + + def self.autoclose_time_interval + case autoclose_time_unit + when 'day' + 1.day * autoclose_tickets_after + when 'hour' + 1.hour * autoclose_tickets_after + else + -1 + end + end + + def self.universal_setting(setting) + [settings[setting.to_sym], settings[setting.to_s]].compact.first + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/controller_contacts_duplicates_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/controller_contacts_duplicates_hook.rb new file mode 100644 index 0000000..c9db72b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/controller_contacts_duplicates_hook.rb @@ -0,0 +1,10 @@ +module RedmineHelpdesk + module Hooks + class ControllerContactsDuplicatesHook < Redmine::Hook::ViewListener + def controller_contacts_duplicates_merge(context={}) + context[:duplicate].journal_messages << context[:contact].journal_messages + context[:duplicate].helpdesk_tickets << context[:contact].helpdesk_tickets + end + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/helper_issues_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/helper_issues_hook.rb new file mode 100644 index 0000000..5fe784d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/helper_issues_hook.rb @@ -0,0 +1,15 @@ +module RedmineHelpdesk + module Hooks + class HelperIssuesHook < Redmine::Hook::ViewListener + + def helper_issues_show_detail_after_setting(context={}) + if context[:detail].prop_key == 'vote' + detail = context[:detail] + context[:detail].value = HelpdeskTicket.vote_message(detail.value) if detail.value && detail.value.to_s =~ /^\d$/ + context[:detail].old_value = HelpdeskTicket.vote_message(detail.old_value) if detail.old_value && detail.old_value.to_s =~ /^\d$/ + end + end + + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/issues_controller_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/issues_controller_hook.rb new file mode 100644 index 0000000..450b668 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/issues_controller_hook.rb @@ -0,0 +1,11 @@ +module RedmineHelpdesk + module Hooks + class IssuesControllerHook < Redmine::Hook::ViewListener + def controller_issues_new_before_save(context = {}) + ticket = context[:issue].helpdesk_ticket + return if ticket.nil? || ticket.from_address.present? || ticket.customer.nil? || ticket.customer.primary_email.blank? + ticket.from_address = ticket.customer.primary_email + end + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_contacts_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_contacts_hook.rb new file mode 100644 index 0000000..450625a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_contacts_hook.rb @@ -0,0 +1,7 @@ +module RedmineHelpdesk + module Hooks + class ViewContactsHook < Redmine::Hook::ViewListener + render_on :view_contacts_context_menu_before_delete, :partial => "context_menus/helpdesk_contacts" + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_issues_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_issues_hook.rb new file mode 100644 index 0000000..6ac1c06 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_issues_hook.rb @@ -0,0 +1,13 @@ +module RedmineHelpdesk + module Hooks + class ViewIssuesHook < Redmine::Hook::ViewListener + render_on :view_issues_edit_notes_bottom, :partial => 'issues/send_response' + def view_issues_sidebar_issues_bottom(context = {}) + context[:controller].send(:render_to_string, { :partial => 'issues/helpdesk_reports', :locals => context }) + + context[:controller].send(:render_to_string, { :partial => 'issues/helpdesk_customer_profile', :locals => context }) + end + render_on :view_issues_show_details_bottom, :partial => 'issues/ticket_data' + render_on :view_issues_form_details_top, :partial => 'issues/ticket_data_form' + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_journals_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_journals_hook.rb new file mode 100644 index 0000000..cade908 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_journals_hook.rb @@ -0,0 +1,9 @@ +include ContactsHelper + +module RedmineHelpdesk + module Hooks + class ShowJournalContactHook < Redmine::Hook::ViewListener + render_on :view_issues_history_journal_bottom, :partial => "journals/journal_contact" + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_layouts_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_layouts_hook.rb new file mode 100644 index 0000000..d269c8a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_layouts_hook.rb @@ -0,0 +1,9 @@ +module RedmineHelpdesk + module Hooks + class ViewsLayoutsHook < Redmine::Hook::ViewListener + def view_layouts_base_html_head(context={}) + return stylesheet_link_tag(:helpdesk, :plugin => 'redmine_contacts_helpdesk') + end + end + end +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_projects_hook.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_projects_hook.rb new file mode 100644 index 0000000..50042b6 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/hooks/view_projects_hook.rb @@ -0,0 +1,10 @@ +module RedmineHelpdesk + module Hooks + class ViewProjectsHook < Redmine::Hook::ViewListener + def view_projects_show_sidebar_bottom(context = {}) + context[:controller].send(:render_to_string, { :partial => 'issues/helpdesk_reports', :locals => context }) + + context[:controller].send(:render_to_string, { :partial => 'projects/helpdesk_tickets', :locals => context }) + end + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/application_helper_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/application_helper_patch.rb new file mode 100644 index 0000000..07846db --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/application_helper_patch.rb @@ -0,0 +1,45 @@ +require_dependency 'application_helper' + +module RedmineHelpdesk + module Patches + module ApplicationHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + + alias_method_chain :avatar, :helpdesk + alias_method_chain :link_to_user, :helpdesk + end + end + + + module InstanceMethods + # include ContactsHelper + + def avatar_with_helpdesk(user, options = { }) + if user.is_a?(Contact) + avatar_to(user, options) + else + avatar_without_helpdesk(user, options) + end + end + + def link_to_user_with_helpdesk(user, options={}) + if user.is_a?(Contact) + link_to_source(user, options) + else + link_to_user_without_helpdesk(user, options) + end + end + + end + + end + end +end + +unless ApplicationHelper.included_modules.include?(RedmineHelpdesk::Patches::ApplicationHelperPatch) + ApplicationHelper.send(:include, RedmineHelpdesk::Patches::ApplicationHelperPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/attachments_controller_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/attachments_controller_patch.rb new file mode 100644 index 0000000..bbe9c36 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/attachments_controller_patch.rb @@ -0,0 +1,27 @@ +module RedmineHelpdesk + module Patches + + module AttachmentsControllerPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + alias_method_chain :read_authorize, :helpdesk + end + end + + module InstanceMethods + def read_authorize_with_helpdesk + unless params[:ticket_id] && params[:hash] && HelpdeskTicket.where(:id => params[:ticket_id]).first && HelpdeskTicket.where(:id => params[:ticket_id]).first.try(:token) == params[:hash] + read_authorize_without_helpdesk + end + end + + end + end + end +end + +unless AttachmentsController.included_modules.include?(RedmineHelpdesk::Patches::AttachmentsControllerPatch) + AttachmentsController.send(:include, RedmineHelpdesk::Patches::AttachmentsControllerPatch) +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/compatibility_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/compatibility_patch.rb new file mode 100644 index 0000000..6e3b99c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/compatibility_patch.rb @@ -0,0 +1,5 @@ +if Redmine::VERSION.to_s < '2.4' + def accept_attachment?(attachment) + true + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_patch.rb new file mode 100644 index 0000000..204f82c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_patch.rb @@ -0,0 +1,47 @@ +module RedmineHelpdesk + module Patches + module ContactPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + has_many :journals, :through => :journal_messages + has_many :journal_messages, :dependent => :destroy + + has_many :tickets, :through => :helpdesk_tickets, :source => :issue #class_name => "Issue", :as => :issue, :foreign_key => 'issue_id' + has_many :helpdesk_tickets, :dependent => :destroy + end + end + + module InstanceMethods + def mail + self.primary_email + end + + def email_name + [name, ' <', mail, '>'].join + end + + def all_tickets + if self.is_company + Issue.eager_load(:customer).where(:contacts => {:id => [self.id] | self.company_contacts.map(&:id) }) + else + self.tickets + end + end + + def find_assigned_user(project, current_assigned_id) + return Principal.find_by_id(current_assigned_id) unless RedmineHelpdesk.settings["helpdesk_assign_contact_user"].to_i > 0 + return assigned_to if assigned_to.present? && Project.visible(assigned_to).pluck(:id).include?(project.id) + return contact_company.assigned_to if contact_company.present? && contact_company.assigned_to.present? && + Project.visible(contact_company.assigned_to).pluck(:id).include?(project.id) + Principal.find_by_id(current_assigned_id) + end + end + end + end +end + +unless Contact.included_modules.include?(RedmineHelpdesk::Patches::ContactPatch) + Contact.send(:include, RedmineHelpdesk::Patches::ContactPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_query_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_query_patch.rb new file mode 100644 index 0000000..f85ab2a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contact_query_patch.rb @@ -0,0 +1,57 @@ +require_dependency 'query' + +module RedmineHelpdesk + module Patches + module ContactQueryPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.send(:include, HelpdeskHelper) + + base.class_eval do + unloadable + + alias_method_chain :available_filters, :helpdesk + end + end + + module InstanceMethods + def sql_for_number_of_tickets_field(_, operator, value) + "(#{Contact.table_name}.id IN (SELECT #{HelpdeskTicket.table_name}.contact_id + FROM #{HelpdeskTicket.table_name} + GROUP BY #{HelpdeskTicket.table_name}.contact_id + HAVING count(#{HelpdeskTicket.table_name}.contact_id) #{operator} #{value.first}))" + end + + def sql_for_open_tickets_field(_, operator, value) + value = value.first + in_cond = if (operator == '!' && value == '0') || (operator == '=' && value == '1') + 'IN' + else + 'NOT IN' + end + "(#{Contact.table_name}.id #{in_cond} (SELECT #{HelpdeskTicket.table_name}.contact_id + FROM #{HelpdeskTicket.table_name} + INNER JOIN #{Issue.table_name} on #{Issue.table_name}.id = #{HelpdeskTicket.table_name}.issue_id + INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id + WHERE (#{IssueStatus.table_name}.is_closed = #{ActiveRecord::Base.connection.quoted_false}) + ))" + end + + def available_filters_with_helpdesk + if @available_filters.blank? && User.current.allowed_to?(:view_helpdesk_tickets, project, :global => true) + + add_available_filter('number_of_tickets', :type => :integer, :name => l(:label_helpdesk_number_of_tickets)) unless available_filters_without_helpdesk.key?('number_of_tickets') + add_available_filter('open_tickets', :type => :list, :name => l(:label_helpdesk_open_tickets), :values => [[l(:general_text_yes), '1'], [l(:general_text_no), '0']]) unless available_filters_without_helpdesk.key?('open_tickets') + else + available_filters_without_helpdesk + end + @available_filters + end + end + end + end +end + +unless ContactQuery.included_modules.include?(RedmineHelpdesk::Patches::ContactQueryPatch) + ContactQuery.send(:include, RedmineHelpdesk::Patches::ContactQueryPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contacts_helper_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contacts_helper_patch.rb new file mode 100644 index 0000000..1a41254 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/contacts_helper_patch.rb @@ -0,0 +1,26 @@ +module RedmineHelpdesk + module Patches + module ContactsHelperPatch + def self.included(base) + base.class_eval do + unloadable + alias_method_chain :contact_tabs, :helpdesk + end + end + + def contact_tabs_with_helpdesk(contact) + tabs = contact_tabs_without_helpdesk(contact) + + if contact.all_tickets.visible.count > 0 + tabs.push({:name => 'helpdesk', :partial => 'contacts/helpdesk_tickets', :label => l(:label_helpdesk_ticket_plural) + " (#{contact.all_tickets.visible.open.count}/#{contact.all_tickets.visible.count})"} ) + end + tabs + end + + end + end +end + +unless ContactsHelper.included_modules.include?(RedmineHelpdesk::Patches::ContactsHelperPatch) + ContactsHelper.send(:include, RedmineHelpdesk::Patches::ContactsHelperPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/gravatar_helper_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/gravatar_helper_patch.rb new file mode 100644 index 0000000..13fd0d0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/gravatar_helper_patch.rb @@ -0,0 +1,27 @@ +require_dependency 'queries_helper' + +module RedmineHelpdesk + module Patches + module GravatarHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + + alias_method_chain :gravatar_api_url, :helpdesk + end + end + + module InstanceMethods + def gravatar_api_url_with_helpdesk(hash) + [Setting[:protocol], ':', gravatar_api_url_without_helpdesk(hash)].join + end + end + end + end +end + +unless GravatarHelper::PublicMethods.included_modules.include?(RedmineHelpdesk::Patches::GravatarHelperPatch) + GravatarHelper::PublicMethods.send(:include, RedmineHelpdesk::Patches::GravatarHelperPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_patch.rb new file mode 100644 index 0000000..08f40f2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_patch.rb @@ -0,0 +1,89 @@ +module RedmineHelpdesk + module Patches + module IssuePatch + def self.included(base) + base.send(:extend, ClassMethods) + base.send(:include, InstanceMethods) + base.send(:include, ActionView::Helpers::DateHelper) + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + has_one :customer, :through => :helpdesk_ticket + has_one :helpdesk_ticket, :dependent => :destroy + + scope :order_by_status, lambda { joins(:status).order("#{IssueStatus.table_name}.is_closed, #{IssueStatus.table_name}.id, #{Issue.table_name}.id DESC") } + + accepts_nested_attributes_for :helpdesk_ticket + + safe_attributes 'helpdesk_ticket_attributes', + :if => lambda { |issue, user| user.allowed_to?(:edit_helpdesk_tickets, issue.project) } + + end + end + + module ClassMethods + def load_helpdesk_data(issues, user=User.current) + if issues.any? + helpdesk_tickets = HelpdeskTicket.where(:issue_id => issues.map(&:id)) + issues.each do |issue| + issue.instance_variable_set "@helpdesk_ticket", (helpdesk_tickets.detect{|c| c.issue_id == issue.id} || nil) + end + end + end + end + + module InstanceMethods + def journal_messages + @journal_messages ||= JournalMessage.includes(:message_file, :contact => [:avatar, :projects]). + where(:journal_id => journals.pluck(:id)). + uniq.to_a + end + + def is_ticket? + helpdesk_ticket.present? + end + + def last_message + self.helpdesk_ticket.last_message.content.truncate(250) if self.helpdesk_ticket + end + + def ticket_source + self.helpdesk_ticket.ticket_source_name if self.helpdesk_ticket + end + + def customer_company + return nil unless self.customer + self.customer.company + end + + def last_message_date + self.helpdesk_ticket.last_message_date if self.helpdesk_ticket + end + + def ticket_reaction_time + helpdesk_ticket && helpdesk_ticket.reaction_time ? distance_of_time_in_words(helpdesk_ticket.reaction_time) : "" + end + + def ticket_first_response_time + helpdesk_ticket && helpdesk_ticket.first_response_time ? distance_of_time_in_words(helpdesk_ticket.first_response_time) : "" + end + + def ticket_resolve_time + helpdesk_ticket && helpdesk_ticket.resolve_time ? distance_of_time_in_words(helpdesk_ticket.resolve_time) : "" + end + + def vote + helpdesk_ticket.present? && helpdesk_ticket.vote.present? ? HelpdeskTicket.vote_message(helpdesk_ticket.vote) : "" + end + + def vote_comment + helpdesk_ticket.present? && helpdesk_ticket.vote_comment.present? ? helpdesk_ticket.vote_comment.to_s : "" + end + + end + end + end +end + +unless Issue.included_modules.include?(RedmineHelpdesk::Patches::IssuePatch) + Issue.send(:include, RedmineHelpdesk::Patches::IssuePatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_query_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_query_patch.rb new file mode 100644 index 0000000..1316f6d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issue_query_patch.rb @@ -0,0 +1,185 @@ +require_dependency 'query' + +module RedmineHelpdesk + module Patches + module IssueQueryPatch + def self.included(base) + base.send(:include, InstanceMethods) + base.send(:include, HelpdeskHelper) + + base.class_eval do + unloadable + + alias_method_chain :available_columns, :helpdesk + alias_method_chain :available_filters, :helpdesk + alias_method_chain :joins_for_order_statement, :helpdesk + alias_method_chain :issues, :helpdesk + + end + end + + + module InstanceMethods + # def issues_with_helpdesk(options={}) + # if project.blank? || (project && User.current.allowed_to?(:view_helpdesk_tickets, project)) + # options[:include] = (options[:include] || []) + [:helpdesk_ticket] + # end + # issues_without_helpdesk(options) + # end + + def issues_with_helpdesk(options={}) + issues = issues_without_helpdesk(options) + if has_column?(:last_message) || has_column?(:last_message_date) || has_column?(:customer) || has_column?(:ticket_source) || has_column?(:customer_company) || has_column?(:helpdesk_ticket) || has_column?(:ticket_reaction_time) || has_column?(:ticket_first_response_time) || has_column?(:ticket_resolve_time) || has_column?(:vote) || has_column?(:vote_comment) + Issue.load_helpdesk_data(issues) + end + issues + end + + def joins_for_order_statement_with_helpdesk(order_options) + joins = joins_for_order_statement_without_helpdesk(order_options) + ticket_joins = [joins].flatten + if order_options && (order_options.include?('reaction_time') || + order_options.include?('first_response_time') || + order_options.include?('resolve_time') || + order_options.include?('vote')) + ticket_joins << "LEFT OUTER JOIN #{HelpdeskTicket.table_name} ON #{Issue.table_name}.id = #{HelpdeskTicket.table_name}.issue_id" + end + ticket_joins.any? ? ticket_joins.join(' ') : nil + end + + + def sql_for_customer_field(field, operator, value) + case operator + when "*", "!*" # Member / Not member + sw = operator == "!*" ? 'NOT' : '' + "(#{Issue.table_name}.id #{sw} IN (SELECT DISTINCT #{HelpdeskTicket.table_name}.issue_id FROM #{HelpdeskTicket.table_name}))" + when "=", "!" + sw = operator == "!" ? 'NOT' : '' + contacts_select = "SELECT #{HelpdeskTicket.table_name}.issue_id FROM #{HelpdeskTicket.table_name} + WHERE #{HelpdeskTicket.table_name}.contact_id IN (#{value.join(',')})" + + "(#{Issue.table_name}.id #{sw} IN (#{contacts_select}))" + end + end + + def sql_for_ticket_source_field(field, operator, value) + case operator + when "=", "!" + sw = operator == "!" ? 'NOT' : '' + contacts_select = "SELECT #{HelpdeskTicket.table_name}.issue_id FROM #{HelpdeskTicket.table_name} + WHERE #{HelpdeskTicket.table_name}.source IN (#{value.join(',')})" + + "(#{Issue.table_name}.id #{sw} IN (#{contacts_select}))" + end + end + + def sql_for_customer_company_field(field, operator, value) + sw = ["!", "!~"].include?(operator) ? 'NOT' : '' + case operator + when "=" + like_value = "LIKE '#{value.first.to_s.downcase}'" + when "!*" + like_value = "IS NULL OR #{Contact.table_name}.company = ''" + when "*" + like_value = "IS NOT NULL OR #{Contact.table_name}.company <> ''" + when "~", "!~" + like_value ="LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'" + end + + contacts_select = "SELECT #{HelpdeskTicket.table_name}.issue_id FROM #{HelpdeskTicket.table_name} + WHERE #{HelpdeskTicket.table_name}.contact_id IN ( + SELECT #{Contact.table_name}.id + FROM #{Contact.table_name} + WHERE LOWER(#{Contact.table_name}.company) #{like_value} + )" + + "(#{Issue.table_name}.id #{sw} IN (#{contacts_select}))" + end + + def sql_for_ticket_reaction_time_field(field, operator, value) + "(#{Issue.table_name}.id IN (SELECT #{HelpdeskTicket.table_name}.issue_id + FROM #{HelpdeskTicket.table_name} + WHERE #{sql_for_field(field, operator, value.map{|v| v.to_i * 60}, + HelpdeskTicket.table_name, "reaction_time")}))" + end + + def sql_for_ticket_first_response_time_field(field, operator, value) + "(#{Issue.table_name}.id IN (SELECT #{HelpdeskTicket.table_name}.issue_id + FROM #{HelpdeskTicket.table_name} + WHERE #{sql_for_field(field, operator, value.map{|v| v.to_i * 60}, + HelpdeskTicket.table_name, "first_response_time")}))" + end + + def sql_for_ticket_resolve_time_field(field, operator, value) + "(#{Issue.table_name}.id IN (SELECT #{HelpdeskTicket.table_name}.issue_id + FROM #{HelpdeskTicket.table_name} + WHERE #{sql_for_field(field, operator, value.map{|v| v.to_i * 60}, + HelpdeskTicket.table_name, "resolve_time")}))" + end + + def sql_for_vote_field(field, operator, value) + case operator + when '=', '*' + compare = 'IN' + when '!', '!*' + compare = 'NOT IN' + end + issues_select = "SELECT DISTINCT(issue_id) FROM helpdesk_tickets WHERE vote IN (#{ value.join(',') })" + issues_with_votes = 'SELECT DISTINCT(issue_id) FROM helpdesk_tickets WHERE vote IS NOT NULL' + "(#{Issue.table_name}.id #{compare} (#{ %w(= !).include?(operator) ? issues_select : issues_with_votes }))" + end + + def available_columns_with_helpdesk + if @available_columns.blank? && User.current.allowed_to?(:view_helpdesk_tickets, project, :global => true) + @available_columns = available_columns_without_helpdesk + @available_columns << QueryColumn.new(:last_message, :caption => :label_helpdesk_last_message) + @available_columns << QueryColumn.new(:last_message_date, :caption => :label_helpdesk_last_message_date) + @available_columns << QueryColumn.new(:customer, :caption => :label_helpdesk_contact) + @available_columns << QueryColumn.new(:ticket_source, :caption => :label_helpdesk_ticket_source) + @available_columns << QueryColumn.new(:customer_company, :caption => :label_helpdesk_contact_company) + @available_columns << QueryColumn.new(:helpdesk_ticket, :caption => :label_helpdesk_ticket) + @available_columns << QueryColumn.new(:ticket_reaction_time, :caption => :label_helpdesk_ticket_reaction_time, :sortable => "#{HelpdeskTicket.table_name}.reaction_time") + @available_columns << QueryColumn.new(:ticket_first_response_time, :caption => :label_helpdesk_ticket_first_response_time, :sortable => "#{HelpdeskTicket.table_name}.first_response_time") + @available_columns << QueryColumn.new(:ticket_resolve_time, :caption => :label_helpdesk_ticket_resolve_time, :sortable => "#{HelpdeskTicket.table_name}.resolve_time") + @available_columns << QueryColumn.new(:vote, :caption => :label_helpdesk_vote, :sortable => "#{HelpdeskTicket.table_name}.vote") + @available_columns << QueryColumn.new(:vote_comment, :caption => :label_helpdesk_vote_comment) + else + available_columns_without_helpdesk + end + @available_columns + end + + def available_filters_with_helpdesk + # && !RedmineHelpdesk.settings[:issues_filters] + if @available_filters.blank? && User.current.allowed_to?(:view_helpdesk_tickets, project, :global => true) + selected_customer = filters['customer'].present? ? Contact.visible.where(:id => filters['customer'][:values]).map { |c| [c.name, c.id.to_s] } : [] + add_available_filter('customer', :type => :list_optional, :field_format => 'contact', :name => l(:label_helpdesk_contact), + :values => selected_customer) unless available_filters_without_helpdesk.key?('customer') + + add_available_filter('ticket_source', :type => :list, :name => l(:label_helpdesk_ticket_source), + :values => helpdesk_tickets_source_for_select) unless available_filters_without_helpdesk.key?('ticket_source') + + add_available_filter('customer_company', :type => :string, :name => l(:label_helpdesk_contact_company)) unless available_filters_without_helpdesk.key?('customer_company') + + add_available_filter('ticket_reaction_time', :type => :integer, :name => l(:label_helpdesk_ticket_reaction_time)) unless available_filters_without_helpdesk.key?('ticket_reaction_time') + + add_available_filter('ticket_first_response_time', :type => :integer, :name => l(:label_helpdesk_ticket_first_response_time)) unless available_filters_without_helpdesk.key?('ticket_first_response_time') + + add_available_filter('ticket_resolve_time', :type => :integer, :name => l(:label_helpdesk_ticket_resolve_time)) unless available_filters_without_helpdesk.key?('ticket_resolve_time') + + add_available_filter('vote', :type => :list_optional, :name => l(:label_helpdesk_vote), + :values => [[l(:label_helpdesk_mark_awesome), '2'], [l(:label_helpdesk_mark_justok), '1'], [l(:label_helpdesk_mark_notgood), '0']]) unless available_filters_without_helpdesk.key?('vote') + + else + available_filters_without_helpdesk + end + @available_filters + end + end + end + end +end + +unless IssueQuery.included_modules.include?(RedmineHelpdesk::Patches::IssueQueryPatch) + IssueQuery.send(:include, RedmineHelpdesk::Patches::IssueQueryPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issues_controller_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issues_controller_patch.rb new file mode 100644 index 0000000..20bb795 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/issues_controller_patch.rb @@ -0,0 +1,82 @@ +module RedmineHelpdesk + module Patches + + module IssuesControllerPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + # before_filter :apply_helpdesk_macro, :only => :update + after_filter :flash_helpdesk, :only => :update + after_filter :send_auto_answer, :only => :create + + alias_method_chain :build_new_issue_from_params, :helpdesk + alias_method_chain :update_issue_from_params, :helpdesk + helper :helpdesk + end + end + + module InstanceMethods + + def flash_helpdesk + if @issue.current_journal.is_send_note + render_send_note_warning_if_needed(@issue.current_journal) + flash[:notice] = flash[:notice].to_s + " " + l(:notice_email_sent, "" + @issue.current_journal.journal_message.to_address + "") if @issue.current_journal.send_note_errors.blank? + end + end + + def send_auto_answer + return unless @issue && @issue.customer && User.current.allowed_to?(:send_response, @project) + case params[:helpdesk_send_as].to_i + when HelpdeskTicket::SEND_AS_NOTIFICATION + msg = HelpdeskMailer.auto_answer(@issue.customer, @issue).deliver + when HelpdeskTicket::SEND_AS_MESSAGE + if msg = HelpdeskMailer.initial_message(@issue.customer, @issue, params).deliver + @issue.helpdesk_ticket.message_id = msg.message_id + @issue.helpdesk_ticket.is_incoming = false + @issue.helpdesk_ticket.from_address = @issue.customer.primary_email + @issue.helpdesk_ticket.save + end + end + flash[:notice].blank? ? flash[:notice] = l(:notice_email_sent, "" + msg.to_addrs.first + "") : flash[:notice] << " " + l(:notice_email_sent, "" + msg.to_addrs.first + "") if msg + rescue Exception => e + flash[:error].blank? ? flash[:error] = e.message : flash[:error] << " " + e.message + end + + def update_issue_from_params_with_helpdesk + is_updated = update_issue_from_params_without_helpdesk + return false unless is_updated + if params[:helpdesk] && params[:helpdesk][:is_send_mail] && User.current.allowed_to?(:send_response, @project) && @issue.customer + @issue.current_journal.build_journal_message + journal_params = params[:journal_message].merge(Hash[params[:journal_message].slice('to_address', 'cc_address', 'bcc_address'). + map { |k, v| [k, v.join(',')] }]) if params[:journal_message] + @issue.current_journal.journal_message.update_attributes(journal_params) + @issue.current_journal.journal_message.to_address ||= @issue.customer.primary_email + @issue.current_journal.is_send_note = true + @issue.current_journal.notes = HelpdeskMailer.apply_macro(@issue.current_journal.notes, @issue.customer, @issue, User.current) + end + is_updated + end + + def build_new_issue_from_params_with_helpdesk + build_new_issue_from_params_without_helpdesk + return if @issue.blank? || params[:customer_id].blank? + contact = Contact.visible.find_by_id(params[:customer_id]) + @issue.build_helpdesk_ticket(:issue => @issue, :ticket_date => Time.now, :customer => contact) if contact + @issue.helpdesk_ticket.source = params[:source] if params[:source] + end + + def render_send_note_warning_if_needed(journal) + return false if journal.blank? || journal.journal_message.blank? + flash[:warning] = flash[:warning].to_s + " " + l(:label_helpdesk_email_sending_problems) + ": " + journal.send_note_errors unless journal.send_note_errors.blank? + end + + end + end + end +end + +unless IssuesController.included_modules.include?(RedmineHelpdesk::Patches::IssuesControllerPatch) + IssuesController.send(:include, RedmineHelpdesk::Patches::IssuesControllerPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journal_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journal_patch.rb new file mode 100644 index 0000000..ce4ce44 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journal_patch.rb @@ -0,0 +1,84 @@ +module RedmineHelpdesk + module Patches + + module JournalPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable # Send unloadable so it will not be unloaded in development + has_one :contact, :through => :journal_message + has_one :journal_message, :dependent => :destroy + + attr_accessor :is_send_note + attr_accessor :send_note_errors + + after_create :send_note + after_create :update_helpdesk_ticket + end + end + + + module InstanceMethods + + def is_incoming? + self.journal_message && self.journal_message.is_incoming? + end + + def is_sent? + self.journal_message && !self.journal_message.is_incoming? + end + + def message_author + self.is_incoming? ? self.contact : self.user + end + + def helpdesk_ticket + self.journalized.respond_to?(:helpdesk_ticket) && self.journalized.helpdesk_ticket + end + + + def send_note + require 'timeout' + if self.issue.customer && self.is_send_note && self.notes + journal_message = self.journal_message + begin + response_options = {:to_address => journal_message.to_address, :cc_address => journal_message.cc_address, :bcc_address => journal_message.bcc_address} + Timeout::timeout(60) do + HelpdeskMailer.with_activated_perform_deliveries do + if msg = HelpdeskMailer.issue_response(self.issue.customer, self, response_options).deliver + journal_message.message_date = msg.date + journal_message.is_incoming = false + journal_message.message_id = msg.message_id.to_s.slice(0, 255) + journal_message.source = HelpdeskTicket::HELPDESK_EMAIL_SOURCE + journal_message.contact = Contact.find_by_emails([msg.to_addrs.first]).first || self.issue.customer + journal_message.to_address = msg.to_addrs.first.to_s.slice(0, 255) + journal_message.cc_address = msg.cc.join(', ').to_s.slice(0, 255) + journal_message.bcc_address = msg.bcc.join(', ').to_s.slice(0, 255) + journal_message.save! + end + end + end + rescue Exception => e + self.send_note_errors = e.message + end + end + end + + + + def update_helpdesk_ticket + return false if helpdesk_ticket.blank? || (helpdesk_ticket && helpdesk_ticket.ticket_date.blank?) + helpdesk_ticket.save + end + + end + + + end + end +end + +unless Journal.included_modules.include?(RedmineHelpdesk::Patches::JournalPatch) + Journal.send(:include, RedmineHelpdesk::Patches::JournalPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journals_controller_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journals_controller_patch.rb new file mode 100644 index 0000000..d83cc13 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/journals_controller_patch.rb @@ -0,0 +1,44 @@ +module RedmineHelpdesk + module Patches + + module JournalsControllerPatch + def self.included(base) # :nodoc: + base.send(:include, InstanceMethods) + + base.class_eval do + alias_method_chain :new, :helpdesk + end + end + + module InstanceMethods + def new_with_helpdesk + @journal = Journal.visible.find(params[:journal_id]) if params[:journal_id] + if @journal + user = @journal.user + text = @journal.notes + if user.anonymous? && @journal.contact + user = @journal.contact + end + else + user = @issue.author + text = @issue.description + if user.anonymous? && @issue.customer + user = @issue.customer + end + end + # Replaces pre blocks with [...] + text = text.to_s.strip.gsub(%r{
        ((.|\s)*?)
        }m, '[...]') + @content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> " + @content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" + rescue ActiveRecord::RecordNotFound + render_404 + end + + end + end + end +end + +unless JournalsController.included_modules.include?(RedmineHelpdesk::Patches::JournalsControllerPatch) + JournalsController.send(:include, RedmineHelpdesk::Patches::JournalsControllerPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/mail_handler_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/mail_handler_patch.rb new file mode 100644 index 0000000..c00271a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/mail_handler_patch.rb @@ -0,0 +1,99 @@ +module RedmineHelpdesk + module Patches + module MailHandlerPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method_chain :receive_issue_reply, :handle_helpdesk + alias_method_chain :cleanup_body, :handle_helpdesk + alias_method_chain :dispatch, :handle_helpdesk + end + end + + module InstanceMethods + def receive_issue_reply_with_handle_helpdesk(issue_id, from_journal=nil) + journal = receive_issue_reply_without_handle_helpdesk(issue_id, from_journal) + helpdesk_receive_issue_reply(issue_id, journal, from_journal) + end + + private + + def dispatch_with_handle_helpdesk + if email_tag + tag_issue = Issue.where(:id => email_tag[/\+(\d+)@/, 1].to_i).first + return dispatch_without_handle_helpdesk unless tag_issue + receive_issue_reply(tag_issue.id) + else + dispatch_without_handle_helpdesk + end + rescue MissingInformation => e + logger.error "#{email && email.message_id}: missing information from #{user}: #{e.message}" if logger + false + rescue UnauthorizedAction => e + logger.error "#{email && email.message_id}: unauthorized attempt from #{user}" if logger + false + rescue Exception => e + # TODO: send a email to the user + logger.error "#{email && email.message_id}: dispatch error #{e.message}" if logger + false + end + + def email_tag + (email.to.present? && email.to.find { |email| email.match(/\+\d+@/) }) || + (email.cc.present? && email.cc.find { |email| email.match(/\+\d+@/) }) + end + + def helpdesk_receive_issue_reply(issue_id, journal, from_journal=nil) + return unless journal + return journal if journal.notes.blank? + project = journal.issue.project + return journal unless journal.user.allowed_to?(:send_response, journal.issue.project) && journal.issue.customer + + unless HelpdeskSettings["send_note_by_default", project] + regexp = /^@@sendmail@@\s*$/ + return journal unless journal.notes.match(regexp) + journal.notes = journal.notes.gsub(regexp, '') + end + + contact = journal.issue.customer + + begin + HelpdeskMailer.with_activated_perform_deliveries do + if msg = HelpdeskMailer.issue_response(contact, journal).deliver + JournalMessage.create(:to_address => msg.to_addrs.first.to_s.slice(0, 255), + :is_incoming => false, + :message_date => Time.now, + :message_id => msg.message_id.to_s.slice(0, 255), + :source => HelpdeskTicket::HELPDESK_EMAIL_SOURCE, + :cc_address => msg.cc.join(', ').to_s.slice(0, 255), + :bcc_address => msg.bcc.join(', ').to_s.slice(0, 255), + :contact => contact, + :journal => journal) + journal.issue.assigned_to = User.current unless journal.issue.assigned_to + journal.issue.status_id = HelpdeskSettings[:helpdesk_new_status, journal.issue.project_id] unless HelpdeskSettings[:helpdesk_new_status, journal.issue.project_id].blank? + journal.issue.save + HelpdeskLogger.info "#{msg.message_id}: Replay sent to #{contact.name} - [#{contact.emails.first}]" if HelpdeskLogger + end + end + rescue Exception => e + HelpdeskLogger.info "Error of replay sending to #{contact.name} - [#{contact.emails.first}], #{e.message}" if HelpdeskLogger + end + + journal.save! + journal + end + + def cleanup_body_with_handle_helpdesk(body) + cleanup_body_without_handle_helpdesk(body).gsub(' ', '') + end + end + + end + end +end + +unless MailHandler.included_modules.include?(RedmineHelpdesk::Patches::MailHandlerPatch) + MailHandler.send(:include, RedmineHelpdesk::Patches::MailHandlerPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/projects_helper_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/projects_helper_patch.rb new file mode 100644 index 0000000..43960a1 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/projects_helper_patch.rb @@ -0,0 +1,46 @@ +require_dependency 'queries_helper' + +module RedmineHelpdesk + module Patches + module ProjectsHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + + alias_method_chain :project_settings_tabs, :helpdesk + end + end + + module InstanceMethods + # include ContactsHelper + + def project_settings_tabs_with_helpdesk + tabs = project_settings_tabs_without_helpdesk + + helpdesk_tabs = [] + helpdesk_tabs.push({ :name => 'helpdesk', + :action => :edit_helpdesk_settings, + :partial => 'projects/settings/helpdesk_settings', + :label => :label_helpdesk }) + helpdesk_tabs.push({ :name => 'helpdesk_template', + :action => :edit_helpdesk_settings, + :partial => 'projects/settings/helpdesk_template', + :label => :label_helpdesk_template }) + helpdesk_tabs.push({ :name => 'helpdesk_canned_responses', + :action => :manage_canned_responses, + :partial => 'projects/settings/helpdesk_canned_responses', + :label => :label_helpdesk_canned_response_plural }) + helpdesk_tabs.each { |tab| tabs << tab if User.current.allowed_to?(tab[:action], @project) } + tabs + end + end + + end + end +end + +unless ProjectsHelper.included_modules.include?(RedmineHelpdesk::Patches::ProjectsHelperPatch) + ProjectsHelper.send(:include, RedmineHelpdesk::Patches::ProjectsHelperPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/queries_helper_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/queries_helper_patch.rb new file mode 100644 index 0000000..f08e140 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/queries_helper_patch.rb @@ -0,0 +1,63 @@ +require_dependency 'queries_helper' + +module RedmineHelpdesk + module Patches + module QueriesHelperPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method_chain :column_content, :helpdesk + end + end + + + module InstanceMethods + include ContactsHelper + + + def column_content_with_helpdesk(column, issue) + if column.name.eql?(:last_message) && issue.helpdesk_ticket && issue.is_a?(Issue) + content_tag(:span, '', :class => "icon #{issue.helpdesk_ticket.last_message.is_incoming ? 'icon-email' : 'icon-email-to'}") + + link_to(content_tag(:span, content_tag(:small, issue.helpdesk_ticket.last_message.content.truncate(250)), :class => 'description'), + {:controller => 'issues', :action => 'show', :id => issue.id, :anchor => "change-#{issue.helpdesk_ticket.last_message.id}"}) + elsif column.name.eql?(:customer) && issue.is_a?(Issue) + issue.customer ? contact_tag(issue.customer, :type => "plain") : "" + elsif column.name.eql?(:customer_company) && issue.is_a?(Issue) + issue.customer ? issue.customer.company : "" + elsif column.name.eql?(:ticket_source) && issue.is_a?(Issue) + issue.helpdesk_ticket ? issue.helpdesk_ticket.ticket_source_name : "" + elsif column.name.eql?(:ticket_reaction_time) && issue.is_a?(Issue) + issue.ticket_reaction_time + elsif column.name.eql?(:ticket_first_response_time) && issue.is_a?(Issue) + issue.ticket_first_response_time + elsif column.name.eql?(:ticket_resolve_time) && issue.is_a?(Issue) + issue.ticket_resolve_time + elsif column.name.eql?(:last_message_date) && issue.is_a?(Issue) + issue.helpdesk_ticket ? l(:label_helpdesk_ago, time_tag(issue.helpdesk_ticket.last_message_date)) : "" + elsif column.name.eql?(:helpdesk_ticket) && issue.customer && issue.is_a?(Issue) + contact_tag(issue.customer, :size => "32", :type => "avatar", :class => "avatar") + + content_tag(:div, + content_tag(:p, link_to(issue.subject, issue_path(issue)), :class => 'ticket-name') + + content_tag(:p, content_tag(:small, issue.description.gsub("(\n|\r)", "").strip.truncate(100)), :class => "ticket-description") + + content_tag(:p, "#{content_tag('span', '', :class => "icon #{helpdesk_ticket_source_icon(issue.helpdesk_ticket)}", :title => l(:label_note_type_email))} #{l(:label_helpdesk_from)}: #{contact_tag(issue.customer, :type => "plain")}, ".html_safe + l(:label_updated_time, time_tag(issue.helpdesk_ticket.last_message_date)).html_safe, :class => "contact-info"), + :class => 'ticket-data') + # content_tag(:div, content_tag(:span, issue.status.name, :class => "tag-label-color status-#{issue.status.id}" , :style => "background-color: #{tag_color(issue.status.name)}"), :class => "ticket-status") + elsif column.name.eql?(:vote) && issue.helpdesk_ticket && issue.is_a?(Issue) + content_tag(:span, HelpdeskTicket.vote_message(issue.helpdesk_ticket.vote)) if issue.helpdesk_ticket.vote + elsif column.name.eql?(:vote_comment) && issue.helpdesk_ticket && issue.is_a?(Issue) + textilizable(issue.helpdesk_ticket.vote_comment.to_s) + else + column_content_without_helpdesk(column, issue) + end + end + + end + end + end +end + +unless QueriesHelper.included_modules.include?(RedmineHelpdesk::Patches::QueriesHelperPatch) + QueriesHelper.send(:include, RedmineHelpdesk::Patches::QueriesHelperPatch) +end diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/time_report_patch.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/time_report_patch.rb new file mode 100644 index 0000000..ab47508 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/patches/time_report_patch.rb @@ -0,0 +1,38 @@ +module RedmineHelpdesk + module Patches + module TimeReportPatch + def self.included(base) + base.send(:include, InstanceMethods) + + base.class_eval do + unloadable + alias_method_chain :load_available_criteria, :helpdesk + end + end + + + module InstanceMethods + def load_available_criteria_with_helpdesk + @available_criteria = load_available_criteria_without_helpdesk + @available_criteria['customer'] = {:sql => "c_helpdesk_tickets.contact_id", + :kclass => Contact, + :joins => "LEFT OUTER JOIN helpdesk_tickets c_helpdesk_tickets ON c_helpdesk_tickets.issue_id = issues.id", + :label => :label_helpdesk_contact} if User.current.allowed_to?(:view_helpdesk_tickets, @project, :global => true) + @available_criteria['helpdesk_contact_company'] = { + :sql => "hcc_contacts.company", + :kclass => Contact, + :joins => "LEFT OUTER JOIN helpdesk_tickets hcc_helpdesk_tickets ON hcc_helpdesk_tickets.issue_id = issues.id LEFT OUTER JOIN contacts hcc_contacts on hcc_helpdesk_tickets.contact_id = hcc_contacts.id", + :label => :label_helpdesk_contact_company} if User.current.allowed_to?(:view_helpdesk_tickets, @project, :global => true) + + @available_criteria + end + + end + + end + end +end + +unless Redmine::Helpers::TimeReport.included_modules.include?(RedmineHelpdesk::Patches::TimeReportPatch) + Redmine::Helpers::TimeReport.send(:include, RedmineHelpdesk::Patches::TimeReportPatch) +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/wiki_macros/helpdesk_wiki_macro.rb b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/wiki_macros/helpdesk_wiki_macro.rb new file mode 100644 index 0000000..1aba8f4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/redmine_helpdesk/wiki_macros/helpdesk_wiki_macro.rb @@ -0,0 +1,26 @@ +module RedmineHelpdesk + module WikiMacros + + Redmine::WikiFormatting::Macros.register do + desc "Mail icon Macro" + macro :mail do |obj, args| + "" + end + + desc "Helpdesk send_file macro" + macro :send_file do |obj, args| + return "" unless obj.is_a?(Issue) || obj.is_a?(Journal) + issue = obj.is_a?(Journal) ? obj.issue : obj + return "" unless issue.respond_to?(:customer) || (issue.respond_to?(:customer) && issue.customer.blank?) + args, options = extract_macro_options(args, :parent) + raise 'No or bad arguments.' if args.size < 1 + attachment = issue.attachments.where(:filename => args.first).first + + link_to_attachment attachment if attachment + end + end + + + + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk.rake b/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk.rake new file mode 100644 index 0000000..6727222 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk.rake @@ -0,0 +1,51 @@ +namespace :redmine do + namespace :plugins do + namespace :helpdesk do + + desc <<-END_DESC +Update Helpdesk tickets from issue contacts + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + +Examples: + + rake redmine:plugins:helpdesk:update_tickets RAILS_ENV="production" \\ + project=foo +END_DESC + + task :update_tickets => :environment do + return "project should be selected" unless ENV['project'] + + project = Project.find(ENV['project']) + issues = project.issues.includes(:contacts).where("contacts.id IS NOT NULL") + issues.each do |issue| + if issue.helpdesk_ticket.blank? && issue.contacts && contact = issue.contacts.first + helpdesk_ticket = HelpdeskTicket.new(:from_address => contact.primary_email, + :to_address => HelpdeskSettings["helpdesk_answer_from", project.id], + :ticket_date => issue.created_on, + :customer => contact, + :issue => issue, + :source => HelpdeskTicket::HELPDESK_EMAIL_SOURCE) + message_file = issue.attachments.where(:filename => 'message.eml').first + helpdesk_ticket.message_file = message_file if message_file + helpdesk_ticket.save + end + end + + JournalMessage.where(:message_date => nil).each do |message| + message.message_date = message.journal.created_on if message.journal + message.save + end + Attachment.where(:container_type => 'ContactJournal').update_all(:container_type => 'JournalMessage') + + end + + + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk_mail.rake b/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk_mail.rake new file mode 100644 index 0000000..c41dc2c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/lib/tasks/helpdesk_mail.rake @@ -0,0 +1,136 @@ +namespace :redmine do + namespace :email do + namespace :helpdesk do + + desc <<-END_DESC +Read an email from standard input. + +Issue attributes control options: + project=PROJECT identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + +Examples: + + rake redmine:email:helpdesk:read RAILS_ENV="production" \\ + project=foo \\ + tracker=bug < raw_email +END_DESC + + task :read => :environment do + options = { :issue => {} } + %w(project project_id status assigned_to tracker category priority due_date).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:reopen_status] = ENV['reopen_status'] if ENV['reopen_status'] + options[:issue][:project_id] = options[:issue][:project] + HelpdeskMailer.receive(STDIN.read, options) + end + + desc <<-END_DESC +Read emails from an IMAP server. + + +Available IMAP options: + host=HOST IMAP server host (default: 127.0.0.1) + port=PORT IMAP server port (default: 143) + ssl=SSL Use SSL? (default: false) + username=USERNAME IMAP account + password=PASSWORD IMAP password + folder=FOLDER IMAP folder to read (default: INBOX) + +Issue attributes control options: + project_id=PROJECT_ID identifier of the target project + status=STATUS name of the target status + tracker=TRACKER name of the target tracker + category=CATEGORY name of the target category + priority=PRIORITY name of the target priority + reopen_status=STATUS name of the target status afret receive response + allow_override=ATTRS allow email content to override attributes + specified by previous options + ATTRS is a comma separated list of attributes + +Processed emails control options: + move_on_success=MAILBOX move emails that were successfully received + to MAILBOX instead of deleting them + move_on_failure=MAILBOX move emails that were ignored to MAILBOX + +Examples: + rake redmine:email:helpdesk:receive_imap RAILS_ENV="production" \\ + host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\ + project=foo \\ + tracker=bug +END_DESC + + task :receive_imap => :environment do + imap_options = {:host => ENV['host'], + :port => ENV['port'], + :ssl => ENV['ssl'], + :username => ENV['username'], + :password => ENV['password'], + :folder => ENV['folder'], + :move_on_success => ENV['move_on_success'], + :move_on_failure => ENV['move_on_failure']} + + options = { :issue => {} } + %w(project_id project status assigned_to tracker category priority due_date).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:reopen_status] = ENV['reopen_status'] if ENV['reopen_status'] + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:issue][:project_id] = options[:issue][:project] + + RedmineContacts::Mailer.check_imap(HelpdeskMailer, imap_options, options) + end + + desc <<-END_DESC +Read emails from an POP3 server. + +Available POP3 options: + host=HOST POP3 server host (default: 127.0.0.1) + port=PORT POP3 server port (default: 110) + username=USERNAME POP3 account + password=PASSWORD POP3 password + apop=1 use APOP authentication (default: false) + delete_unprocessed=1 delete messages that could not be processed + successfully from the server (default + behaviour is to leave them on the server) + +See redmine:email:helpdesk:receive_pop3 for more options and examples. +END_DESC + + task :receive_pop3 => :environment do + pop_options = {:host => ENV['host'], + :port => ENV['port'], + :apop => ENV['apop'], + :username => ENV['username'], + :password => ENV['password'], + :delete_unprocessed => ENV['delete_unprocessed']} + + options = { :issue => {} } + %w(project_id project status assigned_to tracker category priority due_date).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } + options[:reopen_status] = ENV['reopen_status'] if ENV['reopen_status'] + options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] + options[:issue][:project_id] = options[:issue][:project] + + RedmineContacts::Mailer.check_pop3(HelpdeskMailer, pop_options, options) + end + + desc <<-END_DESC +Receive emails using project settings. + +rake redmine:email:helpdesk:receive RAILS_ENV="production" + +END_DESC + + task :receive => :environment do + Project.active.has_module(:contacts_helpdesk).each do |project| + begin + HelpdeskMailer.check_project(project.id) + rescue Exception => e + puts "Helpdesk MailHandler: can't get mail for project #{project.name} with error: #{e.message}" + end + end + end + + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/canned_responses.yml b/plugins/redmine_contacts_helpdesk/test/fixtures/canned_responses.yml new file mode 100644 index 0000000..62d5bbb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/canned_responses.yml @@ -0,0 +1,14 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +cr_1: + id: 1 + name: Canned response 1 + content: "Hello {%contact.first_name%},\n" + is_public: true + project_id: 1 + +cr_2: + id: 2 + name: Canned response 3 + content: "Hello {%contact.last_name%},\n" + is_public: true + project_id: 1 diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/attachment.zip b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/attachment.zip new file mode 100644 index 0000000000000000000000000000000000000000..1b71a22b34688939b6b67da0dc506b554740a0ea GIT binary patch literal 3855 zcma)%q{NAM zrVgjpfKS*b&y=^8svTG=BEI4fsaE8jFP1r+obFt=EnQ@z?v{eHQ8MGLm3wj*f!Qb( zYPua^p<}^&CG4^V9|ykyt^&79#@q!8Hs#Pd1b2(z&&!M9Ty}9Ns1M2$1MR(b;ak6C zIAe6(gGSIzni=ayz7SceRuJ7O^>lf7K^|3LX`jA7A1pU6EoqwipsZrr0#JhC>=hRh z#hqgftTTT>I}`wOTPB&9|wPrzX69PjAm{6f@!kZ-L{+%WRY;{Mw4WCk;QBoKKJkdaCBo|^?X8^Ti79LZb4LKcA8a5PW zPjg3nni@P!i&Cb8p&HkQc38M<-ZxS;rNJX& z^cmGOGkw}AhsP^1m#^w>mchC|UPAV z7QHl@p1!h(rkT`kQqj-gxPvFk_`$Xf)m7JfGR&?G%sZGvQpyVou@$C*Jk_TfyQjH# zHAqO!!eBFZP0DdYkjysgcV1B{7l>3xM5_5)6)m}PpbnnIj&V-_yPGXH zkF**2phL8z>nVZtr%yNjNYH|mcFX{ECQ)AMdK?Qj^m&a;;MKbi>|5XRq9GE+%AyZk z!^xioz}d7D>|z5L$DoMaJbSw^Jw{k_Oq1SqZe$`JXAw2m1=;5U8d`&k-U!z6SqLRI zY}#j#KLD_S?>6di6A4?1C=oiRZsuMHV$K=+-rcy}!1|N*G(57>H<}B76U>?bZl7Vp zdsU!2mi|S19V7*Lk#drgUp*R>lqG_`nEmz<{_==Azd@ln1}-lNNQAY;d|1QE?hJ$? zKbo2E90(l}`hkDMn(xs?S)PB!m|ZRg+PA6de&@ZOcsj4bmnWBk4DhPQ3#UKKa1xIY*n-SReL^MIIln1=tRwV z473sidLv}pe-~tW#Mk~d3g|5Bz_JwK#}CFgeoKyTRjbuIt@igb zx~a5bP|8^E)3kyil_Sz4JV-sU<~+GUhRLs8zV>efF&C8cgb6X?2Wlz#hOzz91A^V; zB#yL9J8qY5p$EJ7|0|T%e1t5s$any-IVG10Otpsd!L{Ljrbe#zL`H5N2hzT#k2bQ5 z%RdD8CV(}P4xcP67HLec z3Jr3o1MfBlC+UW4uJi((kb7iz@(m=pABSP}o1(otn3gi1Jp`p7gyW@Wd4wr-aW9k0 z#T*6>L+B6X_Oh1sM9%9cKe!4hK3)hL2Rd~1NX&iRPvBHLNm)%rWuh8RO$a9louuuh zqB7u_zcccsQ#SjzqPB21Phcw@!pjdoKbQdeYgt>|B^oSdtRQaBSLiActg4}S*O3pF zvNAW=HjacL-zf?ydSVDX+L)`8IKaGadQw9h&{@(|q9UU)&DZOz1DC#E%Sy!FA&!(0 z%3y@I7(gB=&;F&BLVz>*SMwE_gT+?cvbrN4*#iS%26vjG=YdXC@3r3OPWouu6$WM8 z^8DUjHFQ0U8Cw<`2S7``t7I(IjCLvLsA#cHinHbKNNLjHPU)g0I+8lM-iv^B{0^>z zr6oYgYo?3amCe&brgRk6%bZ>D{jDj1w#;jTIe6Wr!fBA3@pDRlR+}(!>R>5f{a5kS z!ZgAR6NTamv@Eh56eUrmRHfJOSW!B$J^AX10e%%QItBt8)9yyhN->*t`#>h;XK$?2&d07L54(k=Hk{>%eB*3%F*o%$q#-l-_CS3N9s#zD+cu5GK$NjiO@+iut5+x!prz zFv7_))48X%H!GalO5WP_h!feAFu)gH7P)}af@AwOdN{oU>k8?}kxgAR;rz@wZ2>Dl z^;LDN+r;FAjHhOdm1d~VllY^G4i!37)TX!cqjG^N-z(pQe(vy{wad2BwUAn2fa>|w z5ABGb(lPG!sZMmoC-sy^1L?KBwTberAfAe{eXhiWS|$KHm}3RDs93??i}G(V15g9`t~ zGCm)gA5yfZ%TJ;>56`vYI~tb@@y*l8s}EjYca86xO$c(Y8{F{cO{i>ZUn0uDIS1jG ziB01_{$v?pVVeF6(?x9UF3{r@pWlx>hdxT1E*S+F zpUq6oW<^f~-7p?9c9f4ESsj`{N-b8Ek5}6HtcGGkvRQBglonqe1}(|1YB)S}OV?hc z*&T=pm8lk* z2(oKsPO!}@-~Nl2(v@vz+fd)&dJ?CZK&ZccTSuDxT=E6wbw=YPxLq4%aKTsZ&@|q6 z9QB^$%MVG@ptS9$@G2y zR${TRL{5UaYuT@Vkg7HlmsLRAxeqr!h1f#P!jbCw+(3NZP$KM_1V8qK2V0<}p~f{I z$Uz&$&Ve~O7vD@!wJp9=Ur#CbDuo_6Xay3BcSbRQA3F?m6iTBR0%~*}w~hBrfbMx@Q8~l}Clg^b}iI`ld~6o)ddWqVehyxPe>(`%{lAVlb`tjLyc! z=;=8Z(LV{okjdfUxj!o^pz|>yNiUq|Wcz_|Iia^Ge7HM>(4N{!>ga>uENEs%d+&J1 zkfQZt60{7h7s{r;%&2LBf>yqT`wXr083lO0N9aMZ57E7f7QIgqR4fZWkIxiIlRJa$ zfn#S&?wpYADst;`;khm(eZC?JYr09F<|O@cJ;|YMu<3CC3t+bd#<=Jct|qqGfAubA#*b zd%f@RDar^Z{BH2l{FZ-R#q&B_GfVES&U*%U=8Hv;)J*)F$?8otUFTr8((plL zQt`Mc5k+I&!?SjrTOQ=;9j4hWQ_jP)Ue*Q9V1upuK~w)ImKgl#1= z{y@z-!>0b9`~x+Ap8aPmV*2|TzxDrzkZhk)*g8Ia_VgI6ZG22(9IY3E@tzu!gOpB4 zP>=xgIWYU{2Xq=d!DX~~W@bfFN=QEd7_Vy3g+(!ok+*(h(b^LhmSNbsFa_&L?t!V4 zxmr_=!=^08SC`lGrlw@Jdct56d4?saN(x<{LKSfv|j_lb-5Y1Kj}{oW!~^gni?V^CoJtMk9DBuBgV z^B+s0AoS1SKR1u}Pdxlbwf|=SzpDLKT)w~jH4MCe4&(m{`dcj1zX_ru`m4l$8_eI@ TuyOwMfO#Lg_d5ykpWFWc9=k51 literal 0 HcmV?d00001 diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer.eml new file mode 100644 index 0000000..f0a7d6b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer.eml @@ -0,0 +1,67 @@ +Message-ID: <507be7af14d32_249d42840984425b@redminecrm.mail> +From: "Ivan Ivanov" +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_507be7aed2b10_249d428409843932"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-Redmine-Issue-ID: 5 +X-Auto-Response-Suppress: oof + + +----==_mimepart_507be7aed2b10_249d428409843932 +Date: Mon, 15 Oct 2012 14:38:39 +0400 +Mime-Version: 1.0 +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Content-ID: <507be7af13d2d_249d428409844181@redminecrm.mail> + + + + + + + + + +
        +

        Hello, User

        + + +

        We hereby confirm that we have received your message.

        + + +

        We will handle your request and get back to you as soon as possible.

        + + +

        Your request has been assigned the following case ID #579.

        + + +
        + += + + +----==_mimepart_507be7aed2b10_249d428409843932 +Date: Mon, 15 Oct 2012 14:38:39 +0400 +Mime-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Content-ID: <507be7af129a3_249d428409844073@redminecrm.mail> + +Hello, User + +We hereby confirm that we have received your message. + +We will handle your request and get back to you as soon as possible. + +Your request has been assigned the following case ID #579. + +----==_mimepart_507be7aed2b10_249d428409843932-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer_exchange.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer_exchange.eml new file mode 100644 index 0000000..f130bdd --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/auto_answer_exchange.eml @@ -0,0 +1,67 @@ +Message-ID: <507be7af14d32_249d42840984425b@redminecrm.mail> +From: "Ivan Ivanov" +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_507be7aed2b10_249d428409843932"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-Redmine-Issue-ID: 5 +X-Auto-Response-Suppress: All + + +----==_mimepart_507be7aed2b10_249d428409843932 +Date: Mon, 15 Oct 2012 14:38:39 +0400 +Mime-Version: 1.0 +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Content-ID: <507be7af13d2d_249d428409844181@redminecrm.mail> + + + + + + + + + +
        +

        Hello, User

        + + +

        We hereby confirm that we have received your message.

        + + +

        We will handle your request and get back to you as soon as possible.

        + + +

        Your request has been assigned the following case ID #579.

        + + +
        + += + + +----==_mimepart_507be7aed2b10_249d428409843932 +Date: Mon, 15 Oct 2012 14:38:39 +0400 +Mime-Version: 1.0 +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +Content-ID: <507be7af129a3_249d428409844073@redminecrm.mail> + +Hello, User + +We hereby confirm that we have received your message. + +We will handle your request and get back to you as soon as possible. + +Your request has been assigned the following case ID #579. + +----==_mimepart_507be7aed2b10_249d428409843932-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/emoji_message.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/emoji_message.eml new file mode 100644 index 0000000..30181c0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/emoji_message.eml @@ -0,0 +1,66 @@ +Received: from mxfront2g.mail.yandex.net ([127.0.0.1]) + by mxfront2g.mail.yandex.net with LMTP id HpKdDHaF + for ; Wed, 16 Aug 2017 22:39:49 +0300 +Received: from mail-it0-x22a.google.com (mail-it0-x22a.google.com [2607:f8b0:4001:c0b::22a]) + by mxfront2g.mail.yandex.net (nwsmtp/Yandex) with ESMTPS id CkF1hmhCpF-dm5KxxmC; + Wed, 16 Aug 2017 22:39:48 +0300 + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (Client certificate not present) +Return-Path: from@test.com +X-Yandex-Front: mxfront2g.mail.yandex.net +X-Yandex-TimeMark: 1502912388 +Authentication-Results: mxfront2g.mail.yandex.net; spf=pass (mxfront2g.mail.yandex.net: domain of gmail.com designates 2607:f8b0:4001:c0b::22a as permitted sender, rule=[ip6:2607:f8b0:4000::/36]) smtp.mail=from@test.com; dkim=pass header.i=@gmail.com +X-Yandex-Spam: 1 +Received: by mail-it0-x22a.google.com with SMTP id 77so22201626itj.1 + for ; Wed, 16 Aug 2017 12:39:48 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=20161025; + h=mime-version:from:date:message-id:subject:to; + bh=UHGJb8sOGf/6ndRtrbuC2t6WSvmQD0HZSDWcSYs/H2Y=; + b=BRCN9PX18bn/K5ZAKDN7vbDqN6ZYwhfwpsAKagN/NhaJuBTzN1GvDP4rmIg/Z20Owx + Cuog+nYwyxCfN2y/O3POBVh50JNOKklyze55izLohhjf6FL986+mbSGzJntbyuZPQc+Y + 8OxKtU+XQQz4A72wtAFjEwVqRYe852iv2i96Ft0ZoiVv5xwhV//86EZMvDQao3yyIJ8a + BVmRlN0dkWyC2LA7ndpsfhyRfxA9fS85RLdI7Ks5op8QNavu1DYJfiM3FjXFQFJVosua + NtWaxlkzfHFOTHaOSgm1g5WuuPZn2ARD+b6oEvHAjGzlS8MMqmGSefUetSq9u2sdZNji + pnfw== +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20161025; + h=x-gm-message-state:mime-version:from:date:message-id:subject:to; + bh=UHGJb8sOGf/6ndRtrbuC2t6WSvmQD0HZSDWcSYs/H2Y=; + b=iDWPi98m2Lbx4/Kvn5HHR+5+QiGkoFLJOk+KZGboMsIp0hRpQHqvp2PqvkPkYpLNrS + cZ1LR0nbgu2Mchl1SzTwcsFdHYBw1Y5jwbljO1+MJhZlgpuNRQKsuHsUKLcuOQe2LUb7 + j6kPOFb9fcLwPOA6eO0tmhxr8M8b04SGCYz9byDUK94FeZPgNaf589Nn6VFjGm+v/pcx + EBeh2vbrBrFo6ch6VkO3+B8HOFH2tNH491P6XGQG7FcSADAGz/l1qGjAyendHwnCNwLC + 7lrHua4lK9NCHtxirAbhLf5i/1JoryTrKsVGezZ1ihjmpkyNleuJhwvukzbY5OMXEaLV + ZcJQ== +X-Gm-Message-State: AHYfb5h3SutF5qTRGWjdCFJnugpRij+wAdn3CoVrl5ifo9WojihKfauD + 9W46M810KBP4tt9PZfMR4vvcV3neceZ2phU= +X-Received: by 10.36.1.1 with SMTP id 1mr84112itk.145.1502912388349; Wed, 16 + Aug 2017 12:39:48 -0700 (PDT) +MIME-Version: 1.0 +Received: by 10.2.96.9 with HTTP; Wed, 16 Aug 2017 12:39:47 -0700 (PDT) +From: Test test +Date: Wed, 16 Aug 2017 22:39:47 +0300 +Message-ID: +Subject: Emoji +To: "test.test" +Content-Type: multipart/alternative; boundary="001a1143d24e0943560556e40fed" +X-Yandex-Forward: 7bea14b16de09ca92ba251bdd0ba1be8 + +--001a1143d24e0943560556e40fed +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +=E2=98=BA=EF=B8=8F=F0=9F=98=90 =F0=9F=99=81 + +--=20 +Regards, +Test test + +--001a1143d24e0943560556e40fed +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
        + +--001a1143d24e0943560556e40fed-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_bad_name.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_bad_name.eml new file mode 100644 index 0000000..013feed --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_bad_name.eml @@ -0,0 +1,38 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New" "Customer"" +To: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_unicode_name.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_unicode_name.eml new file mode 100644 index 0000000..dc32db1 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_contact_unicode_name.eml @@ -0,0 +1,38 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "Кирилл Безруков" +To: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_encoded_subject.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_encoded_subject.eml new file mode 100644 index 0000000..a43895f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_encoded_subject.eml @@ -0,0 +1,30 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: +Bcc: +Subject: =?windows-1251?q?170203 =C8?= + =?windows-1251?q?=E7=EC=E5=ED=E5=ED=E8=E5?= + =?windows-1251?q? =EA=F3=F0=F1=EE=E2 ?= + =?windows-1251?q?=E2=E0=EB=FE=F2 =E2 ?= + =?windows-1251?q?=D3=D1?= +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. + +Project: onlinestore +Tracker: Support request diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_french.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_french.eml new file mode 100644 index 0000000..62e564d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_french.eml @@ -0,0 +1,240 @@ +Received: from MLPAR59002HPDE (83.206.52.181) by MIRANDELLE.host-services.ctx + (172.30.1.4) with Microsoft SMTP Server id 14.2.247.3; Tue, 27 Mar 2012 + 12:48:03 +0200 +From: Laurent Hardy +To: , +Subject: email test ok'ok +Date: Tue, 27 Mar 2012 12:41:41 +0200 +Message-ID: <242E665348E04D888212C1092B47EEF5@MLPAR59002HPDE> +Content-Type: multipart/related; + boundary="----=_NextPart_000_002D_01CD0C16.F92E0910" +X-Mailer: Microsoft Office Outlook 11 +X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7600.16807 +Thread-Index: Ac0MBjJMccMG2TKbTTaSd3UzkbpzTA== +Return-Path: laurent.hardy@1001listes.fr +X-MS-Exchange-Organization-AuthSource: MIRANDELLE.host-services.ctx +X-MS-Exchange-Organization-AuthAs: Anonymous +MIME-Version: 1.0 + +------=_NextPart_000_002D_01CD0C16.F92E0910 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_002E_01CD0C16.F92E0910" + +------=_NextPart_001_002E_01CD0C16.F92E0910 +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +email test nok'nok + +the bug is=92here + +=20 + + + + +=20 + +Laurent Hardy=20 +Chef de projet Informatique=20 + +59 Rue de Richelieu=20 +75002 Paris=20 + + 1001listes.fr & = + +1001mariages.com=20 +t=E9l. 01 53 26 29 30 +fax 01 40 38 15 77=20 + +=20 + +=20 + + +------=_NextPart_001_002E_01CD0C16.F92E0910 +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + +
        + +

        email test nok'nok

        + +

        the bug is’here + +

         

        + + + + + + + +
        +

        <= +span style=3D"font-size:7.5pt;font-family:Arial;color:black">

        +
        +

         

        +
        +

        Laurent + Hardy
        +
        Chef de projet Informatique
        +
        + 59 Rue de Richelieu
        + 75002 Paris
        +
        + 1001listes.fr + & 1001mariages.com

        + t=E9l. 01 53 26 29 30
        + fax 01 40 38 15 77

        +
        + +

         

        + +

         

        + +
        + + + + + +------=_NextPart_001_002E_01CD0C16.F92E0910-- + +------=_NextPart_000_002D_01CD0C16.F92E0910 +Content-Type: image/jpeg; name="image001.jpg" +Content-Transfer-Encoding: base64 +Content-ID: + +/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/b +AIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgIC +AwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD +AwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgANAA4AwERAAIRAQMRAf/EAI8AAAEDBQEAAAAAAAAAAAAA +AAkAAQgDBAUHCgYBAAMBAQEAAAAAAAAAAAAAAAABAgMEBRAAAAYCAQMDAgQDCQAAAAAAAQIDBAUG +BwgRABIJIRMUMWFBUXEXIhUW8IHBQlJiIyUKEQACAgEDAQYGAwAAAAAAAAAAARECMSFBEmFRcYGh +MgPwscHRQhMiUkP/2gAMAwEAAhEDEQA/AOMDr0TUQiBQExhApSgJjGMIABSgHIiIjwAAAfX8g6AK +SK6DkgqN10F0wESiogqmsmBgABEonTMYoGAB+nPTgUiSXQcAYUF0VwIbsOKKpFQKb8SmFMxgKYPy +H16QShC4bgsDcV0AcGL3lQFVMFzF/wBRURMChi+n1AOOnDGMDluK4tgcIC5AvcLcFkxXAvAG7hRA +3ugAAP146IEVukMf+4R+wfXoA9q5qtmqdYquRHS562NjcryuOBEQLOzbavSB2zm6xjQxDg3rMbOs +ztW75coovnzdZJAqoILmI8gdRlqwrjLaHyFeNC55jxrRJqoM/CnTt5c9Umv1SBpdSy5ccVY/y9eF +y26CqbCIjXDK73tnElmSEIn8pl7iHIJiBQwlpNL+8EfiwZ8JPyW+3jM31zLluvUBzsBpjkHW/LuK +L/TcbUPG0i1xBn20WDG9+wdINceV2sx0tjquybVjJwDZ2isrErkFNuqRE6hDUpp7iqsMcLAQB7Xs +ZYh8uOvHh9d4yxnMaduqzg7V3N1Rkcb0txb8i5dzdh1tP3XYGRyevBuckscuRGUrY0dQci0lUSRD +Rik0QSK39wh4zT9msiWJIQREXJwniN8mWF7kzrVheanb+a0Y9x5bHdQrSd2gGs7bsy1m+RjK5JRZ +LR/TtjeY7avBj1HajdBdZb2wAqpg60/0T7UPdATOrKJUaSa5IbabWYVwDJyS8HU7pZnMnkuwtlCo +ua1h+hw0lfsuWFsqYe1J7HY9rch8UR4AXh0Q/HpWfFSsieik1xsRmI2e8v37KTWKbVqrTcgMbjKk +xxARh8dYerLcsBibHkI2LwRtG1ChsWTbgAAVXAKrH5UVOYaroklgNYOrDEcukXeDxr48KZP+d56/ +81TjBtNbHORNWUvFxw/mKXq8Iy7hAVpKdeVYzRqkUDHXcrETIAmOADzWnjZ78yHjxAy6SSP7deH7 +zF3ScTMzY3RbRjXOrg5KKIzOTm2VbPkCerDED8fIlq9VIY714gXuUQQHuOUADrWzT9ysFZhhMMqw +DvJP/qY1ovsAHzatmHJGnG3FVmkAMMc9w21wJWMjSFxSeCAInr8VG094Dl3z7CBkTlOYBKIdQrL9 +PF5Sa8xfiQWJcY3Ivjh83GQ4ZYrmGv3km1Lu0M6IHCbqHtWStm5uJdkD04TdRz1JQv5gYOrcq9V0 ++w90BB6soMv4K62tbtycx1aLSFa12Lx474wdHTSL3vDWyZwq6i2IR5C8nF6ds6VAvZ/FwIgHWfu+ +nxRNk4AusP4o9iXj2+5i2L2HLwZMwtyFEhyegh2c8CHoPPp+HWo0GByJj29zcxpfkhvkPbTIsxWP +FvFbHY/kaROUp7kLWwMRWzLEHRYqoS3sVFxF6/YxsNOJKPXDcriejGD1USqHEoHDOUpiFNhKYfea +ok2vkD3aueuOIMvz1qsjLLuTqOrj2tyauNsfwi1mzv7Lj925WmQoVVIbLfqg0cyKFjm2PzZpggIo +LLlVKVRt1rLW30HMYyZpSw+SWrw901hxrbsyS+LKZY79rNVmMUNNez7qryi9hs77BePb4kRS7uGF +xq7BeYf0muSihVEFzidkUi4EOv4ep5Es6bkSFG2yGOMDzVaVUt9O1+zW9xrf7FU13sOxhr8tBLWd +DEd2lKsq5/qoYtuuEqWFkDtkGLgfdFNRQO0eq0md0N+ZH3pgupNbxy7aH0Y3i1t2oWQcu6/i3IDb +9wI9oQFXMniy2sndOyWybI8CCzslPnHTlsQQEDOm6XoPU3ryq6g8EmfMloUppRtZJ2rHIo2fUDap +R9nvUrKUFy8qE/Rr2ctsf0ZnKJALcszRXE12JtzCRVaIWZuSk7TmBNe3flWH6lkVWn3mdo22GAGL +7WdaYubiOSp/h/2W0nuYuKpaXI1bON4Q2BRp7V4VjEuhkqnYgyDGAWUj/lptgXP8gqXtH4HV69nK +fkCXzK2NtwcGLE0Cyfse5pFt2I1W2S1GBhmHFNMusVkJ5pziJmUbVRtkYx/BQ1PyLdcSqw8JH0uU +iDPZh1GNXCLhc6JG4qHCHbjhpjiJMCTa+rG1xkMXYsz41wVmbCO/OwG0WLL4NTtKdfytQs4sq+wY +ydXuMNRbHa8eZXxxIU1q4aJPmEem7YPe0jhN01Kn0lTjaYlQSqo07lLPuNsu6WVKnZhdUC+bPYhc +YwpOuWUKVWbpXcsxeCmKtrWyFiPZWVkoSJo1/rVM+SxNS5BFaQm0FVlURV+IBwClV1vppX4x9Sko +B3D9R/X7/wCPr1QxdABpdHfKpT8e6/yHj88hGGHe3Xj7nZA0jWYJk/SYZu1lsLhZdYLdgW0PXbEy +TaOePVXaMULtmLZZRUrZcG7hw0Wi1NeVNLEtbrI9/wDGhp7lRRe2+Pryl6tX+rSBiOmGHNyLK61T +z9U/lGMctflZO3wSGP7U4jEuCHfApEkV45ADjyYUr2Ti6c9A5NZRFC8+Py14o73OWdqNBqhGIH4V +Xr22NKzTMqk7BP8A9VTsFssj2uVXOUOE002gCYwgAiX6hXNbKz8A5dCG9waUaNf/AMto87O25k1E +xXFtmYMtTbzCoD2gaEqp3knKRkSAByQ8g4B6sAgJ0Gw/8QUUeQ5+wf2/ToAboAXQA/rz9/X9fv0A +Wrn4faX5nxu3/L8n2u3j/b7vp9emAzb4PJvh/E54Hu+N7PPH493tevHQBddIBdAC6AP/2Q== + +------=_NextPart_000_002D_01CD0C16.F92E0910-- + diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact.eml new file mode 100644 index 0000000..79bd40f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact.eml @@ -0,0 +1,44 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Reply-To: +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Support request diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_encode.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_encode.eml new file mode 100644 index 0000000..af4dd78 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_encode.eml @@ -0,0 +1,38 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: =?koi8-r?B?68nSyczMIOLF2tLVy8/X?= +To: +Subject: =?koi8-r?B?8NLP18XSy8E=?= +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru.eml new file mode 100644 index 0000000..242cfb0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru.eml @@ -0,0 +1,94 @@ +Received: from mxfront24.mail.yandex.net ([127.0.0.1]) + by mxfront24.mail.yandex.net with LMTP id 4EJ8b63Q + for ; Tue, 25 Sep 2012 00:04:14 +0400 +Received: from f262.mail.ru (f262.mail.ru [217.69.128.183]) + by mxfront24.mail.yandex.net (nwsmtp/Yandex) with ESMTP id 4DQWhu8P-4DQanULm; + Tue, 25 Sep 2012 00:04:13 +0400 +X-Yandex-Front: mxfront24.mail.yandex.net +X-Yandex-TimeMark: 1348517053 +X-Yandex-Spam: 1 +Authentication-Results: mxfront24.mail.yandex.net; spf=pass (mxfront24.mail.yandex.net: domain of mail.ru designates 217.69.128.183 as permitted sender) smtp.mail=kr.dinara@mail.ru; dkim=pass header.i=@mail.ru +DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=mail.ru; s=mail; + h=Content-Type:Message-ID:Reply-To:Date:Mime-Version:Subject:To:From; bh=6AG9O02D+tdox2tGn+S0CuNnG+vo8JZFW6O07Wi6pbc=; + b=hD+St2tofZQJpnfiWc359M4AcgBOzST7l+oi+F6Ps6qpkArQB2YNE3oAd5eU73otby43JLacIP1k9swxQnZ3pKgrp2bwHRPbfi5TltRjIc69hu1dTOOBlUcBVTeEDSNR; +Received: from mail by f262.mail.ru with local (envelope-from ) + id 1TGEt3-00080T-1r + for admin@rusdsu.ru; Tue, 25 Sep 2012 00:04:13 +0400 +Received: from [91.122.47.102] by e.mail.ru with HTTP; + Tue, 25 Sep 2012 00:04:12 +0400 +From: =?UTF-8?B?0JTQuNC90LDRgNCwINCa0YDQtdC80YfQtdC10LLQsA==?= +To: admin@rusdsu.ru +Subject: =?UTF-8?B?0YDQtdC30YPQu9GM0YLQsNGC0Ysg0YLRg9GA0L3QuNGA0LA=?= +Mime-Version: 1.0 +X-Mailer: mPOP Web-Mail 2.19 +X-Originating-IP: [91.122.47.102] +Date: Tue, 25 Sep 2012 00:04:13 +0400 +Reply-To: =?UTF-8?B?0JTQuNC90LDRgNCwINCa0YDQtdC80YfQtdC10LLQsA==?= +X-Priority: +Message-ID: <1348517052.646495476@f262.mail.ru> +Content-Type: multipart/mixed; + boundary="----ebeAvE4R-OtZ6LOueGKzSIBFf:1348517052" +X-Spam: Not detected +X-Mras: Ok +Return-Path: kr.dinara@mail.ru +X-Yandex-Forward: 4a1d766bfd8c33858729d5998daf62e5 + + +------ebeAvE4R-OtZ6LOueGKzSIBFf:1348517052 +Content-Type: multipart/alternative; + boundary="--ALT--ebeAvE4R1348517052" + + +----ALT--ebeAvE4R1348517052 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: base64 + + +----ALT--ebeAvE4R1348517052 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: base64 + +CjxIVE1MPjxCT0RZPjwvQk9EWT48L0hUTUw+Cg== + +----ALT--ebeAvE4R1348517052-- + +------ebeAvE4R-OtZ6LOueGKzSIBFf:1348517052 +Content-Type: application/octet-stream; name="=?UTF-8?B?0LLQvtGB0YXQvtC00Y/RidC40LUg0LfQstC10LfQtNGLIDIwMTIgNiDRgtGD?= + =?UTF-8?B?0YAueG1s?=" +Content-Disposition: attachment +Content-Transfer-Encoding: base64 + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id2luZG93cy0xMjUxIj8+DQo8RGFuY2VEYXRh +IHZlcnNpb249IjIuMSI+PEdyb3VwRGF0YT48SGVhZGVyIGxhbmd1YWdlPSJSdXNzaWFuIj48VGl0 +bGUgc3RhdHVzPSLQ7vHx6Onx6u7lIPHu8OXi7e7i4O3o5SDq4PIuIEIiIGRhdGVDb21wPSIyMy4w +OS4yMDEyIj7C7vH17uT/+ejlIOfi5efk+yAyMDEyICg2IPLz8Ck8L1RpdGxlPjxDaXR5PtEuLc/l +8uXw4fPw4zwvQ2l0eT48T3JnYW5pemVyPsTo7eDs7iwgwevo7e7iIMXi4+Xt6Ok8L09yZ2FuaXpl +cj48SW5pdGlhdG9yPtTS0SDR4O3q8i3P5fLl8OHz8OPgPC9Jbml0aWF0b3I+PENvdW50cnk+0O7x +8ej/PC9Db3VudHJ5PjxSZWdpb25JRD43ODwvUmVnaW9uSUQ+PFByb2dyYW0gZGV2ZWxvcGVyPSLK +8Oji7vnl6u7iIMTs6PLw6OksIOMuIM3u4u7x6OHo8PHqIiBlbWFpbD0ic2thdGVAYmFsbHJvb20u +cnUiIHVzZXJlbWFpbD0ia3JlbXppYUB5YWhvby5jb20iIHVzZXI9Isrw5ez35eXi4CDE6O3g8OAs +IOMuINHg7eryLc/l8uXw4fPw4yIgdmVyc2lvbj0iNSAoMjEuMDkuMjAxMikiPlNrYXRpbmcgU3lz +dGVtPC9Qcm9ncmFtPjxHcm91cCBwcm9ncmFtPSLR7urw4Pnl7e3u5SDk4u7l4e7w/OUgKDYg8uDt +9uXiKSIgY2xhc3M9IkUiIG5vPSIyNyIgYWdlQ2F0ZWdvcnk9It7t6O7w+y0xIiBwcm9ncmFtUGFy +dD0iMSI+3u3o7vD7LTEsINHu6vDg+eXt7e7lIOTi7uXh7vD85SAoNiDy4O325eIpIChFIOrr4PHx +KTwvR3JvdXA+PEp1ZGdlcz45PC9KdWRnZXM+PENvdXBsZXM+MTU8L0NvdXBsZXM+PC9IZWFkZXI+ +PFJlZ2lzdHJhdGlvbj48Q2xhc3NSZWdpc3RyYXRpb24+PENsYXNzQ291bnQgbmFtZT0iRSI+MTU8 +L0NsYXNzQ291bnQ+PC9DbGFzc1JlZ2lzdHJhdGlvbj48Q291cGxlcz48Q291cGxlIG49IjE3IiBw +bGFjZT0iMSIgY2xhc3M9IkUiIHBvaW50cz0iMywwIiBjbGFzc1BsYWNlPSIxIiBjbGFzc0ludFJl +Zz0iMTUiPjxNYWxlIGZpcnN0TmFtZT0i0eXw4+XpIiBsYXN0TmFtZT0izPPx6+jt7uIiIGNsYXNz +PSJFIiBib29rTnVtYmVyPSIxMDg4MTIiIGJpcnRoRGF5PSIyMy4xMi4yMDAwIiBsYXN0Rmlyc3RO +YW1lPSLM8/Hr6O3u4iDR5fDj5ekiLz48RmVtYWxlIGZpcnN0TmFtZT0izODw6P8iIGxhc3ROYW1l +PSLO8Ovu4uAiIGNsYXNzPSJEIiBib29rTnVtYmVyPSI5MzE3MSIgYmlydGhEYXk9IjE5LjA4LjIw +MDEiIGxhc3RGaXJzdE5hbWU9Is7w6+7i4CDM4PDo/yIgY2xhc3NJbnRSZWc9IjEiIGNsYXNzUGxh +Y2U9IjEiIHBvaW50cz0iMCwwIi8+PENsdWIgY2l0eT0i0S4tz+Xy5fDh8/DjIiBuYW1lPSLU5eXw +6P8iIGNvdW50cnk9ItDu8fHo/yIgdHJlbmVyMUxhc3ROYW1lPSLN4Onk5e3u4uAiIHRyZW5lcjFG +aXJzdE5hbWU9Isv+5Ozo6+AiIHRyZW5lcjJMYXN0TmFtZT0iIiB0cmVuZXIyRmlyc3ROYW1lPSIi +IHJlZ2lvbklkPSI3OCIvPjwvQ291cGxlPjxDb3VwbGUgbj0iMjAiIHBsYWNlPSIyIiBjbGFzcz0i +RSIgcG9pbnRzPSIyLDAiIGNsYXNzUGxhY2U9IjIiIGNsYXNzSW50UmVnPSIxNSI+PE1hbGUgZmly +c3ROYW1lPSLQ7uzg7SIgbGFzdE5hbWU9Isrz7+jtIiBjbGFzcz0iRSIgYm9va051bWJlcj0iNjY4 +MjAiIGJpcnRoRGF5PSIyMy4wMy4yMDAwIiBsYXN0Rmlyc3ROYW1lPSLK8+/o7SDQ7uzg7SIvPjxG +ZW1hbGUgZmlyc3ROYW1lPSLA8Ojt4CIgbGFzdE5hbWU9IsDt8/Xo7eAiIGNsYXNzPSJFIiBib29r +NTMxMjExPC9SZXN1bHQ+PC9EYW5jZT48L0RhbmNlcz48L1JvdW5kPjwvUmVzdWx0cz48L0dyb3Vw +RGF0YT48L0RhbmNlRGF0YT4NCg== + +------ebeAvE4R-OtZ6LOueGKzSIBFf:1348517052-- \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_2.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_2.eml new file mode 100644 index 0000000..db3e080 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_2.eml @@ -0,0 +1,101 @@ +Received: from mxfront36.mail.yandex.net ([127.0.0.1]) + by mxfront36.mail.yandex.net with LMTP id bjl84g1K + for ; Tue, 16 Oct 2012 15:37:45 +0400 +Received: from fe01x03-cgp.akado.ru (fe01x03-cgp.akado.ru [77.232.31.164]) + by mxfront36.mail.yandex.net (nwsmtp/Yandex) with SMTP id bimGLf30-bimeLv2G; + Tue, 16 Oct 2012 15:37:45 +0400 +X-Yandex-Front: mxfront36.mail.yandex.net +X-Yandex-TimeMark: 1350387465 +X-Yandex-Spam: 1 +Authentication-Results: mxfront36.mail.yandex.net; spf=softfail (mxfront36.mail.yandex.net: transitioning domain of list.ru does not designate 77.232.31.164 as permitted sender) smtp.mail=vmsdance@list.ru +Received: from [10.3.59.222] (HELO dualcore) + by fe01-cgp.akado.ru (CommuniGate Pro SMTP 5.2.13) + with ESMTP id 359150071 for admin@rusdsu.ru; Tue, 16 Oct 2012 15:37:23 +0400 +From: "Valeria Sergeeva" +To: +Subject: =?koi8-r?B?Rlc6INLF2tXM2NTB1NkgIuvVwsvBIOvSxc3M0SI=?= +Date: Tue, 16 Oct 2012 15:37:25 +0400 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_004D_01CDABB4.251F8CA0" +X-Mailer: Microsoft Office Outlook 11 +Thread-Index: Ac2rj6rt9KGFnZ0sQ7KkrmpkApy75gAAut7w +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.6157 +Return-Path: vmsdance@list.ru +X-Yandex-Forward: 4a1d766bfd8c33858729d5998daf62e5 + +This is a multi-part message in MIME format. + +------=_NextPart_000_004D_01CDABB4.251F8CA0 +Content-Type: text/plain; + charset="koi8-r" +Content-Transfer-Encoding: base64 + +DQoNCvMg1dfB1sXOycXNLCD3wczF0snRLg0KDQotLS0tLU9yaWdpbmFsIE1lc3NhZ2UtLS0tLQ0K +RnJvbTog+sHS1cLJziDhzMXL08XKIFttYWlsdG86c2tubm92QHlhbmRleC5ydV0gDQpTZW50OiBU +dWVzZGF5LCBPY3RvYmVyIDE2LCAyMDEyIDM6MTYgUE0NClRvOiDsxdLBDQpTdWJqZWN0OiDSxdrV +zNjUwdTZICLr1cLLwSDr0sXNzNEiDQoNCuzF0sEsIMna18nOyS4NCvDSz8fSwc3NwSDB19TPzcHU +yd7F08vJINrBzcXOyczBIMvB1MXHz9LJySBXRFNGIM7BIPfTxdLP09PJytPL1cAsINPFyt7B0w0K +0MXSxcfFzsXSyczJINDSwdfJzNjOzywgySDc1M/UIMbByswg0NLB18nM2M7Zyg0K + +------=_NextPart_000_004D_01CDABB4.251F8CA0 +Content-Type: text/xml; + name="131012.xml" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: attachment; + filename="131012.xml" + + +
        "=CA=F3=E1=EE=EA =CA=F0=E5=EC=EB=FF - = +2012"=CD=E8=E6=ED=E8=E9 = +=CD=EE=E2=E3=EE=F0=EE=E4=C1=E8=E3 =D2=EE=EF, = +=C1=F3=E7=FB=ED=E8=ED=E0 =C8=F0=E8=ED=E0=D4=D2=D1 = +=CD=E8=E6=E5=E3=EE=F0=EE=E4=F1=EA=EE=E9 = +=EE=E1=EB=E0=F1=F2=E8=D0=EE=F1=F1=E8=FF52Skating System=C4=E5=F2=E8-2, =E4=EE = +E = +=EA=EB=E0=F1=F1=E01330228 + + +------=_NextPart_000_004D_01CDABB4.251F8CA0-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_4.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_4.eml new file mode 100644 index 0000000..304c6f1 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_4.eml @@ -0,0 +1,59 @@ +Return-Path: +Received: from mxback3h.mail.yandex.net ([127.0.0.1]) by mxback3h.mail.yandex.net with LMTP id 4M50bUaA for ; Tue, 16 Oct 2012 22:04:22 +0400 +Received: from web19h.yandex.ru (web19h.yandex.ru [84.201.186.48]) by mxback3h.mail.yandex.net (nwsmtp/Yandex) with ESMTP id 4MSWFxBe-4MSWR5Mr; Tue, 16 Oct 2012 22:04:22 +0400 +Received: from 127.0.0.1 (localhost.localdomain [127.0.0.1]) by web19h.yandex.ru (Yandex) with ESMTP id 4AA0919A8209; Tue, 16 Oct 2012 22:04:22 +0400 +Received: from 231.232.nat.atknet.ru (231.232.nat.atknet.ru [62.192.231.232]) by web19h.yandex.ru with HTTP; Tue, 16 Oct 2012 22:04:21 +0400 +Date: Tue, 16 Oct 2012 22:04:21 +0400 +From: Bobrov Arkadiy +To: "admin@rusdsu.ru" +Message-ID: <82871350410661@web19h.yandex.ru> +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----==--bound.8288.web19h.yandex.ru"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-Yandex-Front: mxback3h.mail.yandex.net +X-Yandex-Front: web19h.yandex.ru +X-Yandex-TimeMark: 1350410662 +X-Yandex-TimeMark: 1350410662 +Authentication-Results: mxback3h.mail.yandex.net; dkim=pass + header.i=@yandex.ru +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yandex.ru; s=mail; + t=1350410662; bh=xMHVnHp+9VzOismIHXMGilFUQrefO/xoM9XGOIBtHuU=; h=From:To:Date; + b=RkMPnej1dIfy/XfhvBv3wWtfRDLVk/TGElPRwXQuL7nBca8RG/asGnvxKke+CSTm7 + ZdMarFz/nsHoZ6hoYuL7BeJ3LLxb/Wxpq5C6RjDmekDjfQguy0xZz6/578xYCKQ3wf + 3kMVZ0Zc1vqXkpU4VtfDtW9BkjFHRF927UwEr+Cw= +X-Yandex-Spam: 1 +X-Mailer: Yamail [ http://yandex.ru ] 5.0 +X-Yandex-Forward: 4a1d766bfd8c33858729d5998daf62e5 + + + +------==--bound.8288.web19h.yandex.ru +Date: Tue, 16 Oct 2012 22:31:09 +0400 +Mime-Version: 1.0 +Content-Type: text/xml; + charset=UTF-8; + name="=?UTF-8?B?0JrRg9Cx0L7QuiDQntGB0LXQvdC4IC0gMjAxMi54bWw=?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="=?UTF-8?B?0JrRg9Cx0L7QuiDQntGB0LXQvdC4IC0gMjAxMi54bWw=?=" +Content-ID: <507da7ed9ba7d_3cb75082096978e0@redminecrm.mail> + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id2luZG93cy0xMjUxIj8+ +DQo8RGFuY2VEYXRhIHZlcnNpb249IjIuMSI+PEdyb3VwRGF0YT48SGVhZGVy +IGxhbmd1YWdlPSJSdXNzaWFuIj48VGl0bGUgc3RhdHVzPSLQ7vHx6Onx6u7l +IPHu8OXi7e7i4O3o5SDq4PIuIEIiIGRhdGVDb21wPSIxNC4xMC4yMDEyIj7K +8+Hu6iDO8eXt6CAtIDIwMTI8L1RpdGxlPjxDaXR5PsDw9eDt4+Xr/PHqPC9D +aXR5PjxPcmdhbml6ZXI+0fLw5ezr5e3o5SwgwO3y7u3u4iDA6+Xq8eDt5PA8 +L09yZ2FuaXplcj48SW5pdGlhdG9yPtTS0SDA8PXg7ePl6/zx6u7pIO7h6+Dx +8ug8L0luaXRpYXRvcj48Q291bnRyeT7Q7vHx6P88L0NvdW50cnk+PFJlZ2lv +bklEPjI5PC9SZWdpb25JRD48UHJvZ3JhbSBkZXZlbG9wZXI9Isrw6OLu+eXq +7uIgxOzo8vDo6Swg4y4gze7i7vHo4ejw8eoiIGVtYWlsPSJza2F0ZUBiYWxs +cm9vbS5ydSIgdXNlcmVtYWlsPSJzY3JhdC4yOUB5YW5kZXgucnUiIHVzZXI9 +IsHu4fDu4iDA8Org5OjpLCDjLiDA8PXg7ePl6/zx6iIgdmVyc2lvbj0iNSAo +MTAuMTAuMjAxMikiPlNrYXRpbmcgU3lzdGVtPC9Qcm9ncmFtPjxHcm91cCBw +L0dyb3VwRGF0YT48L0RhbmNlRGF0YT4NCg== + + +------==--bound.8288.web19h.yandex.ru-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_5.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_5.eml new file mode 100644 index 0000000..b2ca713 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_new_contact_ru_5.eml @@ -0,0 +1,81 @@ +Return-Path: +Received: from mxback22.mail.yandex.net ([127.0.0.1]) by mxback22.mail.yandex.net with LMTP id WF74ELrK for ; Sat, 20 Oct 2012 00:32:15 +0400 +Received: from web25f.yandex.ru (web25f.yandex.ru [95.108.131.159]) by mxback22.mail.yandex.net (nwsmtp/Yandex) with ESMTP id WFDiBVbp-WFDKCRm5; Sat, 20 Oct 2012 00:32:15 +0400 +Received: from 127.0.0.1 (localhost.localdomain [127.0.0.1]) by web25f.yandex.ru (Yandex) with ESMTP id 6EF92436008E; Sat, 20 Oct 2012 00:32:15 +0400 +Received: from ppp91-76-73-220.pppoe.mtu-net.ru (ppp91-76-73-220.pppoe.mtu-net.ru [91.76.73.220]) by web25f.yandex.ru with HTTP; Sat, 20 Oct 2012 00:32:15 +0400 +Date: Sat, 20 Oct 2012 00:32:15 +0400 +From: info@rusdsu.ru +To: =?koi8-r?B?7cHSwdQg4c3Jzs/X?= , + "admin@rusdsu.ru" +Message-ID: <145841350678735@web25f.yandex.ru> +Subject: =?KOI8-R?Q?Fwd:_=F4=D5=D2=CE=C9=D2_14_=CF=CB=D4=D1=C2=D2=D1_2012?= + =?KOI8-R?Q?_=EB=C1=CC=C9=CE=C9=CE=C7=D2=C1=C4?= +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----==--bound.14585.web25f.yandex.ru"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-Yandex-Front: mxback22.mail.yandex.net +X-Yandex-Front: web25f.yandex.ru +X-Yandex-TimeMark: 1350678735 +X-Yandex-TimeMark: 1350678735 +X-Yandex-Spam: 1 +X-Mailer: Yamail [ http://yandex.ru ] 5.0 +X-Yandex-Forward: 4a1d766bfd8c33858729d5998daf62e5 + + + +------==--bound.14585.web25f.yandex.ru +Date: Sat, 20 Oct 2012 00:36:07 +0400 +Mime-Version: 1.0 +Content-Type: text/plain; + charset=koi8-r +Content-Transfer-Encoding: base64 +Content-ID: <5081b9b7c881_742a496c09a27c3@redminecrm.mail> + +CgotLS0tLS0tLSDwxdLF09nMwcXNz8Ug08/Pwt3FzsnFICAtLS0tLS0tLQoy +MC4xMC4yMDEyLCAwMDoxNiwgIvTB1NjRzsEg98neydTFIiA8dmljaGl0ZS50 +QGdtYWlsLmNvbT46CgoKLS0tLS0tLS0g+sHXxdLbxc7JxSDQxdLF09nMwcXN +z8fPINPPz8Ldxc7J0SAtLS0tLS0tLQ== + + +------==--bound.14585.web25f.yandex.ru +Date: Sat, 20 Oct 2012 00:36:07 +0400 +Mime-Version: 1.0 +Content-Type: text/xml; + charset=UTF-8; + name="Kaliningrad 14102012.xml" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="Kaliningrad 14102012.xml" +Content-ID: <5081b9b7e21a_742a496c09a28d2@redminecrm.mail> + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id2luZG93cy0xMjUxIj8+ +DQo8RGFuY2VEYXRhIHZlcnNpb249IjIuMSI+PEdyb3VwRGF0YT48SGVhZGVy +IGxhbmd1YWdlPSJSdXNzaWFuIj48VGl0bGUgc3RhdHVzPSLQ7vHx6Onx6u7l +IPHu8OXi7e7i4O3o5SDq4PIuIEIiIGRhdGVDb21wPSIxNC4xMC4yMDEyIj7O +8eXt/CAyMDEyPC9UaXRsZT48Q2l0eT7K4Ovo7ejt4/Dg5DwvQ2l0eT48T3Jn +YW5pemVyPs7t6O7t4CwgzODw8vvt5e3q7iDR5fDj5ek8L09yZ2FuaXplcj48 +SW5pdGlhdG9yPtTS0SDK4Ovo7ejt4/Dg5PHq7ukg7uHr4PHy6DwvSW5pdGlh +dG9yPjxDb3VudHJ5PtDu8fHo/zwvQ291bnRyeT48UmVnaW9uSUQ+Mzk8L1Jl +Z2lvbklEPjxQcm9ncmFtIGRldmVsb3Blcj0iyvDo4u755eru4iDE7Ojy8Ojp +LCDjLiDN7uLu8ejh6PDx6iIgZW1haWw9InNrYXRlQGJhbGxyb29tLnJ1IiB1 +c2VyZW1haWw9InZpY2hpdGUudEBnbWFpbC5jb20iIHVzZXI9IsLo9+jy5SDS +Lt4uLCDjLiDK4Ovo7ejt4/Dg5CIgdmVyc2lvbj0iNSAoMDQuMTAuMjAxMiki +PlNrYXRpbmcgU3lzdGVtPC9Qcm9ncmFtPjxHcm91cCBwcm9ncmFtPSI0IPLg +7fbgICjB4Ov87fvlKSIgY2xhc3M9IkgiIG5vPSI5IiBhZ2VDYXRlZ29yeT0i +xOXy6C0xIiBwcm9ncmFtUGFydD0iMiI+xOXy6C0xIDIwMDUg6CDs6y4gKEgg +6uvg8fEpPC9Hcm91cD48SnVkZ2VzPjc8L0p1ZGdlcz48Q291cGxlcz4yPC9D +b3VwbGVzPjwvSGVhZGVyPjxSZWdpc3RyYXRpb24+PENsYXNzUmVnaXN0cmF0 +aW9uPjxDbGFzc0NvdW50IG5hbWU9IkgiPjI8L0NsYXNzQ291bnQ+PC9DbGFz +c1JlZ2lzdHJhdGlvbj48Q291cGxlcz48Q291cGxlIG49IjU1IiBwbGFjZT0i +MSIgY2xhc3M9IkgiIHBvaW50cz0iMCwwIiBjbGFzc1BsYWNlPSIxIiBjbGFz +c0ludFJlZz0iMiI+PE1hbGUgZmlyc3ROYW1lPSLN6Oro8uAiIGxhc3ROYW1l +PSLK7u3u4uDr5e3q7iIgY2xhc3M9IkgiIGJvb2tOdW1iZXI9IiIgYmlydGhE +YXk9IiIgbGFzdEZpcnN0TmFtZT0iyu7t7uLg6+Xt6u4gzejq6PLgIi8+PEZl +PFJlc3VsdCBuPSIxMzEiIHBsYWNlPSIyIiBoZWFkPSIxIj4xMjIyMjwvUmVz +dWx0PjwvRGFuY2U+PC9EYW5jZXM+PC9Sb3VuZD48L1Jlc3VsdHM+PC9Hcm91 +cERhdGE+PC9EYW5jZURhdGE+DQo= + + +------==--bound.14585.web25f.yandex.ru-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_no_subject.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_no_subject.eml new file mode 100644 index 0000000..d0903d7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_no_subject.eml @@ -0,0 +1,37 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_to_contact.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_to_contact.eml new file mode 100644 index 0000000..f14cd9d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_to_contact.eml @@ -0,0 +1,38 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Subject: New support issue to Ivan +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_without_text_with_attachment.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_without_text_with_attachment.eml new file mode 100644 index 0000000..73cc94d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/new_issue_without_text_with_attachment.eml @@ -0,0 +1,37 @@ +Delivered-To: support@somenet.foo +Received: by 10.79.28.144 with SMTP id c138csp1453602ivc; + Mon, 21 Nov 2016 04:22:54 -0800 (PST) +X-Received: by 10.36.14.21 with SMTP id 21mr7666872ite.79.1479730974526; + Mon, 21 Nov 2016 04:22:54 -0800 (PST) +Return-Path: +Received: from mx.@somenet.foo (h-8.148.somenet.foo. [127.0.0.1]) + by mx.google.com with SMTP id 125si13733354iou.236.2016.11.21.04.22.52 + for ; + Mon, 21 Nov 2016 04:22:54 -0800 (PST) +Received-SPF: neutral (google.com: 127.0.0.1 is neither permitted nor denied by best guess record for domain of admin@somenet.foo) client-ip=127.0.0.1; +Authentication-Results: mx.google.com; + spf=neutral (google.com: 127.0.0.1 is neither permitted nor denied by best guess record for domain of admin@somenet.foo) smtp.mailfrom=admin@somenet.foo +Date: Mon, 21 Nov 2016 15:24:12 +0300 +From: +To: +Message-ID: <1938134758.56508.1479731052731.JavaMail.root@mmsr-fe2> +Subject: +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_56507_34102980.1479731052731" + +------=_Part_56507_34102980.1479731052731 +Content-ID: +Content-Location: text_0.txt +Content-Type: text/plain; name=text_0.txt; charset=utf-8 +Content-Disposition: inline +Content-Transfer-Encoding: base64 + +0JTQvtCx0YDRi9C5INC00LXQvdGMLiDQktGH0LXRgNCwICwg0LHRi9C7INC/0YDQvtC40LfQstC1 +0LTQtdC9LCDQv9C10YDQtdCy0L7QtCDQtNC10L3QtdC20L3Ri9GFINGB0YDQtdC00YHRgtCyICDQ +siDRgNCw0LfQvNC10YDQtSAxNTAwLCDRgSDQutCw0YDRgtC+0YfQutC4INCh0L7QstC60L7QvNCx +0LDQvdC6INC90LAg0LrQsNGA0YLQvtGH0LrRgyDQodCx0LXRgNCx0LDQvdC60LAuINCU0LXQvdC1 +0LMg0YMg0LDQv9C/0LDQvdC10L3RgtCwINC00L4g0YHQuNGFINC/0L7RgCDQvdC10YIuICDQp9GC +0L4g0LTQtdC70LDRgtGMPw== + +------=_Part_56507_34102980.1479731052731-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_contact.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_contact.eml new file mode 100644 index 0000000..5e9c420 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_contact.eml @@ -0,0 +1,40 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Cc: +Bcc: +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail.eml new file mode 100644 index 0000000..8edcb9d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail.eml @@ -0,0 +1,26 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +@@sendmail@@ + +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_by_default.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_by_default.eml new file mode 100644 index 0000000..04019f2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_by_default.eml @@ -0,0 +1,24 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail by default. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_keywords.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_keywords.eml new file mode 100644 index 0000000..c4fc3bb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_keywords.eml @@ -0,0 +1,28 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Status: Close + +---- This should be cutted ---- + +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail by default. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag.eml new file mode 100644 index 0000000..06049bb --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag.eml @@ -0,0 +1,26 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: ; +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +@@sendmail@@ + +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag_in_cc.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag_in_cc.eml new file mode 100644 index 0000000..867703e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_from_mail_with_tag_in_cc.eml @@ -0,0 +1,27 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: ; +Cc: , =?koi8-r?B?7cHSwdQg4c3Jzs/X?= , Ivanov Ivan +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +@@sendmail@@ + +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. +This is a reply from mail. This is a reply from mail. This is a reply from mail. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_to_mail.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_to_mail.eml new file mode 100644 index 0000000..a9b71e9 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_to_mail.eml @@ -0,0 +1,44 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Reply-To: foo@bar.com +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. + +Project: onlinestore +Tracker: Support request diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_attachment.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_attachment.eml new file mode 100644 index 0000000..121741c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_attachment.eml @@ -0,0 +1,248 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200 +Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris> +From: +To: +Subject: Re: [Cookbook - Ticket #5] Problem +Date: Sat, 21 Jun 2008 15:53:25 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270" +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +This is a multi-part message in MIME format. + +------=_NextPart_000_001F_01C8D3B6.F05C5270 +Content-Type: multipart/alternative; + boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270" + + +------=_NextPart_001_0020_01C8D3B6.F05C5270 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This is a new ticket with attachments +------=_NextPart_001_0020_01C8D3B6.F05C5270 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
        This is  a new ticket with=20 +attachments
        + +------=_NextPart_001_0020_01C8D3B6.F05C5270-- + +------=_NextPart_000_001F_01C8D3B6.F05C5270 +Content-Type: image/jpeg; + name="Paella.jpg" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="Paella.jpg" + +/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU +FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo +KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCACmAMgDASIA +AhEBAxEB/8QAHQAAAgMBAQEBAQAAAAAAAAAABQYABAcDCAIBCf/EADsQAAEDAwMCBQIDBQcFAQAA +AAECAwQABREGEiExQQcTIlFhcYEUMpEVI0Kh0QhSYrHB4fAWJCUzQ3L/xAAaAQADAQEBAQAAAAAA +AAAAAAADBAUCAQYA/8QAKhEAAgIBBAICAgIDAAMAAAAAAQIAAxEEEiExIkEFE1FhMnFCkaEjwdH/ +2gAMAwEAAhEDEQA/ACTUdSsdhRCNE54GTRaBaXHiBtNOVo0wEpSt8BKfmpWCZRPHcVbdZ3X1J9Jx +Tla9OBpIU8Noo7Gjx4qdrCBkfxGupUSck13GJjeT1ObEdthOG04/zpX8SNXjR1njym46ZMmQ+llp +pStuc9T9hRq/X22afhKl3iazEYHdxWCfgDqT9K83eKfiFG1RfIEi3tuC3W9KlNh0YLqyeuO3QV0D +MznM9O2uai4QI8psYQ8gLA9virY615P034xX+zNNslLDsMKOG1J5HuAa3nQPiBZ9WtpUy4lmcE4U +ypXP2rmMHmcI/EealD7te7ZZ2S7dLhGiN9cvOBP+dIF18btHw3C1DkSbi7nATGZJBPwTitTIyZp9 +SsCun9oJaEFUDTy0oyQFyXSOfoB/rQOL466huE9LIagxW1A48tkuKJxwBlQrm4YzNhGPE9Mmua8Y +JrzsrXPiQ42y7+KtsZt4kpS8ltK0p91J5IzXGFr3xFef8pMqE4vJABZT6se3FDNyEZzNCh89Tfbv +aoV2iKj3GO2+0eyh0+h7VkWq/CqTDUqXpp0uJHPkKOFj6HofvQRzxZ1bbwFTG7c+jO0lKeh+cGi8 +bxrebZZVMtjDqljKgw4Rt9uuea5vEIEceoL09ZnHQoyGy3KaOFhxO0j6g0J8QNPr3tzorHmsJSUv +NgdQeprTIuqbfqdtD7MRxh7HO/H6ZHWlnW0e5tQnv2WgupAyEg8p9xUl7WGowpzKCoDXyJ5nvMdK +Uuho4bSv057CqK2stIWrgEZp2kWtE+O5+MC0OKUchHFCbnaWVNeW1KU3tTtwtAUkj6jkfpXoK7gQ +AZLsqYEmJ0mUBlLeCfeqHKl5PqJopNhriupQWyoqPpKeQfpTXYPDW+3ZlEhTTcVpXI8w+oj6Cmty +qMxTazHAi1ZLG/PXuKClv3Ip7t2n4yI3lKZSsEc7hmicXwfu5ThN22fCUH+tXB4QX1KdzN6WVjth +Q/1oDuG/yjCIV/xgWLouQFfiLK/5LqejbnKT9D1FStX05DRaYrTN8K232wEl1aMJV856VKF9hPc3 +9QPM32HEjxEjykBSh/ERSd4s61uGjLbBnQrcie2t4pfClEFKAM8Y704uvtsMrdfcQ20gZUtZAAHu +SawHxt8V7PKt/wCytPp/aLrToW7JAPlNkAjAPfOfpQ0JY4E42B3Nf09ruwXvTQvjM9lmGkfvvOWE +llXdKvn/ADrONZeNwU28zo2Ml1tHpXc5Y2spP+EHlR/5ivOzYkPPKdjMechRDjrCUHy1Ec9Aa1Lw +l0VF10pcy4XJC0RlbTFTgKbHwnokfSibFXkzAJbiJ0tN81jc1yHXplzkEEqkPA7UjvtR2H1/SrOl +rGu6NvP7Q8yhaWkDruVj/n616Lvl20n4Z2cpeS02tSfRHbAU69/t8nivOGoNXzNQSVRbFAbtsFal +FESEjBOepUR1rBs3D8CFVMHjmXNYW+wWtsMrlMvyyOW4h3FB9irpn70lx7k9AeDttW4w70DgWd3+ +1NmlvDi7XpL0iShcWG0dqllO5SlHsB35NG7l4PSRG823z0YbGFqkDaFK+MZx7d6XOu09Z2M8MKHb +OBM1vBuAkJcuUgyHXRu3KfDp+5ycVTaeU36kKUlYOQQcEVrehvC5l1Mh/VClISHFMttIVgL45VnH +TkEH4rQbjpHTbyGWVQIzL7bYabc2AnaMfYnAxk0K35Smo7e/2IRdC7eXUwfT5m6pfbtC/wARIlLW +VNu7yoN9MlQ9h3NO+n9Cwo8rzZU1Sm2Mlx9YLaUkHjaOv3Nc7zd7FoyY5D07HR56SfMl7961ZGNo +9gKXrtd77dnkssoSwt7K9rZG8jHU44Tkc9q0rvbyvipnNgT9kTRLvqKy2JDgS/8AiH3hjecKXjv2 +/SkG8akmRyhqG+hKSQ4dpyofBxxV2w+Hkuda27pMW5tcSpWxati1HJGQTkYp70xoS2MW1pp+ImXN +koJLi+UtfP1FAt1dFPHcPXQ9nPUy+/3pu4usrYZS16MOKCAkuLJypRxX5aG5ExX4VlfC/Vt98e3z +WvL8M9NsNMtyFyVyGx6h5uPMPyMcV9Q9HQbbdWwzHQGFHKVhStw+uTQTr6tu1IQad85M46baVarV +uVkJ/mDVCVqWUll59t4FxlW0ocOA4k+1P8uLGU35UgAhQ2kgdRWUeIMi2WyKqASFLJJbWchQI7Ul +pWWyw5GSYZ1IXA4Ez7U12mR7q95jCWgTuCQeoPsaGqntylbCpIdxnaSM/wBK56lujtydZS4UkNIw +CBzQO4RURywWnUupcQF7knoT1BHYg5r0lFY2DIwZKvYq5x1DjUo26WzJKEuIQoFSFDIP+9bzaL0x ++HZcZcQpC0ggewIrzYzNJQGpGVt+/cUw2PU8+0vqWEJnW8q/9KzgpHslXb6UV6yw4gBZg8z1NZbj +Ek43LQDjkZFMLbkMcJW3+orKvDq86T1SUssrEef3iPq2rz8f3vtTZrtizaR0pOvD8XephOG2959a +ycJH60HBBxDBhjMB+L9/RY7WpT7jam3kkNNJwSs+/NSss0Bpi4+Jmpfxl7kPOQ2k7iCfyI/hQOwz +/vUroqrUnceZ8LnIG2Cdaa61Dq54i7SVJi5ymGwdjSf/ANe/86s6W0TLvkNySp5pcVjBUy0oAD5x +1P1NbDbPALTQjp/aC5bj+OS27tH+VOmjPDqw6QEv9lNPFcpIQ4p5zeSB0A/WtNYoXCwK1nOWgjwk +sFrg2wuJjtKl5IJUBwPakLxDXbNI6/alaGW6b87uL1vjJCmAogjcvHTrnb8DpVnxj1q1oOS7b9PP +j9qSEErA58gHuf8AF7CsStOurpBjKZioQqS6sqU+vlayepPvQytu3cgz/fEPWaXfFjYEfLlo5+bM +/aurr+X33vW6lIJUD/dyen2p80zboMNG6NBEGOygJLy04cdAGRjjn5NYRD1NcjMMme8XpST6Q4Mp +H0HStstF4kO2lMS5vAlTfq9O04PQZ+KifILaqg3PnPodS5o0S3I0q4x2T3Kr+obzH1HsjuFFpeUU +B5s5Snck4ST0z0p502w5HZW86qW5lXLbpSeMfHFZH4gpFutbDlrmNtujlxvzc705HAHfB5qknVSI +VliuWK7STcHVBL7Ticc8c8f70IaMaipWq4z+oo6jT2sr8ma3qCfBky48be4zvcAOB6gR/CMd6EXF +m9EPKhx3Vx92EJdADmOmQKJ2y5xVpiJlW+OzPSj1LbSBtURyoGjFzWqPbHljClFBLbiBnHHUmpeT +WdqiPISuDM/e0bark4YzkEJkJ9RebGF7u+T/AKVeg6DbVdXHJ6U/hi35KAlRGU44zj/WrtpdfSlt +D7m54jKznr/WnOAVKa9Y7cGtDVWodhaH1WnVlD7cZxPhq3NMobbeBeZQnalKlZ47cUQDSGtvlqwn +GEp7AVQdbddWQHkp2dOea6qWHQlPmJSscEE9aET/AJCK/X+JFxUtuKecHnKxx8VXRKiBSkuKII55 +PSvq4yUQmf3qspxwc8is71fqZMeKtTO0AHn3V8UaitrDgdmcdtoyZ215q1USShq0bZClghTYPqFL +Vr0xH1otbt1XKZkpT6cccfOaF6SZkz7q7dZYWHjz0ykJp2Yvi4YaYVHdUXjs2eSUlR7HPt89KoW5 +p8af5D3OVLldz9GLmsNLR1WZiI+oJlRB5aHgBuKe2cdaxd5tVsuy0OJbdWwvkKGUq+or0PqiyXVy +IJ7za1NlIJbz6m/fgdv61lN000qWJ09EWQ8++6lqM01k8geokY5p/wCK1RXK2Nn/AOz75PS1vStt +Y594iCUnOauWi5SLXMDzIQ4g8ONOp3IcT7KHcVduWn7nbWg5OgSI6SopBcQUjPtzXK1RX1OqkMtb +0xcPO9PSkHrzV0WKRkHM86a2BwZqFm0da9c2pdw0asM3JgBT9qdd2uNH+8y51x7A/rSjrXUmq129 +Om9TuyvKhu70NyUYd4GBlX8QofG1hcLbrBF/tZ/DvtqGEDhJQONpA6gjrXq61f8AS/jDo9mXNhNu +nGxxPR2O5jkBXX+tY3bcFhPtoPAin4H6gsMTQgLEhtM7eoyGioBYI4Tx7Yx+pqUr668ILjZXDOtS +XZsdvlMiGkJlND/GgYDg+Rg1KwUDHIM2r7Bgiei5NwiQo635cllllAypbiwAPvWO678c4UJuRH0y +gSHkDBkrHpz2CR3+prHbXJ1L4o6matwkKaYP7xzkhthsdVEf8NLWrzbo94fh2RKjAjqLSHFnKniO +Cs/X/KuLSAcN3OfYW5HUD3SXJutxfnTnVOyn1lbi1HJJNPnh9otyfbJF5lLabjpJQ0FjlZHUis9C +lDOO9bdHkS4WkbXBlIMdaGUnyhwkjqFfU5pf5K566gqe+I98TpBqb9pnB/Q9wu7kdyOGUNNp3oWp +Owq7+3P1r9uQmqllqS+S+ghClFWR+vtT/Z7goWGOopbjodwEltQOcdR16/WrcrTFmW4tyYZHmuDc +dhwkDHSvNvq2BC2+up6PThdIzDvMypelJN2lI8+M9JKxsZS1/Cfcn2+tF9K6Oh6ZeW5fYS5VwKgl +locpR3Cvk0+zJTdtioi2htDe5OVL/KAPcn3r5j3ZtdmkrKFTFJ3EDG7BAzgH9a+XX2sNi8CJXaZW +c3GIN7u0u931+KwhaGGspKQMKcKepVV5UmU1DZZtzspMVKQXm3F5B+gHIH0zQCBImKuiJMeCuEH1 +YCfVkjv+bqSKr6t1U7a7uxEgurS0yMLBASc/arlenBULiSGtOSSY6WKJKXckJU2tplSt6FA7gfvW +gxA/sUBggDGSayGya5ed8tkNqSlXVYOVVpEZydIablRFF6ORgjGFJPyKga3Tuj5Il2rVC6sKT1L9 +tiuPTnDI3eSfc/lqrqWOuHFK4qlF1HIX7j2NWIkyQ8XEApSUcD/Ea5TmZj2SggqUMKSrp9KUByQM +T45U5mSS9UzJMtMZ93GFcqJ7UL8Q3UOOww24Bx6h3V8/Sqev0sx7u4IqkB5w8tJ4KFfNBXG3Fuo/ +FPqLxA3FXXHtXp9PQiBXXiTGZrmIjTo68qh+Y2ygPhYSAlXIBz1rYHp04RkNRnWDOA5KyEgDrgVh +mmSmPcCfQpWCACnINFdRXOW3GQ4+60GgcJKDgr+R70lqdP8AZaAvuUK3woDY4mqyrjeFWppZZUXW +lnzUlYCVp+K+LLeYEoLLG5lGdxQk4wcfyrOourlyIzbDhcKVNhHB7e9XYlxatbam0dVDOAOT96Rf +TEDBHMMpU9dTQpVxiTWXGUqDy1n0hxCSAPvXnfWVtnWO9TI8lpLHnZOGxhKkE54+K1K1XhLj4S4j +GOnxX5qiNZ7wlpd1Di30ZS0hKtu4kdCaN8fqG0luxhwYtrdOtqZXsTA1dTWh+B+unNG6tbTIWTap +hDUhGeE56L+oP8qSbtBXDnyWSB+7WUnadwH3rgYT6IQmEpS0VbU5WNyj8DrXr/F1/ueXIZT1P6Hh +aVoSpJBSoZBB4IqVjPgP4ii72eHZLsSJrCPKadP8YA4B+cfrUpMgg4jK8jMybw5vUfT/AIXatujD +iRc5S24DX95KVAkn/P8ASstODk9asPSXvwZbUEoQpzhtIwkYHt9z1q3NZiO2uNMhFLbif3chkryc +9lAHsabbAbP5i6DI/qctPSokW9w3p0cvsIcBLY7+2fituuVxYvDbAMZ2VIUkeX5I5x3Tgdqznwz0 +xbb/ADZQuy3w2y2FISycHJz3+MVtWnNLwNMb3G0SZDvlgb3DlWPgf86V5/5e+oOAc7l/9y18WLK/ +IdH/AHB+l23bLPLMl0RkyQS22r1eWQO/tR178NEju3GS8ZahyVIc7ewA4qpKKfxzTMOGHCsBZSob +ueveitut+XGo8tpDacEp2DAP69ahNYHO4yo1rMxJgt22RLy0l5bYQ04jckLWfM+o7frVPUMpdg0a +65EfXvaX5XOArnp9hTtGgRbcyhL6PPbaG1ClnJAPvWeeMl0FogwnWGYkqKHSFxnUkpSojgkD79aJ +pQbblr9ZgNRcAhMzli9zZYfS27NkPBIKAFKVnnkn2pf1PaZbMNm4PpkDzeV+c0UEK+p6/WtX8H5M +GXDm3OS22Jq3P/W2AlIHwOgFVPF+VBfjqKi4sEHBKSAVfFegXWsmo+pV4zJZ0wareTFbw71Y1Ab/ +AAjbcNh1Q/8Ae9yaYU33VESW5KdK1wucuMpwgj3FYq4S456E7VDjimGHqa6wYqIS5HmMq42LOQBT +Wo0AYll5z+YCjV7MA+puVmuDkgh7evZt3bsdK46s1uiNZSY6iHwSj82CPnFC7PcbdbdOxkPTiqaB +5iQlXCf61mV9uC79dn39oDIVztGAajafRK9pPoSrZezKAOzKclyXcLgue8VLUo7sHrUaVIfeCloG +T0Uo9qstKdbcBLZUg9DiuzkbY4VDIBGQkdBVkuBxOrRtAwf7naKlyMoqQ4pRI9RHH2qtc1/i/KS+ +p3yWchtKwcIzX7HnoQv1nbgYUR7+9NESXCmR1xdjexxOXCTg9ODSzO1bBiJvCsCBFu3eahwltCnA +O6ATj6082K2rlltyXGSsIGEhzPP1xQa1QJNngLmMuNPMrPKE5BwKuzrw6Yu6JJVGWkZSkHIXn274 +pe8m0+H+51G2DBlu4J/DzFKbWhICiS2EgH7H2FD3JTMuclt7B2ArBzgJPvQNF1lSUFoON5JyST1P +tmgEu5yY0wgJ2uoUd27nPtRKdEzHk8xezVLUnHudtXsRYc4rt8pxZdKvMSpWcH60M07a03W5JZcW +UtgFSj8Dt96orKnVKUQVK6nv966R5b0dCksLLe4gkp68dOatKjBNgPMiM4Z9xHE1fwCkQx4pqYdC +vJcC1RwT0WkZH8s1KVPDm+Psa208ogAtysqWOqyo4JP2qUtanPM2jDEL+OWn49u8R5UK0MbGClDg +bSOApYyQPvSzM0rKt9qiXCRs8uSSlCeQoHnII+1aJ/aAZWjxImL3FILTSwR/+RX7bhqJ561XC5Jj +O20pSnyFYJWMZypJ6djWLdSa1BzxDUaYWnaOzH/RlmZ0nYWPJab9SQqS5t/eLV2+wzj7UfZmouM8 +MNtlsNoKlFZAV8H4FULPfmrmtyCtwJfQjKggFIVx2orHsbUZ1TzCktFwfvVKJJUB05968jqHaxyz +y3t+sBeiJJTLSXA6hAWscFSTjke561yfkAlte4h88BIJwB3q5Hjx297RUpWfUD+YYqs5Gjx3HJJK +ywRylIGM+/vShBMIrDMtpKiyVKcWtvaP3aRnn3HevOfi9eZM/UEiEv8A7eOHgkhfT0jg4+5r0JJu +ENLad0plpWM9c8dqUtTaMtGoJS37gyXH3UANyEHH6iqXx99entD2CK31m1CqmZZomd+HjORbXte8 +hOVLSk4USeTRm4xrvqbTjseUGmozTmVPLH5fgfNNNhYtWmJardbw3tf59XqIwepNM2poyJVpdKEt ++SRuCR/EfemLdWou3oO/cJXVmsI08z3BiFp7UakMuonR0jk47+31oG7iTM/dkNoWvCdx/KCe9P8A +dIzR1PAZfjtI3gx3QsAJHznFKOqbfbbXKSzbriZrwJ8390UJRjpgnrXpdNeLAM9kSDqKDWT+AYcu +1ivcK2x1KdiyYSejrCgSnPZXehTLqou7cghKRkgd6Px9SWp2xsMT23HF7QgpaOCFDoaCxFee4UKC +gCT14P3oKs5B+xccx+kIpG0wlaJKZLB9KglB5Uo9KsLeDj2GzjI+1AjmPLH4ZzCVEApPAIopGCFR +1rSpW4naaFbWB5DqUabMnaYEuTGyc40le4deO1fMZam17krwAOua7yYjyZCiG8hZ65ya57WW3W2y +lS3FDkFW0CmgdygdydZ4MT1HezzUy4iCwVKLKcFtSuD74r9uVtRJabLZ8obckpTlP60ItSLXOeDT +KlR1spG9W7clw/ejN4mXa0MDYA9FLn7olIxtxyFCprVkWbU7/cY+0FNx6/UU70GYDBQw6FrUcAgH +ke9Lq3FHkkk980xXedHuYWt6D5L4A2rQrCQO4xV+yaaiTrW5JL29GRgflUCOoJ5wPmqaOKUy/cl3 +Zufw6itbriuAJHloSVPNlvJ/hB61RCwVAKPHc1YubQZmvNpSlKUqIACtwH371Tzk/FOKAeR7ibEj +g+o06QWy7riziG2pDf4lsJCjknnrUrv4TtIe1/ZQ50Q+Fk/TkfzxUpW7ggQ1a7xmbF/aGsKEX83N +U4IU8wFJZWMbtvBwf04pOieITadOMxXmWRJR6CsD1HHTH2xWx/2irAu9aJTIjJJkQXgsYHJSrg/6 +V5os1rjsynVXOQY8uMsER1t8r+M9j0pSymu1P/J6j+ktatxtE23QtvmwYar3cX0JjyE+hhQ9ROeC +a0CJJaLTe+Uhfm/l7/YUhWKUxfbKxCztdQkJStWdySf7o/rTHZLC7bW3g5M819Y2pLiPy/TmvLak +AsSeCPUp7i1hB6h+Ytbnl+US2AfVx/nXyWg4kpeOQ4CPT2FVX0JacS6qWpASnC0qIINDLlKKGyGp +QaLmADgYA74xzSY7zDpWW4Eq2e0N2yXMdmKS6twlCUO4IQj3+po86RGWzGjtNgO4AATwlPXNAmPK +dLanH15K04SEE5x7GrsGWLnclJ9SHGuCrOCU+1E2s5zNfSE/7mJniFFciyHJ6XEktoIylWBjPPHv +SnC1HKlFK25Kls7cBpSvy4PtWwXHSsCXIUqUt15Tg2qStfpx7kUIc0JZIqHlpGwqTgFJxgZzx809 +XfWE22DJgwQD49TGr0pN2nlL7i2JKjvC1DCc9qUtRR47sjLQWiYkYdbX0PyDWwax09bZpcZtpdbl +FJO5aztJxkD46Vl83TclMT8SlDjh28lIJwfY/NXdDqK8Ag4iGsosYHK8QVKiRIztv/BqccWUhT6l +jASruBVpEoKkOAYLhJO0D9KGIUoqQ2vucYPaidptb0i6lCMNt8lSlq/N8VRcDblz1J9Tbf4CEGYb +rzbjiEBLqQQAtQAzUs7jrqnGFNJy0fUMcA/WjlutUySrLT0dLGw5C08hQ6fbNCrTBuVlubjjkJ58 +pJwU5Lef72B1pQMLFYZGY0bHQggS7KYUw35ivUlXU9xSfdCp5QWltSUp/iPfNaBLtv4KGiVOkYcf +X5imS2dyE9uM8DvjrQc2hyYsg+WGSfSQKxRatfJMLepvXA7iilxtKmlMJcQ4nlSlKzn7U4wbou7Y +RK9SGeUpzjJPciuLmi5ayDF8t3nsrHFfFx0lcbeSptYWhKUlS0EjBP8ADR2votx5DMSFF1eRjiGF +OWuK4mO+y2lTyFIWpw5SCeivgZpNuCzBU4zEmBbTnUtq4UP+ZoxaNIXG6So5ebX5C3NillXQd/pV +zWlmYtEJmEiARLz6XEerf78jrXy3VK4XO4mDsSzbwMYiQI8iQlx5tpa2kfmWBwK4BKVdDiicpq5t +NGItl1DbbYdUgDgAjO40JZSpxwBA5zVBDnn1EnGD+5rn9n+1pXeZlzcQFIYbCEEjoo9x9galN/hp +BFn06wwQA89+9cPfJ7fpUpG072zHql2Libtf225NukRX+WnWyhX0Iry9drM3ar2i4XN0h6BKS28r +O5TiByleD8Yr0ldJyHWtyOD0UKzHW9taloXM8jzkhBbkN4yVt+4HunqPvQXBxkTqH1E2dck2u5wp +9rUW0yiVPKCdwQgkYJx361pca9NSGG3C5kIR6nkD0g/Ws5uMMT4DJtFyZTCdSlAjlsJKTnHpP+hr +hapk+yxP2fNW7+DeSrAIyN3uP0qJfQtij8/9lPTlkznmPNwdh3FgILzgcK/3bqSfUfZQpW1BMuNr +hKeeQlCyrCWeu0DjdXL9oW2NAadjuLbdj4UFBQIWoe6Scg/NEo5cu81h+5JAQtvcgdE++Tmlvr+o +5YZEbpvstyvRlPSGtFvNJjzox4JKHknHP0pq03c2GlTAp5j8Spw7d5CVEYHANL9xsrTbMibHUCUJ +IKEt8JPvxSey4ZylLX/8yOSMbqIK67stXwIT0NxyZubSDKUX1lbawkAZ9u+KHXeez5ja3HwhpPxy +D2HNZu1rG7W5zeqS0EgbUggHA+nvVaNqOXdr5HVNcQhCV71BKQNx7ZzxQxoW7PUIgGcmNs6SqW+W +2hvdc53qRgkHgc0YsdpVGgluSGygrUdqQClJ+TXVu2sSSu4x3PxD20qDa14yccAe2KruPvNw23Lg +z+HDytqh1Chjoo9utAJ9LC22h0CqMRc15omyXhCnLc0mLc0c7mcBKiBnCk/PuKy646YvkCU0qLuL +iWylQUPyE9cH5/WtkRLs0VhTLzqW22sEqLm5xXPTjtV2bLt88sttrCSpQxsOSCPeqGn191ACnyH7 +k27RI/K8TFdFOOYcTcAWENqIcUpJBz23DvTqvWMRElm3uQiUpIQ08BgJV259qdFWjzorsd8RXQ7k +KJHCh7E9yBWWatszVpmsKRuCRgJTn0g5P9KKt9WrtJYYM+q07IgQGWpsNN/lsTH5W7yF7H22+Nqc +ZJz84r8sMda284IRztBHal19yRbslgltMjKVA01abvCmLamK6AprbtGeoo1ysKwF5Eao0TsxK9xu +03BS6hS9gU4DzkUWj26G4osKbSpRysBQJGaE2W822NHDbyngM7s4wM/avmZqdhrelhorSoEbxknn +5qVtctnEOdLZnkQvKjIhuNojNZyraQMYTx1PtXzeYMZtDS30IS4lQWhWMkH4+tIxvz8GT5iQt1Bz +vSoHBPbNVjPvGo33HWnSEsgqTgcE9NtMJpWyGJwJ9dQVGOxAGt9QruazbYxQGMAOOjBUo9hn4pf0 +vYiu7AvEKQ0rcQOh9hX47bJMW5qjlrCyohKSoEgfOKboflWmIhhsb5S+Sfk16SsCmsLX1PLWoXsz +Z2I6QZ3kBKc5dPGPapSw28qMn1q3PK/Mc9PipQ4YVMwyJt2oHV2uZuGVML/mKoKWlwbkHchQ4qkN +ZaevsQxzcmQsj0byUkH71TgOvRVqbeG6Ks+l5PqSD9RXxBioihqTS8Vm7JlNyHGIqlZWWujDmQQr +H9339q/bihUVLqVvh1ak7S6g8KHwO1OshQIIUAoHg96z7VdpkxIEw2chTDqTmOr/AOZ90Ht9KWv0 +7WkYMf0Oqr075sXIgLTkZl7Uy1zZCQhpsuDOOuQOa05NvYkS0J8h1UUDd5w5UOOAfisK026yJZj3 +YOR3i56XRzkn+EitUsN4uEvEeCpDCGlEOL67ldMikfk6HUg54Ef02pS9i6jEcLpcGUMLSW9iU43J +6EjH+VZ9NuLDmQqCIsdxR7e30rQWNPKaebmOTVrdXysq5C+OhFfcm129Y/7ptghJ3JKU8j6VLqtS +rvmNFNx4mNXGMy6jEQqeUF5V8D2oS63JalpaQdrhxjdyQK2O6Ls8SOGm0hO7ohKeVH2FIl205Pdd +cmMskrICkNg+pIz0IqrptWGGDwP3M3VhFye4w2hmVGYaUmUUsrwcpOSn5xTpcpUJu1vOmQpwObUK +S6njfnjjtzWOu6iu3luRnIhQGTtJHBB/pRq1u3G5hhKFlIVneVdz9+lKXaRgdzkCdRxYMg9S9qB+ +A/MS0tpYIVudaZTgOqwAPtUdjTkORXGmhHbKgltKVBJSMd+9Mtv/ABrcWRFLUdxATl0lGFlWOx7/ +AAaEOJhuLZipYdksr6BokraVnnd7VhbOl7xBfWwctnj8T9m39strVFa9aMggZKlK+lLGpXLhc47d +smsKjlSgpJWg5A65B7dfrWk2vTdus8p+clS1vYyEurB2H+pqs9erVc32zJIbeZXtS2oZO8fH+tap +sVH3VrnHucXftIeZf/0zdZDYbKlPlpJWVnkZ7D704WLRhTbkOzg6XVpxsB2+Wfr3p0hzIylPPtth +KEr2uFQxuI7ChV61IhaTGay24okBST0J6GutrLLPACMJY6DxMze/Ldtdzcik7gnlJ+DVJF2KTlVO +0O2M3WK8mQ0h5/HoIOFdepPalq5aTuapziQhptrPUkHA609VZW3i3cbHyRVfKU03RLishXIpfVqe +Q2lyJC/dZWQpfzmqF5f/AGdcSw08hwJxnb3V7CqcNl5qWp6U2lKRnYnOefeqlOjQDcw4kX5D5g2Y +Wn13GOKsQklxR8yU51UecUSt+5GX3vU8rue1CbeypxfnO/YUWB9jRGIHAiVNZc72lgLJVzzUrmg1 +KFiOjjqIwUpPKSR96KWnUl1tLoXCmOt+4CuD9qFlOe9fm3nrT5wexPN5I6msWHxHjzili+Nhlw4A +faGBn5HSmicCI6X2loeiufkeb5Sf6GvPqknrTJpPVs2wPbMh+EvhxhzlKh9KA1XtYZbM9xj1Laos +/K1ICHv74/1qnbryuwBtCIYQgDatbayQv5wehpnu8NiXaBebK6X7csgOIPK4yj/Cr49jSbJXwQel +BesWLseGrsNTbkjx/wBWQ4FvYfdntLW8NwZC8qT9RQ9Gq3bo8ERlBDajgrJ/KPekB1ltLqZCAlK0 +HcCUgjP0NfIuy1Tg+yw2y4kEL8kYSv52nj9KSPxNQ/jyZRr+UYfyGJt+nm7Kje95pflEAFxR6H/C +DQW+OSocpBjL/EFZOHmzyR7GkzSl9ZLr5uE2LFBOPLWlWSPccYFaxpS8WZlP4aEpDri8OKO4KBP+ +lTL9NZQ/kMxg21agBi3MXo9ulOvB1uC8p0j1LV0PH86JQ7QpiSh94mO3tUFBSeMn2zTsJjKFrde8 +g8DbsIJA78VzbuEd6MVLaSWFZSCUZI985pRnJjCviI2nbncJNzXDUhL7aSU5C8J2/OKcbTaodsU7 +K8hLL6zuUndkA/GaU7tM/ZUlQjBlu3bdzbkdHKTnkE+59qU77q+4zISmGY8lbyVH96hKjlPHHFGG +me0+HAM7bcmMxv1V/wCQkLFvcdxzktd6RbNDC71lDgbS2dy3F9sHmh8PVF5ZQtEdteFDar0eof0o +8q7abXHYNxdDEhgYUUnYpffkdxmqFelspGMZz+Io2qQ+51v9/wDw7KkwZflxlElIKgTnPJNcH7mz +Asjbi1smU8QouE/PBH2pd1DreyOwnojMGPIK8+tLe3HGAfrSE9cVrjtJjFfozwv1bfpnj+VOaf40 +so3DETv+RReF5m53LUNis0Bp9ExK3QkAoQ5nPfisq1druXd3CmMVtsDITlXOPn3pcMGS/HW84VKd +zwF9SKFKCs7T27U/pvjqaju7Mm6jW2uMdCE4tsukyI5cmY77sdtYSt4DICuoBNMFoWiapJcVhY6o +V7138N9XK0/JWw42l+BIT5cmMv8AK6jv9COxpi1XpBtE2LctJvfi7bOBdbAI8xrH5krHYj370zaf +R4gqCQwxzOCMJGE9K6A4rm20ttnDysuJ4OBxmq0uWllv08rNIjyOBPRsCg5GJLnODDZQg+s/yqUs +zJKlqUVHJNSmkqGOZOt1TBvGfZIxkVwWsg1KlaEmT8DhxX7u3dqlStTka/D3Ur2nrylKkfiIEr9z +IjK/K4g9fvR/xBsyLDqF+IwsrjqSl5rd1CFjcAfkZqVKHYIZOonyclpZz0oeygoUpWetSpWVmz1O +c6Ol9o9lDoaBIkPMOZS4obTg4URUqUzWAeDE7SVPEYrXrSZb30ORGwhwDG4rUr/M0SXri+SpYcYu +EiMMcJbVx9alSgtpad27aMw6ai0pjdKFz1nqJuSn/wAtIJIznj+lfQu11VueVdJm9weohwjNSpWj +UigYAmfsck8wPPlPKz5jzyz33LJoOt1SieSB7VKlGQQDk5n2w35qwCaYLbEQEBwgY7CpUrlphaAC +3MIkBKc0DuUUKC5CcJIPI96lSh18GH1AyINiI8x9CM4x3Fat4f6okWOY0qKkFv8AKpCgCFp75qVK +xqfUY+MUENmMmv7bHbDV5tqPJjTFcsK6pVgE4+Kz68xy41vZUEKPvUqUovDyufKjmfrVmYbiHd6n +cbis+/WpUqUcMZKdF44n/9k= + +------=_NextPart_000_001F_01C8D3B6.F05C5270-- + diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_duplicated_attachment.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_duplicated_attachment.eml new file mode 100644 index 0000000..42ae7b5 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/reply_with_duplicated_attachment.eml @@ -0,0 +1,177 @@ +From: Kirill Bezrukov +Content-Type: multipart/mixed; + boundary="Apple-Mail=_396D2C77-FD0E-4640-8B6E-402C1A3F7C26" +Subject: =?utf-8?Q?=D0=94=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D1=83=D1=8E=D1=89?= + =?utf-8?Q?=D0=B8=D0=B5=D1=81=D1=8F_=D0=B2=D0=BB=D0=BE=D0=B6?= + =?utf-8?Q?=D0=B5=D0=BD=D0=B8=D1=8F_=5BCookbook_-_Ticket_=235=5D?= +Message-Id: +X-Universally-Unique-Identifier: 6F6E3C1C-AB1A-4799-A9F3-2CEC9C0FB404 +Date: Mon, 27 Jan 2014 12:51:01 +0400 +To: test@redminecrm.test +Mime-Version: 1.0 (Mac OS X Mail 7.1 \(1827\)) + + +--Apple-Mail=_396D2C77-FD0E-4640-8B6E-402C1A3F7C26 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; + charset=utf-8 + +0JIg0Y3RgtC+0Lwg0L/QuNGB0YzQvNC1INGB0L7QtNC10YDQttCw0YLRjNGB0Y8g0LTQstCwINC+ +0LTQuNC90LDQutC+0LLRi9GFINCy0LvQvtC20LXQvdC40Y8uINCaINGC0LjQutC10YLRgyDQtNC+ +0LvQttC90L4g0L/RgNC40LLRj9C30LDRgtGM0YHRjyDRgtC+0LvRjNC60L4g0L7QtNC90L4NCg== + +--Apple-Mail=_396D2C77-FD0E-4640-8B6E-402C1A3F7C26 +Content-Disposition: attachment; + filename*=utf-8''%D0%B2%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.zip +Content-Type: application/zip; + x-unix-mode=0644; + name="=?utf-8?B?0LLQu9C+0LbQtdC90LjQtS56aXA=?=" +Content-Transfer-Encoding: base64 + +UEsDBBQACAAIALZkO0QAAAAAAAAAAAAAAAAUABAA0LLQu9C+0LbQtdC90LjQtS5wbmdVWAwA9Brm +Utca5lL1ARQAdVQHNBts2061dtVO7BFFqyJN8CL2jhpVQii1YkepGEmp0mWU1qy9qkbtXUG10QYN +Ra2i9t6rZvHy6fu+3/ef8//ff5/zPM+9rus+932f80QYG+kyMfAwAAAAJj2klsnZC/p96KjObi+k +5j4AwDDqpqlprKepKWSM83J283ACAJBMpYl4lKkeiLQRuCT8mlojli2APRe9JBvyGNWsg6SiF32q +xlJ5LZKhjJqT1Rjtq63VXAuoAmpxjICHKqBfcrJLIhuGjH/Z2FHWf3XVnQZtd309Hs9Q3B9T3OhS +AIXsJYsAXbLbASk51nqoCkGspwY4coeDJdsumkn+HlHBYeLLl3sHg3snAvkvake+w61Oi/Y+glCE +Yz4pAFsERHwbR0zhyiIH8KIlKdSkOQzuTUu9AzfavkSsmY98kBxlKl5/p5US/yBYQ1ysXbCPm0ao +T55PTyLkk65BV/tMvoO/ayrXyHOCBmurMFegNzWfQOsKLDMzJUUU85X2+s2TsIz+4Oth8ZJ1lKnc +9vtwoIf43hTA/qX8k2rH6blTBUdnZOV2qivNEJIdqZFTMcyhWF20U3JdITLORdeA8sxCy1QyJpRs +BJNrE1TsoDQpGakNcej3YYIKkkWaF6H89lKKzs9FLW/hjnKihquOkzfVSS4dKVnsXMqiN1lpKguI +aizsqi33PxGHTdPjBSD7r9UmoUn6ubdwWaaX1X9ySftUZAQHJXX8OrKqf8w1HJyxGdbGTJfu8OGK ++Xq0/OBNT910WOwE++FxlF29kjj09gc5NJtIUoG3htMvxGto14uELHpyCCBXiEpgU52KEkPNHcMo +SQcQng3hCpwBJLLVHz+QZD53XE3Wv/wJpS7VAWpUeAC0ItemdFXV1fpciBnTTLlEUpwn/6mRPHiS +6FRHReTEnFr7U1vAIYCXrKSlBUDUfnBH1LjD1SrqSQ8QWoKS1v+5Q4zKWJ2+X41xh0T3tZlntZlb +iURLJcwBrkCy2QnfqyTAK5HAz3ALJm1jKis7WhTJZog5bPIKX5iY/HnQJNw6TIrIGJ0NopnkjCIF +cmmyPH36jZUm59miiFg4p0M0EWyDFGJ7qSVMj2SLex7OcrXqomVkglAekrqKuVD7oYaRCCimGCn3 +E7CkGRpT98WFI6zXWBYFktV2yS57C+UJbc1m/IaS4YWoMfYYuircfiGgztvDuOi3I/Uj8oOGrneI +KQnmTSOPTb/WJQh6pE/Om7xM5CQ6WHOX8aCjsc1+k54zAd6fiTcJ4MxHLlOlWwIHLA8kMyO7W8tJ +XmT0JPMU79bawciByjHdCe8jo2/CFqyj9B6frDUk7GGvw4115JHy2kQtoj6X0HtxtgiDHO6eZ1wa +BkL5rElscmx4dt7wUrKng6p3ghbYLbYEmZlLxxaWoC8BXgPzC2eIj4GZYlTiOL8cOgbiWBRiJcHl +wkmxzPHdsV6xtjFtV9o52J8rij7+9EbytUc+X3aNsacJzRUMirOIeyCeR9PwTVl+oulKYdLb6MIN +Dg6Oi5yjHPNX6DjNryZc4zMl5nwr2IzIv1Gd+6tvvi+3xrCG5/a1tXNr/BlGcnlH39iX6ncMd24f +ah+aHupqh+a8AFvFFhBNKk0qb20lTmNFn8dFxJX3auRR5cHz2G/IvQp9dbU9v/2knW+Os/1Ve8Gr +wARdMy4JfwlWiRQJXrNLxdZm3CjRwtQimqKfKEaz/WIb1JrLJJfOxWCapMpE7xDvCFi2DkraFFOc +V/jqLb/ZCor3FtoMj4o27UaXo1SLxzvIMMMUD2caH+lt+W2B7erQstByGlvME52mVsgL5tQ+mSCZ +X6ktqUypeFlVbGUNVW1v7Tt3WTflnlaslXt3j01Jk+5R3N6mTK2bQB+x73kBf9aeMe1AZX9Er2VR +te1CaNfbskzPijILfNK69pi9cn5mtE3quH+TT4Dm4cUgaNDrE/8/tw+hlsuStuY0xekDXTywbzA6 +WAzPqvSiDJe0jHSUrDVmyO6AHO4+657gUDP2ttOVbDFBSNyCzyBmwsQ42NHsjyhG8ZfimfOd87fy +VwzSDGg7ZTqHnKmd/ZyjO5fmx+eH5wfm8ZLhwDLgErAR0iB5S19c/5FT3pzhtf724RuutSNFgQj7 +hhHfnp3lJeXvMljbFaUVxIoKb74hsZMnTSxtUDrK5XLXEOINplXOSnzk4sj5d9GrqDWNte9DtUNN +tXU1Cz/QPya8sxUvKJT4uvoTZlznvuBHZwgTEVMyHW4Zpml9aBeQS4RP03ZMZKV+nP4r/ejiVGzu +is7gNc8Lcqp1rzxdRlXqFRsXGgTxp58//xAZm6lomzSoTjlOoXXeC+QJwCnt398LqHwf9/7h5vLu +H5vzBHSg6jbs59Qvvp37U9iptPlA8n5tVmkWeSNhgjYYfzp3UnqOGDIZMh3SpTZAGiI9JxWoh6oX +qvupizQbNes/sdKkE7YXttIMGGrwkGpMVGZbZmtQqlGOAgL5zVn3JEfh1mzjcMYMTgR2pKSt2tA5 +0llOW+2CooMVOR5Z/WX1S9YCy4JWV2H6tlK7lLslSldGJODWztTiPuwhLA0j0JLadgMzMp0wPd82 +0BoUzhwOfbGRoYhZ/1k/yF7D/pS+MyKXTCQ/iXizTbWwlix3fcLtYc9xIONX4CyU2GAcaN/6qToy +AegNzECCL7eI8xsdfoqYRPnoxA9TzCgi1sHrRuvXMpm9FL3CNyjQuY/ILYOrhtwGuwZjiYjLm8AE ++tBnJ58YptJqYmqa3AR7RXqxvW3S09I8cEqKXkqBdlGO763ARBvzh+Z1QzesL693+nePxyqD6eP5 +vqrjgLMyC0X21N1MhfU7im62mWgbp/G2Fu/Im7PFs5E/vSQrravsKZGPsx8LJk0n4ZLeQaQt3gVY +TuAm5o7Mha7FoWtKZW5bEnF2ZptFmsWFxX3o06pWf8OlVNXB1vK7V/sh1mZDp3c28X7az7XjcjL9 +BFb2ytrKQWUbo5MV9oOhXi3vzz6Ad6JOorZ/4ORsjsbXj35utMnleSyPrr6/3w2cBvYkBlo11QlI +mF2FXFmb+ANrNeP/fW96LXjdombGPQUzPJ7Rn9Gb0TQ3NdfZGIW/O7ybp2HsXddbXvX6RoG75kDY +2Puxur2D1cKJ4sY6x40FwaeqoxOg9+WVP2oaPFS9g8dPFminL/keLx5i1tTXnojSfvwe7h7ArM3v +Xu5XWKpc4kPw4b8tXsqLgJ8iB3tC3nTuft894klmEm2bTN4Z6siKe9iAV1oljNAPsu+KGxqZZzik +S3hq8STJvJShTw1IY09tvBmW6YndclXYre7slnW52IXKNFNt/MW3az1Vv7VNvaBr1VL6GWQC3amD +uAf6eq+3KK75/FCEPrvTgFNY/NlyaeLlNlqpPL1++eHujuwg1nAwo1xu/F6D9zKowNbID4H/Y9y3 +fnh07vtAr3L10ShkwwrkSTv64UFY0MLh2DC+caNlvHFC+mPBvsrGyeSTKFovgXllvsCaZdiU9xb7 +nLjTTb9M81P3Laxmuc/G8IeQD7zdOVnKywt7Hw8hC3XQ88+6l7o7bBY/vuWvEVSS5pDp38WNdzxC +T6Mxn7U/Fh/b7P443qWoPjghwuB4x8kXs50HIy82asYHjpcOXEZcZxf79lM22lQ4gvsPCYvxKwUv +u7eybKMgJxqHJicD09iOugqv6DbptrYWPnqXHCDJXiQ1jmrzHU1IZsjApTsAVAsFcV/jfErJGkzn +ZBm+FbzlCIFYkdzp3m6dC79DzTLOUEG7IsbVV2FpDm85ACxPCm0ka33HmfCtigNe61X0zs4cHCje +AZgwktCUg4Oh9a2tctqKgJNzatg9xyCpPjGJ8wmpbhUx60bxmc0Rf55ynZy/xc/AykFfSA8AnEtz +Q1n4WhgaIDBed6XsHb0cnKQId+8BfosSAXGm3nXytRci3PXw9EEQlMF/ZSDO9N9uKFjorxRfrDLY +wtBYSNML5yQkKyUjdR2swiAkJKSEc3RGmGjp/AM/s5TBrr6+9xBQKB6Pl8JLS3nhXKAwBQUF6HU4 +FA6HnGVAfO57+toTIJ4+In+T/JtHy8kHg3O75+vm5Sn027Z38PLzVQaD/8n5W/4u5ERw+59Knj7/ +tHXWIPR3BAqTug79D/dv+t9ehLEbwcnDQsvtrpOnz1kNFbi0EvS/Bv4r0PL/A1r+H6AS9H+18/es +oP8MS4VBCfqfwaswIKZxxme7uK6npY6SZ8X4UtKb1WB2OUMXYs69ybdjbu25x80EWhUOueMr0xQV +yrjHQpK6kbFDBYh5cM5nrdg//Pci9bSNtMo07B7/C1BLBwgTSYZHjgwAAPgMAABQSwMECgAAAAAA +VmY7RAAAAAAAAAAAAAAAAAkAEABfX01BQ09TWC9VWAwA5B3mUuQd5lL1ARQAUEsDBBQACAAIALZk +O0QAAAAAAAAAAAAAAAAfABAAX19NQUNPU1gvLl/QstC70L7QttC10L3QuNC1LnBuZ1VYDAD0GuZS +1xrmUvUBFABjYBVjZ2BiYPBNTFbwD1aIUIACkBgDJxAbMTAwCgNpIJ/RlQEZCDDgAI4hIUEQFljH +CyCORVPCBBXXAqLk/Fy9xIKCnFS93NSSxJTEkkSrbF8Xz5LUXM/i4OSi1NQ858SCktKiVKiZQkDC +mIFBB7c+FF0hlQVAnUkFOZnFJQYGnBxQUxihLmFEcxknTGVkcWpOanJJZn4eIS1CAFBLBwiY4lxO +mwAAAEUBAABQSwECFQMUAAgACAC2ZDtEE0mGR44MAAD4DAAAFAAMAAAAAAAAAABApIEAAAAA0LLQ +u9C+0LbQtdC90LjQtS5wbmdVWAgA9BrmUtca5lJQSwECFQMKAAAAAABWZjtEAAAAAAAAAAAAAAAA +CQAMAAAAAAAAAABA/UHgDAAAX19NQUNPU1gvVVgIAOQd5lLkHeZSUEsBAhUDFAAIAAgAtmQ7RJji +XE6bAAAARQEAAB8ADAAAAAAAAAAAQKSBFw0AAF9fTUFDT1NYLy5f0LLQu9C+0LbQtdC90LjQtS5w +bmdVWAgA9BrmUtca5lJQSwUGAAAAAAMAAwDqAAAADw4AAAAA + +--Apple-Mail=_396D2C77-FD0E-4640-8B6E-402C1A3F7C26 +Content-Disposition: attachment; + filename*=utf-8''%D0%B2%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.zip +Content-Type: application/zip; + x-unix-mode=0644; + name="=?utf-8?B?0LLQu9C+0LbQtdC90LjQtS56aXA=?=" +Content-Transfer-Encoding: base64 + +UEsDBBQACAAIALZkO0QAAAAAAAAAAAAAAAAUABAA0LLQu9C+0LbQtdC90LjQtS5wbmdVWAwA9Brm +Utca5lL1ARQAdVQHNBts2061dtVO7BFFqyJN8CL2jhpVQii1YkepGEmp0mWU1qy9qkbtXUG10QYN +Ra2i9t6rZvHy6fu+3/ef8//ff5/zPM+9rus+932f80QYG+kyMfAwAAAAJj2klsnZC/p96KjObi+k +5j4AwDDqpqlprKepKWSM83J283ACAJBMpYl4lKkeiLQRuCT8mlojli2APRe9JBvyGNWsg6SiF32q +xlJ5LZKhjJqT1Rjtq63VXAuoAmpxjICHKqBfcrJLIhuGjH/Z2FHWf3XVnQZtd309Hs9Q3B9T3OhS +AIXsJYsAXbLbASk51nqoCkGspwY4coeDJdsumkn+HlHBYeLLl3sHg3snAvkvake+w61Oi/Y+glCE +Yz4pAFsERHwbR0zhyiIH8KIlKdSkOQzuTUu9AzfavkSsmY98kBxlKl5/p5US/yBYQ1ysXbCPm0ao +T55PTyLkk65BV/tMvoO/ayrXyHOCBmurMFegNzWfQOsKLDMzJUUU85X2+s2TsIz+4Oth8ZJ1lKnc +9vtwoIf43hTA/qX8k2rH6blTBUdnZOV2qivNEJIdqZFTMcyhWF20U3JdITLORdeA8sxCy1QyJpRs +BJNrE1TsoDQpGakNcej3YYIKkkWaF6H89lKKzs9FLW/hjnKihquOkzfVSS4dKVnsXMqiN1lpKguI +aizsqi33PxGHTdPjBSD7r9UmoUn6ubdwWaaX1X9ySftUZAQHJXX8OrKqf8w1HJyxGdbGTJfu8OGK ++Xq0/OBNT910WOwE++FxlF29kjj09gc5NJtIUoG3htMvxGto14uELHpyCCBXiEpgU52KEkPNHcMo +SQcQng3hCpwBJLLVHz+QZD53XE3Wv/wJpS7VAWpUeAC0ItemdFXV1fpciBnTTLlEUpwn/6mRPHiS +6FRHReTEnFr7U1vAIYCXrKSlBUDUfnBH1LjD1SrqSQ8QWoKS1v+5Q4zKWJ2+X41xh0T3tZlntZlb +iURLJcwBrkCy2QnfqyTAK5HAz3ALJm1jKis7WhTJZog5bPIKX5iY/HnQJNw6TIrIGJ0NopnkjCIF +cmmyPH36jZUm59miiFg4p0M0EWyDFGJ7qSVMj2SLex7OcrXqomVkglAekrqKuVD7oYaRCCimGCn3 +E7CkGRpT98WFI6zXWBYFktV2yS57C+UJbc1m/IaS4YWoMfYYuircfiGgztvDuOi3I/Uj8oOGrneI +KQnmTSOPTb/WJQh6pE/Om7xM5CQ6WHOX8aCjsc1+k54zAd6fiTcJ4MxHLlOlWwIHLA8kMyO7W8tJ +XmT0JPMU79bawciByjHdCe8jo2/CFqyj9B6frDUk7GGvw4115JHy2kQtoj6X0HtxtgiDHO6eZ1wa +BkL5rElscmx4dt7wUrKng6p3ghbYLbYEmZlLxxaWoC8BXgPzC2eIj4GZYlTiOL8cOgbiWBRiJcHl +wkmxzPHdsV6xtjFtV9o52J8rij7+9EbytUc+X3aNsacJzRUMirOIeyCeR9PwTVl+oulKYdLb6MIN +Dg6Oi5yjHPNX6DjNryZc4zMl5nwr2IzIv1Gd+6tvvi+3xrCG5/a1tXNr/BlGcnlH39iX6ncMd24f +ah+aHupqh+a8AFvFFhBNKk0qb20lTmNFn8dFxJX3auRR5cHz2G/IvQp9dbU9v/2knW+Os/1Ve8Gr +wARdMy4JfwlWiRQJXrNLxdZm3CjRwtQimqKfKEaz/WIb1JrLJJfOxWCapMpE7xDvCFi2DkraFFOc +V/jqLb/ZCor3FtoMj4o27UaXo1SLxzvIMMMUD2caH+lt+W2B7erQstByGlvME52mVsgL5tQ+mSCZ +X6ktqUypeFlVbGUNVW1v7Tt3WTflnlaslXt3j01Jk+5R3N6mTK2bQB+x73kBf9aeMe1AZX9Er2VR +te1CaNfbskzPijILfNK69pi9cn5mtE3quH+TT4Dm4cUgaNDrE/8/tw+hlsuStuY0xekDXTywbzA6 +WAzPqvSiDJe0jHSUrDVmyO6AHO4+657gUDP2ttOVbDFBSNyCzyBmwsQ42NHsjyhG8ZfimfOd87fy +VwzSDGg7ZTqHnKmd/ZyjO5fmx+eH5wfm8ZLhwDLgErAR0iB5S19c/5FT3pzhtf724RuutSNFgQj7 +hhHfnp3lJeXvMljbFaUVxIoKb74hsZMnTSxtUDrK5XLXEOINplXOSnzk4sj5d9GrqDWNte9DtUNN +tXU1Cz/QPya8sxUvKJT4uvoTZlznvuBHZwgTEVMyHW4Zpml9aBeQS4RP03ZMZKV+nP4r/ejiVGzu +is7gNc8Lcqp1rzxdRlXqFRsXGgTxp58//xAZm6lomzSoTjlOoXXeC+QJwCnt398LqHwf9/7h5vLu +H5vzBHSg6jbs59Qvvp37U9iptPlA8n5tVmkWeSNhgjYYfzp3UnqOGDIZMh3SpTZAGiI9JxWoh6oX +qvupizQbNes/sdKkE7YXttIMGGrwkGpMVGZbZmtQqlGOAgL5zVn3JEfh1mzjcMYMTgR2pKSt2tA5 +0llOW+2CooMVOR5Z/WX1S9YCy4JWV2H6tlK7lLslSldGJODWztTiPuwhLA0j0JLadgMzMp0wPd82 +0BoUzhwOfbGRoYhZ/1k/yF7D/pS+MyKXTCQ/iXizTbWwlix3fcLtYc9xIONX4CyU2GAcaN/6qToy +AegNzECCL7eI8xsdfoqYRPnoxA9TzCgi1sHrRuvXMpm9FL3CNyjQuY/ILYOrhtwGuwZjiYjLm8AE ++tBnJ58YptJqYmqa3AR7RXqxvW3S09I8cEqKXkqBdlGO763ARBvzh+Z1QzesL693+nePxyqD6eP5 +vqrjgLMyC0X21N1MhfU7im62mWgbp/G2Fu/Im7PFs5E/vSQrravsKZGPsx8LJk0n4ZLeQaQt3gVY +TuAm5o7Mha7FoWtKZW5bEnF2ZptFmsWFxX3o06pWf8OlVNXB1vK7V/sh1mZDp3c28X7az7XjcjL9 +BFb2ytrKQWUbo5MV9oOhXi3vzz6Ad6JOorZ/4ORsjsbXj35utMnleSyPrr6/3w2cBvYkBlo11QlI +mF2FXFmb+ANrNeP/fW96LXjdombGPQUzPJ7Rn9Gb0TQ3NdfZGIW/O7ybp2HsXddbXvX6RoG75kDY +2Puxur2D1cKJ4sY6x40FwaeqoxOg9+WVP2oaPFS9g8dPFminL/keLx5i1tTXnojSfvwe7h7ArM3v +Xu5XWKpc4kPw4b8tXsqLgJ8iB3tC3nTuft894klmEm2bTN4Z6siKe9iAV1oljNAPsu+KGxqZZzik +S3hq8STJvJShTw1IY09tvBmW6YndclXYre7slnW52IXKNFNt/MW3az1Vv7VNvaBr1VL6GWQC3amD +uAf6eq+3KK75/FCEPrvTgFNY/NlyaeLlNlqpPL1++eHujuwg1nAwo1xu/F6D9zKowNbID4H/Y9y3 +fnh07vtAr3L10ShkwwrkSTv64UFY0MLh2DC+caNlvHFC+mPBvsrGyeSTKFovgXllvsCaZdiU9xb7 +nLjTTb9M81P3Laxmuc/G8IeQD7zdOVnKywt7Hw8hC3XQ88+6l7o7bBY/vuWvEVSS5pDp38WNdzxC +T6Mxn7U/Fh/b7P443qWoPjghwuB4x8kXs50HIy82asYHjpcOXEZcZxf79lM22lQ4gvsPCYvxKwUv +u7eybKMgJxqHJicD09iOugqv6DbptrYWPnqXHCDJXiQ1jmrzHU1IZsjApTsAVAsFcV/jfErJGkzn +ZBm+FbzlCIFYkdzp3m6dC79DzTLOUEG7IsbVV2FpDm85ACxPCm0ka33HmfCtigNe61X0zs4cHCje +AZgwktCUg4Oh9a2tctqKgJNzatg9xyCpPjGJ8wmpbhUx60bxmc0Rf55ynZy/xc/AykFfSA8AnEtz +Q1n4WhgaIDBed6XsHb0cnKQId+8BfosSAXGm3nXytRci3PXw9EEQlMF/ZSDO9N9uKFjorxRfrDLY +wtBYSNML5yQkKyUjdR2swiAkJKSEc3RGmGjp/AM/s5TBrr6+9xBQKB6Pl8JLS3nhXKAwBQUF6HU4 +FA6HnGVAfO57+toTIJ4+In+T/JtHy8kHg3O75+vm5Sn027Z38PLzVQaD/8n5W/4u5ERw+59Knj7/ +tHXWIPR3BAqTug79D/dv+t9ehLEbwcnDQsvtrpOnz1kNFbi0EvS/Bv4r0PL/A1r+H6AS9H+18/es +oP8MS4VBCfqfwaswIKZxxme7uK6npY6SZ8X4UtKb1WB2OUMXYs69ybdjbu25x80EWhUOueMr0xQV +yrjHQpK6kbFDBYh5cM5nrdg//Pci9bSNtMo07B7/C1BLBwgTSYZHjgwAAPgMAABQSwMECgAAAAAA +VmY7RAAAAAAAAAAAAAAAAAkAEABfX01BQ09TWC9VWAwA5B3mUuQd5lL1ARQAUEsDBBQACAAIALZk +O0QAAAAAAAAAAAAAAAAfABAAX19NQUNPU1gvLl/QstC70L7QttC10L3QuNC1LnBuZ1VYDAD0GuZS +1xrmUvUBFABjYBVjZ2BiYPBNTFbwD1aIUIACkBgDJxAbMTAwCgNpIJ/RlQEZCDDgAI4hIUEQFljH +CyCORVPCBBXXAqLk/Fy9xIKCnFS93NSSxJTEkkSrbF8Xz5LUXM/i4OSi1NQ858SCktKiVKiZQkDC +mIFBB7c+FF0hlQVAnUkFOZnFJQYGnBxQUxihLmFEcxknTGVkcWpOanJJZn4eIS1CAFBLBwiY4lxO +mwAAAEUBAABQSwECFQMUAAgACAC2ZDtEE0mGR44MAAD4DAAAFAAMAAAAAAAAAABApIEAAAAA0LLQ +u9C+0LbQtdC90LjQtS5wbmdVWAgA9BrmUtca5lJQSwECFQMKAAAAAABWZjtEAAAAAAAAAAAAAAAA +CQAMAAAAAAAAAABA/UHgDAAAX19NQUNPU1gvVVgIAOQd5lLkHeZSUEsBAhUDFAAIAAgAtmQ7RJji +XE6bAAAARQEAAB8ADAAAAAAAAAAAQKSBFw0AAF9fTUFDT1NYLy5f0LLQu9C+0LbQtdC90LjQtS5w +bmdVWAgA9BrmUtca5lJQSwUGAAAAAAMAAwDqAAAADw4AAAAA + +--Apple-Mail=_396D2C77-FD0E-4640-8B6E-402C1A3F7C26-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_from_redmine_user.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_from_redmine_user.eml new file mode 100644 index 0000000..f4bbc08 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_from_redmine_user.eml @@ -0,0 +1,40 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "John Smith" +To: +Cc: +Bcc: +Subject: New support issue from redmine user +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_html_only.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_html_only.eml new file mode 100644 index 0000000..282cab2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_html_only.eml @@ -0,0 +1,21 @@ +x-receiver: +Received: from [127.0.0.1] ([127.0.0.1]) by somenet.foo with Quick 'n Easy Mail Server SMTP (1.0.0.0); + Sun, 14 Dec 2008 16:18:06 GMT +Message-ID: <494531B9.1070709@somenet.foo> +Date: Sun, 14 Dec 2008 17:18:01 +0100 +From: "New Customer" +User-Agent: Thunderbird 2.0.0.18 (Windows/20081105) +MIME-Version: 1.0 +To: redmine@somenet.foo +Subject: HTML email +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit + + + + + + +

        Header one

        Header two

        • one
        • two
        • three
        • four
        • five

        Header three

        1. one
        2. two
        3. three
        4. four
        5. five

        This is paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks, paragraph number one without line breaks

        This is paragraph number two with line breaks, paragraph number one with line breaks,
        paragraph number one with line breaks, paragraph number one with line breaks,
        paragraph number one with line breaks, paragraph number one with line breaks,

        Two lines breaks
        One line break

        + + diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_iso_8859_15.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_iso_8859_15.eml new file mode 100644 index 0000000..898a929 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_iso_8859_15.eml @@ -0,0 +1,213 @@ + +Delivered-To: kundenservice@regiohelden.de +Received: by 10.182.38.68 with SMTP id e4csp1190obk; + Wed, 26 Jun 2013 01:36:42 -0700 (PDT) +X-Received: by 10.194.243.164 with SMTP id wz4mr1987170wjc.28.1372235802080; + Wed, 26 Jun 2013 01:36:42 -0700 (PDT) +Return-Path: +Received: from serv01.consus.info (serv01.consus.info. [188.40.98.75]) + by mx.google.com with ESMTP id fx15si2592703wic.56.2013.06.26.01.36.40 + for ; + Wed, 26 Jun 2013 01:36:41 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of joachim.hoehl@consus.info designates 188.40.98.75 as permitted sender) client-ip=188.40.98.75; +Authentication-Results: mx.google.com; + spf=pass (google.com: best guess record for domain of joachim.hoehl@consus.info designates 188.40.98.75 as permitted sender) smtp.mail=joachim.hoehl@consus.info +Received: from localhost (localhost [127.0.0.1]) + by serv01.consus.info (Postfix) with ESMTP id B67472634163 + for ; Wed, 26 Jun 2013 10:36:40 +0200 (CEST) +X-Virus-Scanned: amavisd-new at serv01.consus.info +Received: from [192.168.178.26] (aftr-37-24-151-159.unity-media.net [37.24.151.159]) + by serv01.consus.info (Postfix) with ESMTPSA id 2E8D02634138 + for ; Wed, 26 Jun 2013 10:36:39 +0200 (CEST) +Message-ID: <51CAA81B.3060900@consus.info> +Date: Wed, 26 Jun 2013 10:36:43 +0200 +From: =?ISO-8859-15?Q?Joachim_H=F6hl?= +User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:17.0) Gecko/20130509 Thunderbird/17.0.6 +MIME-Version: 1.0 +To: kundenservice@regiohelden.de +Subject: Nachgefragt 1 :: Monatsbericht - Mai 2013 +References: <51AF2F3D.6050008@consus.info> +In-Reply-To: <51AF2F3D.6050008@consus.info> +X-Forwarded-Message-Id: <51AF2F3D.6050008@consus.info> +Content-Type: multipart/alternative; + boundary="------------050204050002090000010209" + +This is a multi-part message in MIME format. +--------------050204050002090000010209 +Content-Type: text/plain; charset=ISO-8859-15; format=flowed +Content-Transfer-Encoding: 8bit + + + + +-------- Original-Nachricht -------- +Betreff: Re: RegioHelden :: Monatsbericht - Mai 2013 +Datum: Wed, 05 Jun 2013 14:29:49 +0200 +Von: Joachim Höhl +An: Regiohelden.de + + + +Sehr geehrte Damen und Herren, + + + +im Hinblick auf das beendete Vertragsverhältnis bitte ich Sie höflichst +um Schlussabrechnung und Erstattung nicht verbrauchten Budgets auf das +Ihnen bekannte Konto bis zum 15.06.2013. + + +Mit freundlichen Grüßen + +RA Joachim Höhl + +Consus Informations GmbH +Benesisstr. 24-32 +50672 Köln + +mailto:joachim.hoehl@consus.info + +fon 0221 / 29 20 120 +fax 0221 / 29 20 112 + +HRB 39774 Amtsgericht Köln + +Geschäftsführer: Joachim Höhl, Wolfgang Hoss + +Am 01.06.2013 07:16, schrieb Regiohelden.de: +> Sehr geehrter RegioHelden-Kunde, +> +> anbei erhalten Sie die monatliche Auswertung Ihrer Google AdWords +> Werbekampagne. +> +> Eine detaillierte Übersicht der Erfolgskennzahlen finden Sie außerdem +> tagesaktuell unter http://regioheld.com/administration/login +> Sollten Sie Ihre Zugangsdaten vergessen haben, senden wir Ihnen diese +> gerne erneut zu. +> Bei weiteren Fragen erreichen Sie uns unter der (0711) 128 501-0 +> +> Mit den besten Grüßen +> Ihr RegioHelden-Team! +> +> Telefon: (0711) 128 501-0 +> Fax: (0711) 128 501-99 +> www.RegioHelden.de +> +> Die RegioHelden wurden 2010 durch die Europäische Union und das +> Bundesministerium für Wirtschaft und Technologie (BMWi) gefördert. +> Mehr Informationen zur Förderung unter: +> http://www.RegioHelden.de/Förderung +> +> +> RegioHelden GmbH +> Marienstraße 23 - 70178 Stuttgart - DE +> Sitz der Gesellschaft:Stuttgart +> Amtsgericht Stuttgart, HRB 733189 +> Geschäftsführer: Feliks Eyser + + + + +--------------050204050002090000010209 +Content-Type: text/html; charset=ISO-8859-15 +Content-Transfer-Encoding: 8bit + + + + + + + +
        +

        +
        + -------- Original-Nachricht -------- + + + + + + + + + + + + + + + + + + + +
        Betreff: + Re: RegioHelden :: Monatsbericht - Mai 2013
        Datum: Wed, 05 Jun 2013 14:29:49 +0200
        Von: Joachim Höhl <joachim.hoehl@consus.info>
        An: Regiohelden.de <kundenservice@regiohelden.de>
        +
        +
        +
        Sehr geehrte Damen und Herren,
        +
        +
        +
        +im Hinblick auf das beendete Vertragsverhältnis bitte ich Sie höflichst 
        +um Schlussabrechnung und Erstattung nicht verbrauchten Budgets auf das 
        +Ihnen bekannte Konto bis zum 15.06.2013.
        +
        +
        +Mit freundlichen Grüßen
        +
        +RA Joachim Höhl
        +
        +Consus Informations GmbH
        +Benesisstr. 24-32
        +50672 Köln
        +
        +mailto:joachim.hoehl@consus.info
        +
        +fon 0221 / 29 20 120
        +fax 0221 / 29 20 112
        +
        +HRB 39774 Amtsgericht Köln
        +
        +Geschäftsführer: Joachim Höhl, Wolfgang Hoss
        +
        +Am 01.06.2013 07:16, schrieb Regiohelden.de:
        +> Sehr geehrter RegioHelden-Kunde,
        +>
        +> anbei erhalten Sie die monatliche Auswertung Ihrer Google AdWords 
        +> Werbekampagne.
        +>
        +> Eine detaillierte Übersicht der Erfolgskennzahlen finden Sie außerdem 
        +> tagesaktuell unter http://regioheld.com/administration/login
        +> Sollten Sie Ihre Zugangsdaten vergessen haben, senden wir Ihnen diese 
        +> gerne erneut zu.
        +> Bei weiteren Fragen erreichen Sie uns unter der (0711) 128 501-0
        +>
        +> Mit den besten Grüßen
        +> Ihr RegioHelden-Team!
        +>
        +> Telefon: (0711) 128 501-0
        +> Fax: (0711) 128 501-99
        +> www.RegioHelden.de
        +>
        +> Die RegioHelden wurden 2010 durch die Europäische Union und das 
        +> Bundesministerium für Wirtschaft und Technologie (BMWi) gefördert.
        +> Mehr Informationen zur Förderung unter: 
        +> http://www.RegioHelden.de/Förderung 
        +> <http://www.RegioHelden.de/F%F6rderung>
        +>
        +> RegioHelden GmbH
        +> Marienstraße 23 - 70178 Stuttgart - DE
        +> Sitz der Gesellschaft:Stuttgart
        +> Amtsgericht Stuttgart, HRB 733189
        +> Geschäftsführer: Feliks Eyser
        +
        +
        +
        +
        +
        + + + +--------------050204050002090000010209-- \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_koi8_r.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_koi8_r.eml new file mode 100644 index 0000000..bcc5266 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_koi8_r.eml @@ -0,0 +1,40 @@ +Delivered-To: support@redminecrm.com +Received: by 10.224.146.65 with SMTP id g1csp215673qav; + Wed, 14 Aug 2013 03:23:43 -0700 (PDT) +X-Received: by 10.112.210.136 with SMTP id mu8mr7637164lbc.25.1376475823205; + Wed, 14 Aug 2013 03:23:43 -0700 (PDT) +Return-Path: +Received: from forward16.mail.yandex.net (forward16.mail.yandex.net. [2a02:6b8:0:1402::1]) + by mx.google.com with ESMTP id gm9si19775110lbc.106.2013.08.14.03.23.42 + for ; + Wed, 14 Aug 2013 03:23:43 -0700 (PDT) +Received-SPF: pass (google.com: domain of roman@shipiev.pro designates 2a02:6b8:0:1402::1 as permitted sender) client-ip=2a02:6b8:0:1402::1; +Authentication-Results: mx.google.com; + spf=pass (google.com: domain of roman@shipiev.pro designates 2a02:6b8:0:1402::1 as permitted sender) smtp.mail=roman@shipiev.pro; + dkim=pass header.i=@shipiev.pro +Received: from web4g.yandex.ru (web4g.yandex.ru [95.108.252.104]) + by forward16.mail.yandex.net (Yandex) with ESMTP id 48F89D21474 + for ; Wed, 14 Aug 2013 14:23:42 +0400 (MSK) +Received: from 127.0.0.1 (localhost.localdomain [127.0.0.1]) + by web4g.yandex.ru (Yandex) with ESMTP id E31C31AC8004; + Wed, 14 Aug 2013 14:23:41 +0400 (MSK) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=shipiev.pro; s=mail; + t=1376475822; bh=K7MXxSAFi0LqsitVh6FYdes2wgX24T6bpFG9NTMxZrs=; + h=From:To:Subject:Date; + b=rLN448CnTfbjg0AtUwdiNezdHM3ZOBPJLfUivjRmeNrApLRuMFcTxUmfZx2lxeRYt + tF5Jz6bF+tyguW1b+5cwVkDsAD6x91fmN4csifg8q1dagSyNWnhN8c9zkxqaLIpnyR + Sc641GlK9xx4OuYeoalUwcE6frDb9IKHD/Y9L5us= +Received: from [89.17.39.194] ([89.17.39.194]) by web4g.yandex.ru with HTTP; + Wed, 14 Aug 2013 14:23:41 +0400 +From: =?koi8-r?B?+8nQycXXIPLPzcHO?= +Envelope-From: roman@shipiev.me +To: RedmineCRM +Subject: =?koi8-r?B?58TFINfXxdPUySDQz8zFIHVzZXIuY29tcGFueT8=?= +MIME-Version: 1.0 +Message-Id: <196871376475821@web4g.yandex.ru> +X-Mailer: Yamail [ http://yandex.ru ] 5.0 +Date: Wed, 14 Aug 2013 13:23:41 +0300 +Content-Transfer-Encoding: 8bit +Content-Type: text/plain; charset=koi8-r + +òÅÞØ ÉÄÅÔ ÐÒÏ ÐÌÁÇÉÎ redmine_people 0.1.6. ðÏÌÅ company × ÍÏÄÅÌØ User ÄÏÂÁ×ÌÑÅÔÓÑ, Á ÉÎÔÅÒÆÅÊÓÁ ÄÌÑ ÅÇÏ ××ÏÄÁ Ñ ÔÁË É ÎÅ ÎÁÛÅÌ. \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_win1251.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_win1251.eml new file mode 100644 index 0000000..7a5a2b3 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_in_win1251.eml @@ -0,0 +1,44 @@ +Received: from mxfront10m.mail.yandex.net ([127.0.0.1]) + by mxfront10m.mail.yandex.net with LMTP id Luw8wKCX + for ; Thu, 23 Jan 2014 07:21:56 +0400 +Received: from server39.hosting.com (server39.hosting.com [11.22.33.44]) + by mxfront10m.mail.yandex.net (nwsmtp/Yandex) with ESMTPS id 7lL0OpbbJv-LuFCVrZb; + Thu, 23 Jan 2014 07:21:56 +0400 + (using TLSv1 with cipher AES256-SHA (256/256 bits)) + (Client certificate not present) +Received: from u7886911 by server39.hosting.com with local (Exim 4.72) + (envelope-from ) + id 1W6Arc-0001V8-2V + for admin@somenet.foo; Thu, 23 Jan 2014 07:21:56 +0400 +Date: Thu, 23 Jan 2014 07:21:56 +0400 +Message-Id: +To: admin@somenet.foo +Subject: Ðåçóëüòàòû ñîðåâíîâàíèé ÑÒÑÐ +X-PHP-Originating-Script: 2540:send_results.php +From: RDSU Results Check +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="----8c65880a4c29e0f5" +Return-Path: rusdancesport@gmail.com +X-Yandex-Forward: 4a1d766bfd8c33858729d5998daf62e5 + +------8c65880a4c29e0f5 +Content-Type: text/html; charset=Windows-1251 +Content-Transfer-Encoding: 8bit + +Ðåçóëüòàòû ñîðåâíîâàíèé ÑÒÑÐ + + + + + + +
        Íàçâàíèå:Òàíöåâàëüíûé ðèíã - 2014
        Äàòû:19.01.2014
        Ãîðîä:Ñàðàïóë
        Îðãàíèçàòîð:Ñîâðåìåííèê, Ðóäàâèíà Îëüãà
        Ñ÷åòíàÿ êîìèññèÿ:Êîðñèêîâ Àëåêñåé Íèêîëàåâè÷, Èæåâñê
        + +------8c65880a4c29e0f5 +Content-Type: application/xml; name="2014_01_23__07_21_39.xml" +Content-Disposition: attachment; filename="2014_01_23__07_21_39.xml" +Content-Transfer-Encoding: base64 + +PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0id2luZG93cy0xMjUxIj8+DQo8RGFuY2VEYXRhIHZlcnNpb249IjIuMSI+PEdyb3VwRGF0YT48SGVhZGVyIGxhbmd1YWdlPSJSdXNzaWFuIj48VGl0bGUgc3RhdHVzPSLQ7vHx6Onx6u7lIPHu8OXi7e7i4O3o5SDq4PIuIEIiIGRhdGVDb21wPSIxOS4wMS4yMDE0Ij7S4O325eLg6/zt++kg8Ojt4yAtIDIwMTQ8L1RpdGxlPjxDaXR5PtHg8ODv8+s8L0NpdHk+PE9yZ2FuaXplcj7R7uLw5ezl7e3o6iwg0PPk4OLo7eAgzuv84+A8L09yZ2FuaXplcj48SW5pdGlhdG9yPtTS0SDQ5fHv8+Hr6OroINPk7PPw8uj/PC9Jbml0aWF0b3I+PENvdW50cnk+0O7x8ej/PC9Db3VudHJ5PjxSZWdpb25JjMyMjIyMjMyMzwvUmVzdWx0PjxSZXN1bHQgbj0iNSIgcGxhY2U9IjEiIGhlYWQ9IjEiPjExMTExMTIxMTwvUmVzdWx0PjxSZXN1bHQgbj0iNiIgcGxhY2U9IjQiIGhlYWQ9IjEiPjQ1MzQ0NTE1MjwvUmVzdWx0PjwvRGFuY2U+PC9EYW5jZXM+PC9Sb3VuZD48L1Jlc3VsdHM+PC9Hcm91cERhdGE+PC9EYW5jZURhdGE+DQo= +------8c65880a4c29e0f5-- + diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_bq_encoding.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_bq_encoding.eml new file mode 100644 index 0000000..7f3fe21 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_bq_encoding.eml @@ -0,0 +1,101 @@ +Return-Path: +Received: from ZiX.example.org (unknown [192.168.158.6]) by bazissoft.ru (Postfix) with ESMTP id 46AFE222A5D for ; Mon, 06 May 2013 11:49:04 +0400 +Date: Mon, 06 May 2013 11:50:29 +0400 +From: =?utf-8?B?0J/Qu9C10YLQvdGR0LIg0JDQu9C10LrRgdC10Lk=?= +Reply-To: =?utf-8?B?0J/Qu9C10YLQvdGR0LIg0JDQu9C10LrRgdC10Lk=?= +To: =?utf-8?B?0JHQsNC30LjRgS3QptC10L3RgtGAINCT0YDRg9C/0L/QsCDRgtC10YXQvdC40YfQtdGB0Lo=?= =?utf-8?B?0L7QuSDQv9C+0LTQtNC10YDQttC60Lg=?= +Message-ID: <1784691455.20130506115029@bazissoft.ru> +In-Reply-To: <5187605c2a559_29e2425680045842@RedmineSRV.mail> +References: <231744907.20130506114902@bazissoft.ru> + <5187605c2a559_29e2425680045842@RedmineSRV.mail> +Subject: =?UTF-8?Q?Re[2]:_=D0=97=D0=B0=D0=BF=D1=80=D0=BE=D1=81_=D0=BD=D0=B0?= + =?UTF-8?Q?_=D1=82=D0=B5=D1=85.?= + =?UTF-8?Q?_=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D1=83.?= + =?UTF-8?Q?_[=D0=A2=D0=B5=D1=85=D0=BD=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B0=D1=8F?= + =?UTF-8?Q?_=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0_#2065]?= +Mime-Version: 1.0 +Content-Type: text/html; + charset=utf-8 +Content-Transfer-Encoding: quoted-printable +Delivered-To: redmine@bazissoft.ru +X-Mailer: The Bat! (v4.0.24) Professional +Organization: =?UTF-8?Q?=D0=9E=D0=9E=D0=9E?= + =?UTF-8?Q?_=22=D0=91=D0=B0=D0=B7=D0=B8=D1=81-=D0=A6=D0=B5=D0=BD=D1=82=D1=80=22?= +X-Priority: 3 (Normal) + +=0D +Re[2]: =C7=E0=EF=F0=EE=F1 =ED=E0 =F2=E5=F5. =EF=EE=E4=E4= +=E5=F0=E6=EA=F3. [=D2=E5=F5=ED=E8=F7=E5=F1=EA=E0=FF =EF=EE=E4=E4=E5=F0=E6= +=EA=E0 #2065]=0D +=0D= + +=0D +=0D +=0D +=0D +

        =D0=9E=D1=82=D0=B2=D0=B5=D1=82 =D0=BD=D0=B0 =D0=BF=D0=B8=D1=81=D1=8C=D0= +=BC=D0=BE.

        =0D +


        =0D +
        =0D +=0D +=0D +=0D +=0D +
        =0D +

        >

        =0D +
        =0D +

        -----------------------------------------=0D +

        =D0=9F=D0=BE=D0=B6=D0=B0=D0=BB=D1=83=D0=B9=D1=81=D1= +=82=D0=B0, =D0=BF=D0=B8=D1=88=D0=B8=D1=82=D0=B5 =D0=B2=D0=B0=D1=88 =D0=BE= +=D1=82=D0=B2=D0=B5=D1=82 =D0=B2=D1=8B=D1=88=D0=B5 =D1=8D=D1=82=D0=BE=D0=B9= + =D0=BB=D0=B8=D0=BD=D0=B8=D0=B8. =D0=92=D0=B5=D1=81=D1=8C =D1=82=D0=B5=D0= +=BA=D1=81=D1=82, =D0=BD=D0=B0=D1=85=D0=BE=D0=B4=D1=8F=D1=89=D0=B8=D0=B9=D1= +=81=D1=8F =D0=BD=D0=B8=D0=B6=D0=B5 =D0=BD=D0=B5=D1=91 =D0=B1=D1=83=D0=B4=D0= +=B5=D1=82 =D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD =D1=81=D0=B8=D1=81=D1=82=D0= +=B5=D0=BC=D0=BE=D0=B9 =D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1= +=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B9 =D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1= +=82=D0=BA=D0=B8 =D0=BF=D0=BE=D1=87=D1=82=D1=8B.

        =0D +

        =D0=97=D0=B4=D1=80=D0=B0=D0=B2=D1=81=D1=82=D0=B2=D1= +=83=D0=B9=D1=82=D0=B5, =D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9 =D0=9F=D0= +=BB=D0=B5=D1=82=D0=BD=D1=91=D0=B2 .

        =0D +

        =D0=A2=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9 =D0= +=BE=D1=82=D0=B2=D0=B5=D1=82

        =0D +

        =D0=A1 =D1=83=D0=B2=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0= +=B5=D0=BC, 

        =0D +

        =D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9 =D0=9F=D0= +=BB=D0=B5=D1=82=D0=BD=D1=91=D0=B2

        =0D +

        =D0=93=D1=80=D1=83=D0=BF=D0=BF=D0=B0 =D1=82=D0=B5=D1= +=85=D0=BD=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B9 =D0=BF=D0=BE=D0=B4=D0= +=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8

        =0D +

        =D0=9E=D0=9E=D0=9E "=D0=91=D0=B0=D0=B7=D0=B8=D1=81= +-=D0=A6=D0=B5=D0=BD=D1=82=D1=80" 

        =0D +

        =D0=A2=D0=B5=D0=BB.: +7(496)623-09-90

        =0D= + +

        E-mail: gtp@bazissoft.ru

        =0D +
        =0D +
        =0D +


        =0D +


        =0D +


        =0D +


        =0D +

        Best regards,

        =0D +

        =D0=9F=D0=BB=D0=B5=D1=82=D0=BD=D1=91=D0=B2 =D0=90=D0=BB=D0=B5=D0=BA=D1= +=81=D0=B5=D0=B9

        =0D +

        =D0=A0=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=87=D0=B8=D0=BA =D0= +=A1=D0=97

        =0D +

        E-mail: zix@baz= +issoft.ru

        =0D +

           ICQ: 310-243-476

        =0D +

         Skype: zix_bazis

        =0D +


        =0D +

        =D0=9E=D0=9E=D0=9E "=D0=91=D0=B0=D0=B7=D0=B8=D1=81-=D0=A6=D0=B5=D0=BD=D1= +=82=D1=80"

        =0D +

        E-mail: info@b= +azissoft.ru

        =0D +

           Web: www.bazissoft.ru

        =0D +

          =D0=A2=D0=B5=D0=BB.: +7 (496) 623-09-90

        =0D +

          =D0=A4=D0=B0=D0=BA=D1=81: +7 (496) 623-09-90

        =0D +=0D += diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_cc.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_cc.eml new file mode 100644 index 0000000..a7b7ba2 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_cc.eml @@ -0,0 +1,39 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: , =?koi8-r?B?7cHSwdQg4c3Jzs/X?= , Ivanov Ivan +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_from.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_from.eml new file mode 100644 index 0000000..ac5b927 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_from.eml @@ -0,0 +1,38 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_to.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_to.eml new file mode 100644 index 0000000..0d7456f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_empty_to.eml @@ -0,0 +1,39 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_encoded_attachment.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_encoded_attachment.eml new file mode 100755 index 0000000..f0b657b --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_encoded_attachment.eml @@ -0,0 +1,73 @@ +Return-Path: +Delivery-Date: Fri, 24 Jan 2014 14:59:33 +0100 +Subject: =?iso-8859-1?Q?asdf?= +From: =?iso-8859-1?Q?Alexander_Test?= +To: =?iso-8859-1?Q?ticketsystem=40accenon=2Ede?= +Date: Fri, 24 Jan 2014 14:59:31 +0100 +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_37DUSAHCWwoZnvFM2hGvfAuk9vZCvQYsVUWDM0OphZysoX6w" +X-Priority: 3 (Normal) +X-Mailer: Zarafa 7.0.5-31880 +Thread-Index: Ac8ZDGrxoIr7vlf1QsO6Pf5HsUiilA== +Message-Id: +Envelope-To: ticketsystem@somenet.de + +This is a multi-part message in MIME format. Your mail reader does not +understand MIME message format. +--=_37DUSAHCWwoZnvFM2hGvfAuk9vZCvQYsVUWDM0OphZysoX6w +Content-Type: multipart/alternative; + boundary="=_37DUpBkBg8D0MxDzg4gxN5c2U6-Q8s+xVUOqL0WFFSm109Do" + +--=_37DUpBkBg8D0MxDzg4gxN5c2U6-Q8s+xVUOqL0WFFSm109Do +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +This ticket is for testing purposes.=0D=0A=0D=0A +--=_37DUpBkBg8D0MxDzg4gxN5c2U6-Q8s+xVUOqL0WFFSm109Do +Content-Type: text/html; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +

        This ticket is for testing purposes.<= +o:p>

        +--=_37DUpBkBg8D0MxDzg4gxN5c2U6-Q8s+xVUOqL0WFFSm109Do-- + +--=_37DUSAHCWwoZnvFM2hGvfAuk9vZCvQYsVUWDM0OphZysoX6w +Content-Type: image/png; name*=iso-8859-1''%4D%FC%6C%6C%65%72%2E%70%6E%67 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename*=iso-8859-1''%4D%FC%6C%6C%65%72%2E%70%6E%67 + +iVBORw0KGgoAAAANSUhEUgAAA/MAAAK8CAYAAACjhe+uAAAAAXNSR0IArs4c6QAAAARnQU1B +AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAOZWSURBVHhe7f15sB3VvecL6r+K6lcV +0VHR748X/V71q1evo1/HtTFcuqIqol69W7eGft1Vr/t1xK0ydW1jG+7FNjYe7r2F62KMADPP +ZrQNRkLMgxjEJBkZEEISQhIyEpOEBEIDAsQgMWkApNX7t8U65Mmzc+fvt4bM3Ht/TkTGOWfn +wAAMwAAMwAAMwAAMwAAMwAAMJGBAjPzfXnCNe/DxZ/KYeRnNf3z18+60y65zMrrPggYwAAMw +AAMwAAMwAAMwAAMwAAMwEMeAjMjf/+hqt2nbHvf/B5DKfKY0wjWRAAAAAElFTkSuQmCC +--=_37DUSAHCWwoZnvFM2hGvfAuk9vZCvQYsVUWDM0OphZysoX6w-- + diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_in_reply_to.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_in_reply_to.eml new file mode 100644 index 0000000..0269962 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_in_reply_to.eml @@ -0,0 +1,39 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +In-Reply-To: <123456789@mail.com> +From: "New Customer-Name" +To: +Cc: , =?koi8-r?B?7cHSwdQg4c3Jzs/X?= , Ivanov Ivan +Subject: Reply to support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_japanese.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_japanese.eml new file mode 100644 index 0000000..4d6a1aa --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_japanese.eml @@ -0,0 +1,60 @@ +Delivered-To: uq_support@i-trenta.net +Received: by 10.194.51.100 with SMTP id j4csp10696wjo; + Fri, 8 Aug 2014 21:07:09 -0700 (PDT) +X-Received: by 10.66.237.206 with SMTP id ve14mr1081190pac.40.1407557228385; + Fri, 08 Aug 2014 21:07:08 -0700 (PDT) +Return-Path: +Received: from wp023.wappy.ne.jp (wp023.wappy.ne.jp. [203.145.230.194]) + by mx.google.com with ESMTPS id iw4si7544395pac.156.2014.08.08.21.07.07 + for + (version=TLSv1.2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); + Fri, 08 Aug 2014 21:07:08 -0700 (PDT) +Received-SPF: softfail (google.com: domain of transitioning kent_guiseppe_saida@icloud.com does not designate 203.145.230.194 as permitted sender) client-ip=203.145.230.194; +Authentication-Results: mx.google.com; + spf=softfail (google.com: domain of transitioning kent_guiseppe_saida@icloud.com does not designate 203.145.230.194 as permitted sender) smtp.mail=kent_guiseppe_saida@icloud.com; + dmarc=fail (p=NONE dis=NONE) header.from=icloud.com +Received: by wp023.wappy.ne.jp (Postfix, from userid 110) + id 3FE6E880331; Sat, 9 Aug 2014 13:07:05 +0900 (JST) +X-Original-To: support-uq@j-akua.com +Delivered-To: support-uq@j-akua.com +X-No-Auth: unauthenticated sender +X-No-Relay: not in my network +Received: from st13p13im-asmtp002.me.com (st13p13im-asmtp002.me.com [17.164.56.161]) + by wp023.wappy.ne.jp (Postfix) with ESMTPS id A5CD588032B + for ; Sat, 9 Aug 2014 13:07:04 +0900 (JST) +Received: from [192.168.1.11] + (pool-96-245-83-150.phlapa.fios.verizon.net [96.245.83.150]) + by st13p13im-asmtp002.me.com + (Oracle Communications Messaging Server 7u4-27.10(7.0.4.27.9) 64bit (built Jun + 6 2014)) with ESMTPSA id <0NA0009VJU3P7W20@st13p13im-asmtp002.me.com> for + support-uq@j-akua.com; Sat, 09 Aug 2014 04:07:02 +0000 (GMT) +X-Proofpoint-Virus-Version: vendor=fsecure + engine=2.50.10432:5.12.52,1.0.27,0.0.0000 + definitions=2014-08-09_01:2014-08-08,2014-08-09,1970-01-01 signatures=0 +X-Proofpoint-Spam-Details: rule=notspam policy=default score=0 spamscore=0 + suspectscore=0 phishscore=0 adultscore=0 bulkscore=0 classifier=spam adjust=0 + reason=mlx scancount=1 engine=7.0.1-1402240000 definitions=main-1408090054 +Subject: =?iso-2022-jp?B?GyRCJCpMZCQkOWckbyQ7GyhC?= +From: =?iso-2022-jp?B?GyRCc25FRCUxJXMlSBsoQg==?= + +Content-type: text/plain; charset=iso-2022-jp +X-Mailer: iPhone Mail (11D257) +Message-id: +Date: Sat, 09 Aug 2014 00:06:59 -0400 +To: "support-uq@j-akua.com" +Content-transfer-encoding: quoted-printable +MIME-version: 1.0 (1.0) + +=1B$B%W%l%$%d!=1B(B:=1B$B$W$s$1=1B(B +=1B$B%W%l%$%d!<=1B(BID=1B$B!'=1B(B252123764 +=1B$B%"%W%j%P!<%8%g%s=1B(B:1.0 +=1B$B$4;HMQ$N5! +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + + Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet + turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus + blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti + sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In + in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + + Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_multiline_subject.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_multiline_subject.eml new file mode 100644 index 0000000..a9b5bd0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_multiline_subject.eml @@ -0,0 +1,40 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: "New Customer-Name" +To: +Cc: , =?koi8-r?B?7cHSwdQg4c3Jzs/X?= , Ivanov Ivan +Subject: =?utf-8?B?INCf0YDQvtCy0LXRgNC60LAgKNC90LUg0L7RgtC60YDRi9Cy0LA=?= + =?utf-8?B?0LnRgtC1INGN0YLQviDQv9C40YHRjNC80L4p?= +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="utf-8"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_quotes.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_quotes.eml new file mode 100644 index 0000000..4b5d53c --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_quotes.eml @@ -0,0 +1,125 @@ +Return-Path: +Received: from [172.16.1.73] (helo=visa31) by natalie-tours.ru with esmtpa (Exim 4.69) (envelope-from ) id 1UPTqy-0002Yd-El for helpdesk@natalie-tours.ru; Tue, 09 Apr 2013 12:24:32 +0400 +Date: Tue, 09 Apr 2013 12:25:15 +0400 +From: =?koi8-r?B?7cHL08nNIPPL18/Sw8/X?= +To: helpdesk@natalie-tours.ru +Message-ID: <011401ce34fb$c5dc5530$5194ff90$@natalie-tours.ru> +Subject: =?UTF-8?Q?=F0=D2=CF=C2=CC=C5=CD=C1?= + =?UTF-8?Q?_=D0=D2=C9_=D3=CF=DA=C4=C1=CE=C9=C9_=D4=C9=D0=C1?= +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0115_01CE351D.4CEEDF90"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +Envelope-to: helpdesk@natalie-tours.ru +Delivery-date: Tue, 09 Apr 2013 12:24:32 +0400 +X-AntiVirus: Checked by Dr.Web [version: 6.0.13.01170, engine: 8.0.2.12140, + virus records: 4316875, updated: 9.04.2013] +X-Mailer: Microsoft Outlook 14.0 +Thread-Index: Ac40+7aorK35TJ2UT5eBejDhYc+68A== +Content-Language: ru + +This is a multipart message in MIME format. + +------=_NextPart_000_0115_01CE351D.4CEEDF90 +Date: Tue, 09 Apr 2013 12:30:02 +0400 +Mime-Version: 1.0 +Content-Type: text/plain; + charset=koi8-r +Content-Transfer-Encoding: base64 +Content-ID: <5163d18a8f092_6bfd3fcc730661b4110155@helpdesk.natalie-tours.ru.mail> + +68/MzMXHySENCg0KIA0KDQrw0skg08/axMHOyckg1yDh09Ugzs/X2cgg1MnQ +z9cgzs/NxdLP1yDOxSDQz83F3cHF1NPRIM7B2tfBzsnFIM7PzcXSwSA8ZGVs +dXhlDQpsYWdvb24gdmlsbGEgd2l0aCBwb29sPiDXINDPzMUgPOvP0s/Uy8/F +IM7B2tfBzsnFPi4g9MHLycggzs/NxdLP1yDNzs/HzywNCtzUz9Qg0NLJ18XM +IMzJ29ggxMzRINDSyc3F0sEuDQoNCiANCg0KIA0KDQrzINXXwdbFzsnFzSwN +Cg0K7cHL08nNIPPL18/Sw8/XDQoNCvLVy8/Xz8TJ1MXM2CDP1MTFzMEg1MHS +ycbP1yDJIMLB2iDEwc7O2cgNCg0K7+/vIDzuwdTBzMkg9NXS0z4NCg0K1C4g +KDQ5NSkgNzg1LTM3MjAgxM/CLjIwNzANCg0K1C4gKDQ5NSkgNzg1LTA3NDcg +xM/CLjIwNzANCg0KIA0KDQogDQoNCg== + + +------=_NextPart_000_0115_01CE351D.4CEEDF90 +Date: Tue, 09 Apr 2013 12:30:02 +0400 +Mime-Version: 1.0 +Content-Type: text/html; + charset=koi8-r +Content-Transfer-Encoding: quoted-printable +Content-ID: <5163d18a904fa_6bfd3fcc730661b4110274@helpdesk.natalie-tours.ru.mail> + +

        =EB=CF=CC=CC=C5= +=C7=C9!

         

        =F0=D2=C9 =D3=CF=DA=C4=C1=CE=C9=C9 =D7 =E1=D3=D5 =CE=CF=D7=D9=C8= + =D4=C9=D0=CF=D7 =CE=CF=CD=C5=D2=CF=D7 =CE=C5 =D0=CF=CD=C5=DD=C1=C5=D4=D3= +=D1 =CE=C1=DA=D7=C1=CE=C9=C5 =CE=CF=CD=C5=D2=C1 «deluxe lagoon vill= +a with pool» =D7 =D0=CF=CC=C5 «=EB=CF=D2=CF=D4=CB=CF=C5 =CE=C1= +=DA=D7=C1=CE=C9=C5». =F4=C1=CB=C9=C8 =CE=CF=CD=C5=D2=CF=D7 =CD=CE=CF= +=C7=CF, =DC=D4=CF=D4 =D0=D2=C9=D7=C5=CC =CC=C9=DB=D8 =C4=CC=D1 =D0=D2=C9=CD= +=C5=D2=C1.

         

         

        =F3 =D5=D7=C1=D6=C5=CE=C9=C5=CD,

        =ED=C1=CB= +=D3=C9=CD =F3=CB=D7=CF=D2=C3=CF=D7

        =F2=D5=CB=CF=D7=CF=C4=C9=D4=C5= +=CC=D8 =CF=D4=C4=C5=CC=C1 =D4=C1=D2=C9=C6=CF=D7 =C9 =C2=C1=DA =C4=C1=CE=CE= +=D9=C8

        =EF=EF=EF «=EE=C1=D4=C1=CC=C9 =F4=D5=D2=D3»

        =D4. (495) 785-3720 =C4=CF=C2.2070

        =D4. (495) 785-0747 =C4=CF=C2.2070

         

         

        = + + +------=_NextPart_000_0115_01CE351D.4CEEDF90-- \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_rus_attachment.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_rus_attachment.eml new file mode 100644 index 0000000..3a5784e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_with_rus_attachment.eml @@ -0,0 +1,51 @@ +Subject: =?koi8-r?Q?=F2=D5=D3=D3=CB=C1=D1_=D4=C5=CD=C1_Apple_Mail?= +Mime-Version: 1.0 (Mac OS X Mail 6.2 \(1499\)) +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_2ECEB346-20A5-4E34-B618-17DB880C1AF3" +X-Apple-Base-Url: x-msg://449/ +X-Universally-Unique-Identifier: f85e4a62-e4c5-4222-bf11-cc8bf7ccbd3d +X-Apple-Mail-Remote-Attachments: YES +From: "New Customer" +X-Apple-Windows-Friendly: 1 +Date: Fri, 26 Oct 2012 14:16:38 +0400 +X-Apple-Mail-Signature: +Message-Id: +X-Uniform-Type-Identifier: com.apple.mail-draft +To: "New Customer" + + +--Apple-Mail=_2ECEB346-20A5-4E34-B618-17DB880C1AF3 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; + charset=koi8-r + +=F7 = +=DC=D4=CF=CD =D0=C9=D3=D8=CD=C5 =C1=D4=D4=C1=DE =D3 =D2=D5=D3=D3=CB=C9=CD = +=CE=C1=DA=D7=C1=CE=C9=C5=CD
        = + +--Apple-Mail=_2ECEB346-20A5-4E34-B618-17DB880C1AF3 +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename*=koi8-r''%E1%D4%D4%C1%DE%20%CE%CF%CD%C5%D2%20%CF%C4%C9%CE.rtf +Content-Type: text/rtf; + x-mac-hide-extension=yes; + x-unix-mode=0644; + name="=?koi8-r?Q?=E1=D4=D4=C1=DE_=CE=CF=CD=C5=D2_=CF=C4=C9=CE=2Ertf?=" +Content-Id: <55154B24-C79C-47EF-ACB8-3D9B9B48E95D> + +{\rtf1\ansi\ansicpg1251\cocoartf1187\cocoasubrtf340 +{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural + +\f0\fs24 \cf0 \uc0\u1069 \u1090 \u1086 +\b \uc0\u1072 \u1090 \u1090 \u1072 \u1095 +\b0 \uc0\u1089 \u1088 \u1091 \u1089 \u1089 \u1082 \u1080 \u1084 \u1080 \u1084 \u1077 \u1085 \u1077 \u1084 } +--Apple-Mail=_2ECEB346-20A5-4E34-B618-17DB880C1AF3-- diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_without_name.eml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_without_name.eml new file mode 100644 index 0000000..2779fe7 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/ticket_without_name.eml @@ -0,0 +1,40 @@ +Return-Path: +Received: from osiris ([127.0.0.1]) + by OSIRIS + with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 +Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> +From: +To: +Cc: +Bcc: +Subject: New support issue from email +Date: Sun, 22 Jun 2008 12:28:07 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; + format=flowed; + charset="iso-8859-1"; + reply-type=original +Content-Transfer-Encoding: 7bit +X-Priority: 3 +X-MSMail-Priority: Normal +X-Mailer: Microsoft Outlook Express 6.00.2900.2869 +X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet +turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus +blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti +sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In +in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras +sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum +id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus +eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique +sed, mauris. Pellentesque habitant morbi tristique senectus et netus et +malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse +platea dictumst. + +Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque +sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. +Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, +dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, +massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo +pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_tickets.yml b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_tickets.yml new file mode 100644 index 0000000..511eb03 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_tickets.yml @@ -0,0 +1,37 @@ +ticket001: + id: 1 + contact_id: 1 + issue_id: 1 + from_address: "test1@mail.address" + ticket_date: "<%= 10.days.ago.to_s(:db) %>" + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + is_incoming: true + first_response_time: 3600 + reaction_time: 13521 +ticket002: + id: 2 + contact_id: 2 + issue_id: 2 + from_address: "test2@mail.address" + ticket_date: "<%= 100.days.ago.to_s(:db) %>" + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + first_response_time: 7800 + is_incoming: true +ticket003: + id: 3 + contact_id: 1 + issue_id: 5 + from_address: "test1@mail.address" + ticket_date: "<%= 2.hours.ago.to_s(:db) %>" + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + is_incoming: false + first_response_time: 9200 +ticket004: + id: 4 + contact_id: 3 + issue_id: 8 + from_address: "test1@mail.address" + ticket_date: "<%= 2.hours.ago.to_s(:db) %>" + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + is_incoming: false + first_response_time: 9200 diff --git a/plugins/redmine_contacts_helpdesk/test/fixtures/journal_messages.yml b/plugins/redmine_contacts_helpdesk/test/fixtures/journal_messages.yml new file mode 100644 index 0000000..c2b282e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/fixtures/journal_messages.yml @@ -0,0 +1,15 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +jm001: + id: 1 + contact_id: 1 + journal_id: 1 + is_incoming: false + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + message_date: "<%= (Time.now - 2.days).to_s(:db) %>" +jm002: + id: 2 + contact_id: 1 + journal_id: 2 + is_incoming: true + source: <%= HelpdeskTicket::HELPDESK_EMAIL_SOURCE %> + message_date: "<%= (Time.now - 10.hours).to_s(:db) %>" diff --git a/plugins/redmine_contacts_helpdesk/test/functional/canned_responses_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/canned_responses_controller_test.rb new file mode 100644 index 0000000..1ef2b6e --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/canned_responses_controller_test.rb @@ -0,0 +1,85 @@ +require File.expand_path('../../test_helper', __FILE__) + +class CannedResponsesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :canned_responses, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + @request.session[:user_id] = 1 + # @response = ActionController::TestResponse.new + end + + def test_should_get_index + get :index, :project_id => 1 + assert_response 200 + end + + def test_should_get_new + get :new, :project_id => 1 + assert_response 200 + end + + def test_should_get_edit + get :edit, :id => 1 + assert_response 200 + end + + def test_should_post_create + post :create, :canned_response => {:name => "New canned response", :content => "Hi there!", :is_public => false}, :project_id => 1 + assert_redirected_to settings_project_path(Project.find('ecookbook'), :tab => 'helpdesk_canned_responses') + assert_equal "New canned response", CannedResponse.last.name + end + + def test_should_put_update + put :update, :id => 1, :canned_response => {:name => "New name"} + assert_redirected_to settings_project_path(Project.find('ecookbook'), :tab => 'helpdesk_canned_responses') + assert_equal "New name", CannedResponse.find(1).name + end + + def test_should_delete_destroy + delete :destroy, :id => 1 + assert_redirected_to settings_project_path(Project.find('ecookbook'), :tab => 'helpdesk_canned_responses') + assert_nil CannedResponse.find_by_id(1) + end + + def test_should_get_add + xhr :get, :add, :id => 1, :project_id => 1, :issue_id => 1 + assert_response 200 + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/contacts_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/contacts_controller_test.rb new file mode 100644 index 0000000..b97ffb5 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/contacts_controller_test.rb @@ -0,0 +1,87 @@ +require File.expand_path('../../test_helper', __FILE__) + +# Re-raise errors caught by the controller. +# class HelpdeskMailerController; def rescue_action(e) raise e end; end + +class ContactsControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures( + Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', + [ + :contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries + ] + ) + + RedmineHelpdesk::TestCase.create_fixtures( + Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', + [ :journal_messages, :helpdesk_tickets] + ) + + include RedmineHelpdesk::TestHelper + + def setup + RedmineHelpdesk::TestCase.prepare + ActionMailer::Base.deliveries.clear + + @controller = ContactsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_contacts_with_closed_tickets + @request.session[:user_id] = 1 + get 'index', "f"=>["open_tickets", ""], "op"=>{"open_tickets"=>"="}, + "v"=>{"open_tickets"=>["0"]} + assert_response :success + assert !assigns(:contacts).include?(Contact.find(1)) + end + + def test_contacts_with_open_tickets + @request.session[:user_id] = 1 + get 'index', "f"=>["open_tickets", ""], "op" => { "open_tickets" => "=" }, + "v" => { "open_tickets"=>["1"] } + assert_response :success + assert assigns(:contacts).include?(Contact.find(1)) + end + + def test_contacts_with_number_of_tickets + @request.session[:user_id] = 1 + get 'index', "f"=>["number_of_tickets", ""], "op"=>{ "number_of_tickets" => "=" }, + "v"=>{ "number_of_tickets"=>["1"] } + assert_response :success + assigns(:contacts).each do |contact| + assert contact.helpdesk_tickets.count == 1 + end + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/contacts_duplicates_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/contacts_duplicates_controller_test.rb new file mode 100644 index 0000000..2e510c8 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/contacts_duplicates_controller_test.rb @@ -0,0 +1,60 @@ +require File.expand_path('../../test_helper', __FILE__) +# require 'contacts_duplicates_controller' + +class ContactsDuplicatesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + + @controller = ContactsDuplicatesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_merge_helpdesk_ticket_contacts + @request.session[:user_id] = 1 + total_tickets_count = Contact.find(2).tickets.count + Contact.find(1).tickets.count + get :merge, :project_id => 1, :contact_id => 1, :duplicate_id => 2 + assert_redirected_to :controller => "contacts", :action => 'show', :id => 2, :project_id => 'ecookbook' + + contact = Contact.find(2) + assert_equal total_tickets_count, contact.tickets.count + end + +end \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_controller_test.rb new file mode 100644 index 0000000..37a05e0 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_controller_test.rb @@ -0,0 +1,144 @@ +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) +# require 'helpdesk_controller' + + +class HelpdeskControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + FIXTURES_PATH = Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/helpdesk_mailer' + + def setup + RedmineHelpdesk::TestCase.prepare + + @controller = HelpdeskController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def credentials(user, password=nil) + ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user) + end + + def test_show_original + @request.session[:user_id] = 1 + Setting.default_language = 'en' + + a = Attachment.create!(:container => HelpdeskTicket.find(1), + :file => uploaded_file("new_issue_new_contact_ru_2.eml", "message/rfc822"), + :author => User.find(1)) + + get :show_original, :id => a, :project_id => 1 + assert_response :success + assert_template 'attachments/file' + assert_not_nil assigns(:content) + assert_match 'Программа автоматичеÑки заменила категории', @response.body + end + + def test_should_delete_spam + @request.session[:user_id] = 1 + Setting.default_language = 'en' + issue = Issue.new + issue.copy_from(1).save + contact = Contact.create(:first_name => "New contact", :project => Project.find('ecookbook'), :email => "mail@test.new") + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + assert_not_nil customer = issue.customer + + delete :delete_spam, :project_id => 1, :issue_id => issue.id + assert_redirected_to :controller => 'issues', :action => 'index', :project_id => 'ecookbook' + assert_nil Contact.find_by_id(contact.id) + assert_nil Issue.find_by_id(issue.id) + assert_match customer.primary_email, HelpdeskSettings["helpdesk_blacklist", '1'] + end + + def test_should_save_settings + @request.session[:user_id] = 1 + Setting.default_language = 'en' + @project = Project.find('ecookbook') + put :save_settings , :project_id => @project.id, "helpdesk_answer_from" => 'test@test.ru', "helpdesk_lifetime" => 60, :helpdesk_protocol => 'pop3', :helpdesk_host => 'pop3.test.ru' + assert_response :redirect + assert_equal('test@test.ru', ContactsSetting["helpdesk_answer_from", @project.id]) + assert_equal('60', ContactsSetting["helpdesk_lifetime", @project.id]) + assert_equal('pop3', ContactsSetting[:helpdesk_protocol, @project.id]) + assert_equal('pop3.test.ru', ContactsSetting[:helpdesk_host, @project.id]) + end + + def test_should_notify_sender_on_ticket_created_via_api + user = User.find(1) + user.pref[:no_self_notified] = false + user.pref.save + @request.session[:user_id] = 1 + @project = Project.find('ecookbook') + Setting.default_language = 'en' + Setting.rest_api_enabled = 1 + ContactsSetting["helpdesk_answer_from", @project.id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", @project.id] = 1 + token = Token.create!(:user => User.find(1), :action => 'api', :value => 'topsecret') + ActionMailer::Base.deliveries = [] + post :create_ticket, + :format => :xml, + :project_id => @project.id, + :key => token.value, + :ticket => { + :issue => { + :subject => 'test1', + :tracker_id => Tracker.first.id + }, + :contact => { + :email => 'test@example.com', + :first_name => 'John' + } + } + assert_response 201 + assert_equal(2, ActionMailer::Base.deliveries.count) + assert_equal(['test@example.com'], ActionMailer::Base.deliveries.last.to) + end + + private + + def uploaded_file(filename, mime) + fixture_file_upload("../../plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/#{filename}", mime, true) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_mailer_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_mailer_controller_test.rb new file mode 100644 index 0000000..fc0869d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_mailer_controller_test.rb @@ -0,0 +1,121 @@ +require File.expand_path('../../test_helper', __FILE__) + +# Re-raise errors caught by the controller. +# class HelpdeskMailerController; def rescue_action(e) raise e end; end + +class HelpdeskMailerControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages]) + + FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/helpdesk_mailer' + + def setup + RedmineHelpdesk::TestCase.prepare + + @controller = HelpdeskMailerController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_should_create_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + post :index, :key => 'secret', :issue => {:project_id => 'ecookbook', :status => 'Closed', :tracker => 'Bug', :assigned_to => 'jsmith'}, :email => IO.read(File.join(FIXTURES_PATH, 'new_issue_new_contact.eml')) + assert_response 201 + assert_not_nil Contact.find_by_first_name('New') + end + + def test_should_create_issue_from_mailhandler + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + post :index, :key => 'secret', :issue => {:project => 'ecookbook', :status => 'Closed', :tracker => 'Bug', :priority => 'low'}, :email => IO.read(File.join(FIXTURES_PATH, 'new_issue_new_contact.eml')) + assert_response 201 + assert_not_nil Contact.find_by_first_name('New') + end + + def test_should_use_project_helpdesk_settings_for_issue + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + # Project settings + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + ContactsSetting["helpdesk_assigned_to", Project.find('ecookbook').id] = 2 + ContactsSetting[:helpdesk_issue_due_date,Project.find('ecookbook').id] = Date.today + 5 + ActionMailer::Base.deliveries.clear + @request.session[:user_id] = 1 + + post :index, :key => 'secret', :issue => { :project => 'ecookbook' }, :email => IO.read(File.join(FIXTURES_PATH, 'new_issue_new_contact.eml')) + assert_response 201 + + issue = Issue.last + assert_equal 'Normal', issue.priority.name + assert_equal Date.today + 5, issue.due_date + assert_equal User.find(2).login, issue.assigned_to.login + contact = issue.customer + assert_equal "New", contact.first_name + end + + def test_should_get_mail + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + + post :get_mail, :key => 'secret' + assert_response :ok + end + + def test_should_change_state_for_ticket_on_reply + project = Project.find_by_identifier('ecookbook') + issue = Issue.find(5) + + # Enable API and set a key + Setting.mail_handler_api_enabled = 1 + Setting.mail_handler_api_key = 'secret' + ContactsSetting["helpdesk_reopen_status", project.id] = IssueStatus.where(:name => 'Feedback').first.id + + assert_not_equal 'Feedback', issue.status.name + post :index, :key => 'secret', :issue => { :project => 'ecookbook' }, :email => IO.read(File.join(FIXTURES_PATH, 'reply_from_contact.eml')) + + issue.reload + assert_equal 'Feedback', issue.status.name + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_reports_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_reports_controller_test.rb new file mode 100644 index 0000000..d99bb56 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_reports_controller_test.rb @@ -0,0 +1,129 @@ +require File.expand_path('../../test_helper', __FILE__) + +class HelpdeskReportsControllerTest < ActionController::TestCase + + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + + @controller = HelpdeskReportsController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show_first_response_time_report + HelpdeskDataCollectorFirstResponse.any_instance.stubs(:issues).returns(Issue.where(:id => 1)) + @request.session[:user_id] = 1 + get :show, :project_id => 'ecookbook', :report => 'first_response_time', + :set_filter => '1', + :f => ['message_date', ''], + :op => { 'message_date' => 't' } + assert_response :success + assert_select '#content h2', /First response time/ + assert_select '.chart_table .header .column_data', 8 + assert_select '.column_data .percents', 1 + assert_select 'tr.metrics td p', /Average first response time/ + assert_select 'tr.metrics td p', /Average closing ticket time/ + assert_select 'tr.metrics td p', /Average count of responses to close/ + assert_select 'tr.metrics td p', /Total replies/ + ensure + HelpdeskDataCollectorFirstResponse.any_instance.unstub(:issues) + end + + def test_show_first_response_time_report_without_params + HelpdeskDataCollectorFirstResponse.any_instance.stubs(:issues).returns(Issue.where(:id => 1)) + @request.session[:user_id] = 1 + get :show, :project_id => 'ecookbook', :report => 'first_response_time', + :set_filter => '1', + :f => [''] + assert_response :success + assert_select '#content h2', /First response time/ + assert_select '.chart_table .header .column_data', 8 + assert_select '.column_data .percents', 1 + assert_select 'tr.metrics td p', /Average first response time/ + assert_select 'tr.metrics td p', /Average closing ticket time/ + assert_select 'tr.metrics td p', /Average count of responses to close/ + assert_select 'tr.metrics td p', /Total replies/ + ensure + HelpdeskDataCollectorFirstResponse.any_instance.unstub(:issues) + end + + def test_show_productivity_report_with_no_data + HelpdeskDataCollectorFirstResponse.any_instance.stubs(:issues).returns(Issue.where(:id => 0)) + @request.session[:user_id] = 1 + get :show, :project_id => 'ecookbook', :report => 'first_response_time', + :set_filter => '1', + :f => ['message_date', ''], + :op => { 'message_date' => 'lm' } + assert_response :success + assert_select '#content h2', /First response time/ + assert_select 'p.nodata', /No data to display/ + ensure + HelpdeskDataCollectorFirstResponse.any_instance.unstub(:issues) + end + + def test_show_busiest_time_of_day_report + @request.session[:user_id] = 1 + get :show, :project_id => 'ecookbook', :report => 'busiest_time_of_day', + :set_filter => '1', + :f => ['message_date', ''], + :op => { 'message_date' => 'y' } + assert_response :success + assert_select '#content h2', /Busiest time of day/ + assert_select '.chart_table .header .column_data', 12 + assert_select '.column_data .percents', 2 + assert_select 'tr.metrics td p', /New tickets/ + assert_select 'tr.metrics td p', /New contacts/ + assert_select 'tr.metrics td p', /Total incoming/ + end + + def test_show_busiest_time_of_day_report_with_no_data + HelpdeskDataCollectorBusiestTime.any_instance.stubs(:issues_count).returns(0) + @request.session[:user_id] = 1 + get :show, :project_id => 'ecookbook', :report => 'busiest_time_of_day', + :set_filter => '1', + :f => ['message_date', ''], + :op => { 'message_date' => 't' } + assert_response :success + assert_select '#content h2', /Busiest time of day/ + assert_select 'p.nodata', /No data to display/ + ensure + HelpdeskDataCollectorBusiestTime.any_instance.unstub(:issues_count) + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_tickets_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_tickets_controller_test.rb new file mode 100644 index 0000000..769ad8d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_tickets_controller_test.rb @@ -0,0 +1,85 @@ +require File.expand_path('../../test_helper', __FILE__) + +class HelpdeskTicketsControllerTest < ActionController::TestCase + + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + end + + def test_should_get_edit + @request.session[:user_id] = 1 + xhr :get, :edit, :issue_id => 1, :id => 1 + assert_response 200 + end + + def test_should_create_ticket + @request.session[:user_id] = 1 + put :update, + :helpdesk_ticket => {:contact_id => 1, + :source => "0", + :ticket_date => "2013-01-01"}, + :time => {:hour => 21 , :minute => 12}, + :issue_id => 1, + :id => 1 + assert_redirected_to :controller => 'issues', :action => 'show', :id => '1' + assert_not_nil HelpdeskTicket.find_by_from_address(Contact.find(1).primary_email) + end + + def test_should_destroy + @request.session[:user_id] = 1 + delete :destroy, :id => 3 + assert_response :redirect + assert_nil HelpdeskTicket.find_by_id(3) + end + + def test_should_update_cutomer_profile + @request.session[:user_id] = 1 + put :update, + :helpdesk_ticket => {:contact_id => 1, + :source => "2", + :ticket_date => "2013-01-01"}, + :issue_id => 12, + :id => 1 + assert_redirected_to :controller => 'issues', :action => 'show', :id => '12' + assert_equal Contact.find(1).primary_email, HelpdeskTicket.last.from_address + assert_equal 12, HelpdeskTicket.last.issue_id + assert_equal 2, HelpdeskTicket.last.source + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_votes_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_votes_controller_test.rb new file mode 100644 index 0000000..0327e99 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/helpdesk_votes_controller_test.rb @@ -0,0 +1,131 @@ +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class HelpdeskVotesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + FIXTURES_PATH = Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/helpdesk_mailer' + + def setup + RedmineHelpdesk::TestCase.prepare + User.current = nil + RedmineHelpdesk.settings["helpdesk_vote_accept"] = 1 + end + + def test_should_open_on_correct_token + get :show, :id => 1, :hash => HelpdeskTicket.find(1).token + assert_response :success + assert_match 'Please rate our work', @response.body + end + + def test_should_show_404_with_incorrect_token + get :show, :id => 1, :hash => '111111' + assert_response 404 + end + + def test_should_hide_vote_comment_if_comments_off + RedmineHelpdesk.settings["helpdesk_vote_comment_accept"] = 0 + get :show, :id => 1, :hash => HelpdeskTicket.find(1).token + assert_response :success + assert_match 'Please rate our work', @response.body + assert_not_match /Leave a comment/, @response.body if self.respond_to?(:assert_not_match) + end + + def test_should_show_vote_comment_if_comments_off + RedmineHelpdesk.settings["helpdesk_vote_comment_accept"] = 1 + get :show, :id => 1, :hash => HelpdeskTicket.find(1).token + assert_response :success + assert_match 'Please rate our work', @response.body + assert_match 'Leave a comment', @response.body + end + + def test_should_save_last_comment_from_ticket + post :vote, :id => 1, :hash => HelpdeskTicket.find(1).token, :vote => 2, :vote_comment => 'test test' + assert_response :success + assert_match 'Thank you for voting', @response.body + assert_equal(2, HelpdeskTicket.find(1).vote) + assert_equal('test test', HelpdeskTicket.find(1).vote_comment) + end + + def test_fast_vote_should_update_ticket_if_comments_off + RedmineHelpdesk.settings["helpdesk_vote_comment_accept"] = 0 + get :fast_vote, :id => 1, :vote => 1, :hash => HelpdeskTicket.find(1).token + assert_response :success + assert_match 'Thank you for voting', @response.body + assert_equal(1, HelpdeskTicket.find(1).vote) + end + + def test_fast_vote_should_open_vote_page_if_comments_on + RedmineHelpdesk.settings["helpdesk_vote_comment_accept"] = 1 + get :fast_vote, :id => 1, :vote => 1, :hash => HelpdeskTicket.find(1).token + assert_response :success + assert_match 'Please rate our work', @response.body + if Redmine::VERSION.to_s >= "3.0" + assert_match 'id="vote_1" value="1" checked="checked"', @response.body + else + assert_match 'input checked="checked" id="vote_1"', @response.body + end + + end + + def test_should_save_votes_in_logs + RedmineHelpdesk.settings[:helpdesk_vote_save_log] = 1 + post :vote, :id => 1, :hash => HelpdeskTicket.find(1).token, :vote => 1, :vote_comment => 'Test test test' + assert_response :success + assert_match 'Thank you for voting', @response.body + assert_equal(1, HelpdeskTicket.find(1).vote) + assert_equal(HelpdeskTicket.find(1).issue, Journal.last.journalized) + assert_equal(1, Journal.last.details.where(:value => '1').count) + assert_equal(1, Journal.last.details.where(:value => 'Test test test').count) + end + + def test_vote_journal_save_user_if_he_present + RedmineHelpdesk.settings[:helpdesk_vote_save_log] = 1 + @request.session[:user_id] = 2 + post :vote, :id => 1, :hash => HelpdeskTicket.find(1).token, :vote => 0 + assert_response :success + assert_equal(User.find(2), Journal.last.user) + end + + def test_if_user_not_present_vote_anonymous + RedmineHelpdesk.settings[:helpdesk_vote_save_log] = 1 + post :vote, :id => 1, :hash => HelpdeskTicket.find(1).token, :vote => 0 + assert_response :success + assert_equal(User.where(:lastname => 'Anonymous').first, Journal.last.user) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/issues_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/issues_controller_test.rb new file mode 100644 index 0000000..4df4cae --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/issues_controller_test.rb @@ -0,0 +1,512 @@ +require File.expand_path('../../test_helper', __FILE__) + +# Re-raise errors caught by the controller. +# class HelpdeskMailerController; def rescue_action(e) raise e end; end + +class IssuesControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + include RedmineHelpdesk::TestHelper + + def setup + RedmineHelpdesk::TestCase.prepare + ActionMailer::Base.deliveries.clear + + @controller = IssuesController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + User.current = nil + end + + def test_show_issue + issue = Issue.find(1) + assert_not_nil issue.helpdesk_ticket + get :show, :id => 1 + assert_response :success + end + + def test_show_reply_to_for_issue_with_ticket + @request.session[:user_id] = 1 + issue = Issue.find(1) + assert_not_nil issue.helpdesk_ticket + get :show, :id => issue.id + assert_response :success + assert_equal response.body.match('#content .contextual:first a:first').size, 1 + assert_equal response.body.match('#content .contextual:last a:first').size, 1 + end + + def test_show_reply_to_for_issue_without_ticket + @request.session[:user_id] = 1 + issue = Issue.find(3) + assert_nil issue.helpdesk_ticket + get :show, :id => issue.id + assert_response :success + assert_nil response.body.match('#content .contextual:first a:first') + assert_nil response.body.match('#content .contextual:last a:first') + end + + def test_show_issue_with_uniq_cc_in_send_note + @request.session[:user_id] = 1 + issue = Issue.find(1) + cc_contact = Contact.find(1) + issue.helpdesk_ticket.update_attributes(:cc_address => "#{cc_contact.primary_email},test@email.com") + issue.contacts << cc_contact + with_helpdesk_settings('send_note_by_default' => '1') do + assert_not_nil issue.helpdesk_ticket + get :show, :id => 1 + assert_response :success + assert_select '#helpdesk_cc' do + assert_select '[value=?]', cc_contact.primary_email + assert_select '[value=?]', cc_contact.email_name, 0 + assert_select '[value=?]', 'test@email.com' + end + end + ensure + issue.contacts = [] + issue.helpdesk_ticket.update_attributes(:cc_address => '') + end + + def test_get_index_with_filters + ticket = HelpdeskTicket.find(1) + ticket.save + + @request.session[:user_id] = 1 + get :index, :set_filter =>"1", + :f => ["ticket_reaction_time", ""], + :op => {"ticket_reaction_time" => ">="}, + :v => {"ticket_reaction_time"=>["10"]}, + :c => ["customer", "ticket_source", "customer_company", "helpdesk_ticket", "ticket_reaction_time", "ticket_first_response_time", "ticket_resolve_time"], + :project_id => "ecookbook" + assert_response :success + end + + def test_get_vote_issues + ticket = HelpdeskTicket.find(1) + ticket.update_attributes(:vote => 1, :vote_comment => 'Good!') + + @request.session[:user_id] = 1 + get :index, :set_filter =>"1", + :f => ["vote", ""], + :op => { "vote" => "*" }, + :c => ["tracker", "vote", "vote_comment"], + :project_id => "ecookbook" + assert_response :success + assert_select "table.list.issues td.vote span", /Just ok/ + assert_select "table.list.issues td.vote_comment p", /Good/ + end + + def test_get_not_vote_issues + ticket = HelpdeskTicket.find(1) + ticket.update_attributes(:vote => 1, :vote_comment => 'Good!') + + @request.session[:user_id] = 1 + get :index, :set_filter =>"1", + :f => ["vote", ""], + :op => { "vote" => "!*" }, + :c => ["tracker", "vote", "vote_comment"], + :project_id => "ecookbook" + assert_response :success + assert_select "table.list.issues td.vote", "" + end + + def test_get_only_bad_voted_issues + ticket = HelpdeskTicket.find(1) + ticket.update_attributes(:vote => 1, :vote_comment => 'Good!') + ticket = HelpdeskTicket.find(2) + ticket.update_attributes(:vote => 0, :vote_comment => 'Bad!') + + @request.session[:user_id] = 1 + get :index, :set_filter =>"1", + :f => ["vote", ""], + :op => { "vote" => "=" }, + :v => { "vote" => ["0"] }, + :c => ["tracker", "vote", "vote_comment"], + :project_id => "ecookbook" + assert_response :success + assert_select "table.list.issues td.vote span", /Not good/ + assert_select "table.list.issues td.vote_comment p", /Bad/ + assert_select "table.list.issues td.vote span" do |votes| + votes.each do |vote| + assert_match /^((?!Just ok).)*/, vote.to_s + end + end + end + + def test_get_tickets_as_csv + ticket = HelpdeskTicket.find(1) + ticket.update_attributes(:vote => 1, :vote_comment => 'Good!') + ticket = HelpdeskTicket.find(2) + ticket.update_attributes(:vote => 0, :vote_comment => 'Bad!') + + @request.session[:user_id] = 1 + get :index, :set_filter =>"1", + :f => ["vote", ""], + :op => { "vote" => "=" }, + :v => { "vote"=>["1", "0"] }, + :c => ["tracker", "vote", "vote_comment", "last_message", "last_message_date", "customer", "ticket_source", "customer_company", "helpdesk_ticket", "ticket_reaction_time", "ticket_first_response_time", "ticket_resolve_time"], + :project_id => "ecookbook" + get :index, :format => 'csv' + assert_response :success + assert_not_nil assigns(:issues) + assert_equal "text/csv; header=present", @response.content_type + assert @response.body.starts_with?("#,") + end + + def test_should_send_note + user = User.find(1) + @request.session[:user_id] = 1 + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :issue => {:notes => notes} + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal "Hello, Ivan\r\n Bye, #{user.firstname}", j.notes + assert_equal 0, j.details.size + assert_equal User.find(1), j.user + + mail = last_ticket_mail + assert_mail_body_match "Hello, Ivan\r\n Bye, #{user.firstname}", mail + assert_equal Issue.find(1).customer.primary_email, mail.to.first + end + + def test_should_calculate_metrics + @request.session[:user_id] = 1 + + issue = Issue.find(1) + issue.journals.destroy_all + + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :issue => {:notes => 'Response to customer'} + + issue.reload + assert_not_nil issue.helpdesk_ticket.first_response_time + assert (issue.helpdesk_ticket.reaction_time - issue.helpdesk_ticket.first_response_time) < 100 + end + + def test_should_forward_note + user = User.find(1) + @request.session[:user_id] = 1 + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :journal_message => {:to_address => ["jsmith@somenet.foo"]}, + :issue => {:notes => notes} + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_match "Hello, Ivan\r\n Bye, #{user.firstname}", j.notes + assert_equal 0, j.details.size + assert_equal User.find(1), j.user + assert_equal Contact.find(4), j.journal_message.contact + + mail = last_ticket_mail + assert_mail_body_match "Hello, Ivan\r\n Bye, #{user.firstname}", mail + assert_equal "jsmith@somenet.foo", mail.to.first + end + + def test_should_send_note_with_bcc + issue = Issue.find(1) + contact = Contact.find(1) + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + @request.session[:user_id] = 1 + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :journal_message => {:bcc_address => ["mail1@mail.com", "mail2@mail.com"]}, + :issue => {:notes => notes} + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal "Hello, Ivan\r\n Bye, #{user.firstname}", j.notes + assert_equal 0, j.details.size + assert_equal User.find(1), j.user + + mail = last_ticket_mail + assert_mail_body_match "Hello, Ivan\r\n Bye, #{user.firstname}", mail + assert_equal Issue.find(1).customer.primary_email, mail.to.first + assert_equal ["mail1@mail.com", "mail2@mail.com"].sort, mail.bcc.sort + end + + def test_should_not_send_note_with_cc + issue = Issue.find(1) + contact = Contact.find(1) + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + @request.session[:user_id] = 1 + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :journal_message => {:cc_address => ["mail3@mail.com", "mail4@mail.com"]}, + :issue => {:notes => notes} + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal "Hello, Ivan\r\n Bye, #{user.firstname}", j.notes + assert_equal 0, j.details.size + assert_equal User.find(1), j.user + + mail = last_ticket_mail + assert_mail_body_match "Hello, Ivan\r\n Bye, #{user.firstname}", mail + assert_equal Issue.find(1).customer.primary_email, mail.to.first + assert_equal ["mail3@mail.com", "mail4@mail.com"].sort, mail.cc.sort + assert mail.bcc.empty?, "Bcc should be empty" + end + + def test_should_send_note_with_attachments + issue = Issue.find(1) + contact = Contact.find(1) + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + @request.session[:user_id] = user.id + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :issue => {:notes => notes}, + :attachments => {'1' => {'file' => helpdesk_uploaded_file('attachment.zip', 'application/octet-stream')}} + mail = last_ticket_mail + assert_not_nil mail.attachments + assert_equal 3855, mail.attachments.first.decoded.size + end + + def test_should_send_private_note_with_attachments + issue = Issue.find(1) + contact = Contact.find(1) + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + @request.session[:user_id] = user.id + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => { :is_send_mail => 1 }, + :issue => { :notes => notes, :private_notes => 1 }, + :attachments => { '1' => { 'file' => helpdesk_uploaded_file('attachment.zip', 'application/octet-stream') } } + assert_equal issue.reload.journals.last.private_notes, true + mail = last_ticket_mail + assert_not_nil mail.attachments + assert_equal 3855, mail.attachments.first.decoded.size + end + + def test_should_send_note_issue_from_anonymous + issue = Issue.find(1) + issue.author_id = 6 + contact = Contact.find(1) + user = User.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + + @request.session[:user_id] = 1 + notes = "Hello, %%NAME%%\r\n Bye, %%NOTE_AUTHOR.FIRST_NAME%%" + # anonymous user + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :issue => { :notes => notes } + assert_redirected_to :action => 'show', :id => '1' + j = Journal.order('id DESC').first + assert_equal "Hello, Ivan\r\n Bye, #{user.firstname}", j.notes + assert_equal 0, j.details.size + assert_equal User.find(1), j.user + + mail = last_ticket_mail + assert_mail_body_match "Hello, Ivan\r\n Bye, #{user.firstname}", mail + assert_equal Issue.find(1).customer.primary_email, mail.to.first + end + + def test_should_create_ticket + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + assert_difference 'HelpdeskTicket.count' do + post :create, + :issue => {:tracker_id => 3, :subject => "test", :status_id => 2, :priority_id => 5, + :helpdesk_ticket_attributes => {:contact_id => 1, + :source => "0", + :ticket_date => "2013-01-01 01:01:01 +0400"}}, + :project_id => project + end + assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id + assert_not_nil Issue.last.helpdesk_ticket + end + + def test_should_send_auto_answer + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + assert_difference 'HelpdeskTicket.count' do + post :create, + :issue => {:tracker_id => 3, :subject => "test", :status_id => 2, + :priority_id => 5, :description => "test description", + :helpdesk_ticket_attributes => {:contact_id => 1, + :source => "0", + :ticket_date => "2013-01-01 01:01:01 +0400"}}, + :helpdesk_send_as => HelpdeskTicket::SEND_AS_NOTIFICATION, + :project_id => 1 + end + mail = last_ticket_mail + assert_mail_body_match "We hereby confirm that we have received your message", mail + end + + def test_should_send_initial_message + with_helpdesk_settings("helpdesk_first_answer_subject" => 'Message for ticket id: #{%ticket.id%}') do + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + assert_difference 'HelpdeskTicket.count' do + post :create, + :issue => {:tracker_id => 3, :subject => "test", :status_id => 2, + :priority_id => 5, :description => "test initial description", + :helpdesk_ticket_attributes => {:contact_id => 1, + :source => "0", + :ticket_date => "2013-01-01 01:01:01 +0400"}}, + :helpdesk_send_as => HelpdeskTicket::SEND_AS_MESSAGE, + :project_id => 1 + end + mail = last_ticket_mail + assert_mail_body_match "test initial description", mail + assert_equal mail.subject, "Message for ticket id: \##{Issue.maximum(:id)}" + assert_equal HelpdeskTicket.order('id ASC').last.message_id, mail.message_id + assert_equal false, HelpdeskTicket.order('id ASC').last.is_incoming + end + end + + def test_should_not_create_ticket_for_invalid_issue + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + ActionMailer::Base.deliveries.clear + put :update, + :id => 1, + :helpdesk => {:is_send_mail => 1}, + :issue => { :notes => 'Test notes', :subject => '' } + assert_equal ActionMailer::Base.deliveries, [] + end + + def test_should_not_create_ticket_with_empty_customer + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + assert_no_difference 'HelpdeskTicket.count' do + post :create, + :issue => {:tracker_id => 3, :subject => "Test subject", :status_id => 2, :priority_id => 5, + :helpdesk_ticket_attributes => {:source => "0", + :contact_id => '', + :ticket_date => "2013-01-01 01:01:01 +0400"}}, + :project_id => project + if ActiveRecord::VERSION::MAJOR >= 4 + assert_select 'div#errorExplanation', /customer cannot be blank/i + else + assert_error_tag :content => /helpdesk_ticket.customer can't be blank/i + end + end + end + + def test_put_update_form + if ActiveRecord::VERSION::MAJOR < 4 + @request.session[:user_id] = 1 + issue = Issue.find(1) + ContactsSetting["helpdesk_tracker", issue.project.id] = 2 + xhr :put, :update_form, + :issue => {:tracker_id => 2, + :helpdesk_ticket_attributes => {:contact_id => 3, + :source => HelpdeskTicket::HELPDESK_PHONE_SOURCE}}, + :helpdesk_send_as => HelpdeskTicket::SEND_AS_MESSAGE, + :project_id => issue.project + assert_response :success + assert_equal 'text/javascript', response.content_type + assert_template 'update_form' + + + issue = assigns(:issue) + assert_kind_of Issue, issue + assert_equal 3, issue.helpdesk_ticket.customer.id + assert_equal HelpdeskTicket::HELPDESK_PHONE_SOURCE, issue.helpdesk_ticket.source + end + end + + def test_should_set_from_field_for_ticket + @request.session[:user_id] = 1 + project = Project.find('ecookbook') + contact = Contact.find(1) + assert_difference 'HelpdeskTicket.count' do + post :create, + :issue => {:tracker_id => 3, :subject => "test_for_field", :status_id => 2, :priority_id => 5, + :helpdesk_ticket_attributes => {:contact_id => contact.id, + :source => "0", + :ticket_date => "2013-01-01 01:01:01 +0400"}}, + :project_id => project + end + assert_not_nil Issue.last.helpdesk_ticket + assert_equal Issue.last.helpdesk_ticket.from_address, contact.primary_email + end + + private + + def last_ticket_mail + ActionMailer::Base.deliveries.detect{|m| m["X-Redmine-Ticket-ID"]} + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/mail_fetcher_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/mail_fetcher_controller_test.rb new file mode 100644 index 0000000..260e583 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/mail_fetcher_controller_test.rb @@ -0,0 +1,8 @@ +require File.expand_path('../../test_helper', __FILE__) + +class MailFetcherControllerTest < ActionController::TestCase + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/public_tickets_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/public_tickets_controller_test.rb new file mode 100644 index 0000000..7dd949d --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/public_tickets_controller_test.rb @@ -0,0 +1,115 @@ +require File.expand_path('../../test_helper', __FILE__) + +class PublicTicketsControllerTest < ActionController::TestCase + + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + RedmineHelpdesk.settings["helpdesk_public_tickets"] = 1 + @request.session[:user_id] = User.anonymous.id + # @response = ActionController::TestResponse.new + end + + def test_should_show_issue_with_correct_hash + get :show, :id => 1, :hash => HelpdeskTicket.order('id ASC').first.token + assert_response 200 + end + + def test_should_show_404_with_incorrect_hash + get :show, :id => 1, :hash => '123' + assert_response 404 + end + + def test_should_not_show_private_issues_and_notes + private_issue = Issue.find(5) + private_issue.is_private = true + private_issue.save + get :show, :id => 1, :hash => HelpdeskTicket.order('id ASC').first.token + assert_select 'div#sidebar .issue', {:html => /#{private_issue.subject}/, :count => 0} + end + + + def test_should_show_404_with_public_deny + RedmineHelpdesk.settings["helpdesk_public_tickets"] = 0 + get :show, :id => 1, :hash => HelpdeskTicket.order('id ASC').first.token + assert_response 404 + end + + def test_should_show_creator_email + get :show, :id => 1, :hash => HelpdeskTicket.order('id ASC').first.token + assert_select "p.author", /#{HelpdeskTicket.first.from_address}/ + end + + def test_should_add_comment + RedmineHelpdesk.settings["helpdesk_public_comments"] = 1 + ticket = HelpdeskTicket.find(1) + get :add_comment, :id => 1, :hash => ticket.token, :journal => {:notes => "Test public comment"} + assert_equal "Test public comment", Journal.order('id DESC').first.notes + assert_equal Journal.order('id DESC').first.created_on.to_date.to_s, JournalMessage.order('id DESC').first.message_date.to_date.to_s + get :show, :id => 1, :hash => ticket.token + assert_select ".journal .wiki p", /Test public comment/ + end + + def test_should_change_status + ticket = HelpdeskTicket.order('id ASC').first + reopen_status = IssueStatus.where('id != ?', ticket.issue.status).last + RedmineHelpdesk.settings["helpdesk_public_comments"] = 1 + ContactsSetting["helpdesk_reopen_status", ticket.issue.project_id] = reopen_status.id + get :add_comment, :id => 1, :hash => ticket.token, :journal => { :notes => 'Test public comment' } + assert_equal reopen_status, Journal.order('id DESC').last.issue.status + assert_equal reopen_status, ticket.issue.reload.status + end + + def test_should_not_add_comment_if_deny + RedmineHelpdesk.settings["helpdesk_public_comments"] = 0 + get :add_comment, :id => 1, :hash => HelpdeskTicket.first.token, :journal => {:notes => "Test comment"} + assert_response 404 + end + + def test_should_show_followups + @request.session[:user_id] = 1 + #first(:order => 'id ASC').issue.journals.create(:journalized_type => 'Issue', :notes => 'Followup1') + #puts HelpdeskTicket.order('id ASC').first.issue.journals + journal = HelpdeskTicket.order('id ASC').first.issue.journals.create(:journalized_type => 'Issue', :notes => 'Followup1') + journal_message = journal.create_journal_message(:contact => Contact.order('id ASC').first, :is_incoming => true, :from_address => Contact.order('id ASC').first.email, :message_date => Time.now) + assert journal_message.valid? + get :show, :id => 1, :hash => HelpdeskTicket.order('id ASC').first.token + assert_select ".journal h4", /#{Contact.order('id ASC').first.email}/ + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/functional/timelog_controller_test.rb b/plugins/redmine_contacts_helpdesk/test/functional/timelog_controller_test.rb new file mode 100644 index 0000000..968d50a --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/functional/timelog_controller_test.rb @@ -0,0 +1,52 @@ +require File.expand_path('../../test_helper', __FILE__) + +class TimelogControllerTest < ActionController::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + end + + def test_get_report_with_customer + @request.session[:user_id] = 1 + get :report, :columns => "month", :criteria => ["customer"], :project_id => "ecookbook" + assert_response :success + assert_select "table#time-report td", /Ivan/ + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/integration/api_test/helpdesk_test.rb b/plugins/redmine_contacts_helpdesk/test/integration/api_test/helpdesk_test.rb new file mode 100644 index 0000000..7830902 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/integration/api_test/helpdesk_test.rb @@ -0,0 +1,194 @@ +require File.expand_path('../../../test_helper', __FILE__) +# require File.dirname(__FILE__) + '/../../../../../test/test_helper' + +class Redmine::ApiTest::HelpdeskTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + include RedmineHelpdesk::TestHelper + + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + Setting.rest_api_enabled = '1' + RedmineHelpdesk::TestCase.prepare + end + + test "POST /helpdesk/email_note.xml" do + # Issue.find(1).contacts << Contact.find(1) + Redmine::ApiTest::Base.should_allow_api_authentication(:post, + '/helpdesk/email_note.xml', + {:message => {:issue_id => 1, :content => 'Test note', :status_id => 3}}, + {:success_code => :created}) if ActiveRecord::VERSION::MAJOR < 4 + + assert_difference('Journal.count') do + post '/helpdesk/email_note.xml', {:message => {:issue_id => 1, :content => 'Test note', :status_id => 3}}, credentials('admin') + end + assert_response :created + + journal = Journal.order('id DESC').first + assert_equal 'Test note', journal.notes + + assert_equal 'application/xml', @response.content_type + assert_select 'message', :child => {:tag => 'journal_id', :content => journal.id.to_s} + end + + def test_post_email_note_returns_not_found_error + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, + '/helpdesk/email_note.xml', + { :message => { :issue_id => 999, :content => 'Test' } }, + { :success_code => :created }) + end + + post '/helpdesk/email_note.xml', { :message => { :issue_id => 999, :content => 'Test' } }, credentials('admin') + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_match /Couldn't find Issue/, @response.body + end + + def test_post_email_note_returns_not_helpdesk_ticker_error + if ActiveRecord::VERSION::MAJOR < 4 + Redmine::ApiTest::Base.should_allow_api_authentication(:post, + '/helpdesk/email_note.xml', + { :message => { :issue_id => 3, :content => 'Test' } }, + { :success_code => :created }) + end + + post '/helpdesk/email_note.xml', { :message => { :issue_id => 3, :content => 'Test' } }, credentials('admin') + assert_response :unprocessable_entity + assert_equal 'application/xml', @response.content_type + assert_match /should be present and relate to customer/, @response.body + end + + def test_post_create_ticket + ActionMailer::Base.deliveries.clear + params = {:ticket => {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3, :description => 'Ticket body'}, + :contact => {:first_name => 'API Contact', :email => 'api@contact.mail'}}} + Redmine::ApiTest::Base.should_allow_api_authentication(:post, + '/helpdesk/create_ticket.xml', + params, + {:success_code => :created}) if ActiveRecord::VERSION::MAJOR < 4 + assert_difference('Issue.count') do + post '/helpdesk/create_ticket.xml', + params, credentials('admin') + end + issue = Issue.order('id DESC').first + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'Ticket body', issue.description + assert_equal 'API test', issue.subject + + contact = issue.customer + assert_equal 'API Contact', contact.first_name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_match /Issue \d+ created/, @response.body + assert_match /You have received this notification because you have/, ActionMailer::Base.deliveries.first.text_part.body.to_s + end + + def test_post_create_ticket_with_redirect + params = {:ticket => {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3, :description => 'Ticket body'}, + :contact => {:first_name => 'API Contact', :email => 'api@contact.mail'}}, + :redirect_on_success => 'http://ya.ru'} + + assert_difference('HelpdeskTicket.count') do + post '/helpdesk/create_ticket.xml', params, credentials('admin') + end + + assert_redirected_to 'http://ya.ru' + end + + def test_post_create_ticket_with_attachments + set_tmp_attachments_directory + # upload the file + assert_difference 'Attachment.count' do + post '/uploads.xml', 'test_create_with_upload', + {"CONTENT_TYPE" => 'application/octet-stream'}.merge(credentials('jsmith')) + assert_response :created + end + xml = Hash.from_xml(response.body) + token = xml['upload']['token'] + attachment = Attachment.order('id DESC').first + + + params = {:ticket => {:issue => {:project_id => 1, :subject => 'API test', + :tracker_id => 2, :status_id => 3, :description => 'Ticket body', + :uploads => [{:token => token, :filename => 'test.txt', + :content_type => 'text/plain'}]}, + :contact => {:first_name => 'API Contact', :email => 'api@contact.mail'}}} + + Redmine::ApiTest::Base.should_allow_api_authentication(:post, + '/helpdesk/create_ticket.xml', + params, + {:success_code => :created}) if ActiveRecord::VERSION::MAJOR < 4 + + assert_difference('Issue.count') do + post '/helpdesk/create_ticket.xml', + params, credentials('admin') + end + + issue = Issue.order('id DESC').first + assert_equal 1, issue.attachments.count + assert_equal attachment, issue.attachments.first + + attachment.reload + assert_equal 'test.txt', attachment.filename + assert_equal 'text/plain', attachment.content_type + assert_equal 'test_create_with_upload'.size, attachment.filesize + assert_equal 2, attachment.author_id + + + issue = Issue.order('id DESC').first + assert_equal 1, issue.project_id + assert_equal 2, issue.tracker_id + assert_equal 3, issue.status_id + assert_equal 'Ticket body', issue.description + assert_equal 'API test', issue.subject + + contact = issue.customer + assert_equal 'API Contact', contact.first_name + + assert_response :created + assert_equal 'application/xml', @response.content_type + assert_match /Issue \d+ created/, @response.body + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/integration/api_test/message.xml b/plugins/redmine_contacts_helpdesk/test/integration/api_test/message.xml new file mode 100644 index 0000000..2189426 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/integration/api_test/message.xml @@ -0,0 +1,5 @@ + + + 588 + Test message + \ No newline at end of file diff --git a/plugins/redmine_contacts_helpdesk/test/integration/common_views_test.rb b/plugins/redmine_contacts_helpdesk/test/integration/common_views_test.rb new file mode 100644 index 0000000..ebcf00f --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/integration/common_views_test.rb @@ -0,0 +1,85 @@ +require File.expand_path('../../test_helper', __FILE__) + +class RedmineHelpdesk::CommonViewsTest < ActiveRecord::VERSION::MAJOR >= 4 ? Redmine::ApiTest::Base : ActionController::IntegrationTest + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :canned_responses, + :helpdesk_tickets]) + + def setup + RedmineHelpdesk::TestCase.prepare + + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + @request.env['HTTP_REFERER'] = '/' + end + + test "View project settings" do + log_user("admin", "admin") + get "/projects/ecookbook/settings" + assert_response :success + end + + test "View helpdesk plugin settings" do + log_user("admin", "admin") + get "/settings/plugin/redmine_contacts_helpdesk" + assert_response :success + end + + test "View helpdesk plugin settings with hidden tab" do + log_user("admin", "admin") + get "/settings/plugin/redmine_contacts_helpdesk?hidden=true" + assert_response :success + end + + test "View issue" do + log_user("admin", "admin") + get "/issues/1" + assert_response :success + end + + test "View issues" do + log_user("admin", "admin") + get "/issues" + assert_response :success + end + + test "View project issues" do + log_user("admin", "admin") + get "/projects/ecookbook/issues" + assert_response :success + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/test_helper.rb b/plugins/redmine_contacts_helpdesk/test/test_helper.rb new file mode 100644 index 0000000..4b95839 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/test_helper.rb @@ -0,0 +1,75 @@ +require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') + +# Engines::Testing.set_fixture_path + +module RedmineHelpdesk + + module TestHelper + HELPDESK_FIXTURES_PATH = File.dirname(__FILE__) + '/fixtures/helpdesk_mailer' + + def submit_email(filename, options={}) + raw = IO.read(File.join(HELPDESK_FIXTURES_PATH, filename)) + MailHandler.receive(raw, options) + end + + def submit_helpdesk_email(filename, options={}) + raw = IO.read(File.join(HELPDESK_FIXTURES_PATH, filename)) + HelpdeskMailer.receive(raw, options) + end + + def helpdesk_uploaded_file(filename, mime) + fixture_file_upload("../../plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/#{filename}", mime, true) + end + + def last_email + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail + mail + end + + def with_helpdesk_settings(options, &block) + Setting.plugin_redmine_contacts_helpdesk.stubs(:[]).returns(nil) + options.each { |k, v| Setting.plugin_redmine_contacts_helpdesk.stubs(:[]).with(k).returns(v) } + yield + ensure + options.each { |k, v| Setting.plugin_redmine_contacts_helpdesk.unstub(:[]) } + end + end + + class TestCase + + def self.create_fixtures(fixtures_directory, table_names, class_names = {}) + if ActiveRecord::VERSION::MAJOR >= 4 + ActiveRecord::FixtureSet.create_fixtures(fixtures_directory, table_names, class_names = {}) + else + ActiveRecord::Fixtures.create_fixtures(fixtures_directory, table_names, class_names = {}) + end + end + + def self.prepare + Role.where(:id => [1, 2, 3, 4]).each do |r| + r.permissions << :view_contacts + r.save + end + Role.where(:id => [1, 2]).each do |r| + r.permissions << :edit_contacts + r.save + end + + Role.where(:id => [1, 2, 3]).each do |r| + r.permissions << :view_deals + r.save + end + Project.where(:id => [1, 2, 3, 4]).each do |project| + EnabledModule.create(:project => project, :name => 'contacts') + EnabledModule.create(:project => project, :name => 'deals') + EnabledModule.create(:project => project, :name => 'contacts_helpdesk') + end + end + + def assert_error_tag(options={}) + assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options)) + end + + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/canned_response_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/canned_response_test.rb new file mode 100644 index 0000000..4d4e332 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/canned_response_test.rb @@ -0,0 +1,9 @@ +require File.expand_path('../../test_helper', __FILE__) + +class CannedResponseTest < ActiveSupport::TestCase + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_mailer_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_mailer_test.rb new file mode 100644 index 0000000..3c5cde4 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_mailer_test.rb @@ -0,0 +1,1056 @@ +# encoding: utf-8 +require File.expand_path('../../test_helper', __FILE__) + +class HelpdeskMailerTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + include RedmineHelpdesk::TestHelper + + def setup + RedmineHelpdesk::TestCase.prepare + + ActionMailer::Base.deliveries.clear + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + RedmineHelpdesk.settings["helpdesk_vote_accept"] = 1 + end + + def test_should_not_change_perfom_delivery_state + ActionMailer::Base.perform_deliveries = false + HelpdeskMailer.with_activated_perform_deliveries do + true + end + assert_equal false, ActionMailer::Base.perform_deliveries + + ActionMailer::Base.perform_deliveries = true + HelpdeskMailer.with_activated_perform_deliveries do + true + end + assert_equal true, ActionMailer::Base.perform_deliveries + end + + def test_should_add_issue_and_contact + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_created_contact_tag", Project.find('ecookbook').id] = 'test,main' + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + + assert_not_nil issue.helpdesk_ticket.ticket_date + assert_equal "support@somenet.foo", issue.helpdesk_ticket.to_address + assert_equal "new_customer@somenet.foo", issue.helpdesk_ticket.from_address + assert_equal "New", contact.first_name + assert_equal "Customer-Name", contact.last_name + assert_equal "new_customer@somenet.foo", contact.company + assert contact.tag_list.include?('test') + assert contact.tag_list.include?('main') + assert_equal 'ecookbook', contact.project.identifier + assert_equal "new_customer@somenet.foo", contact.email + assert last_email.from.include?('test@email.from') + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + end + + def test_should_add_with_all_tracker + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook', :tracker_id => 'all'}) + assert_equal Issue, issue.class + issue.reload + assert_equal issue, Issue.order(:id).last + assert_equal issue.tracker, Project.find('ecookbook').trackers.first + assert_not_nil issue.helpdesk_ticket.ticket_date + end + + test "Should assign to duplicated contact" do + ActionMailer::Base.deliveries.clear + Contact.create(:first_name => "New", + :last_name => "Customer-Name", + :company => "Somenet.foo", + :email => "customer@somenet.foo", + :projects => [Project.find('ecookbook')]) + + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + contact = issue.customer + + assert_not_nil issue.helpdesk_ticket.ticket_date + assert_equal "support@somenet.foo", issue.helpdesk_ticket.to_address + assert_equal "new_customer@somenet.foo", issue.helpdesk_ticket.from_address + assert_equal "New", contact.first_name + assert_equal "Customer-Name", contact.last_name + assert_equal "new_customer@somenet.foo", contact.company + assert_equal 'ecookbook', contact.project.identifier + assert_match "new_customer@somenet.foo", contact.email + end + + test "Should add duplicated contact" do + ActionMailer::Base.deliveries.clear + duplicate = Contact.create(:first_name => "new_customer", + :last_name => "-", + :company => "Somenet.foo", + :email => "customer@somenet.foo", + :projects => [Project.find('ecookbook')]) + + issue = submit_helpdesk_email('ticket_without_name.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + contact = issue.customer + + assert_not_nil issue.helpdesk_ticket.ticket_date + assert_equal "support@somenet.foo", issue.helpdesk_ticket.to_address + assert_equal "new_customer@somenet.foo", issue.helpdesk_ticket.from_address + assert_equal 'new_customer', contact.first_name + assert_equal "-", contact.last_name + assert_equal "new_customer@somenet.foo", contact.company + assert_equal 'ecookbook', contact.project.identifier + assert_match "new_customer@somenet.foo", contact.email + end + + test "Should assign ticket to group" do + ActionMailer::Base.deliveries.clear + group = Group.find(11) + ContactsSetting["helpdesk_answer_from", Project.find('onlinestore').id] = 'test@email.from' + with_settings :issue_group_assignment => '1' do + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'onlinestore', :assigned_to_id => group.id}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal group, issue.assigned_to + end + end + + test "Should assign ticket to user" do + ActionMailer::Base.deliveries.clear + user = User.find(2) + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook', :assigned_to_id => user.id}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal user, issue.assigned_to + end + + test "Should set author from redmine user" do + ActionMailer::Base.deliveries.clear + user = User.find(2) + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + issue = submit_helpdesk_email('ticket_from_redmine_user.eml', :issue => {:project_id => 'ecookbook', :assigned_to_id => user.id}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal user, issue.author + end + + test "Should recieve ticket without to address" do + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_empty_to.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + + assert_not_nil issue.helpdesk_ticket.ticket_date + assert_equal "new_customer@somenet.foo", issue.helpdesk_ticket.from_address + assert_equal "", issue.helpdesk_ticket.to_address + assert_equal "New", contact.first_name + assert_equal "Customer-Name", contact.last_name + end + + test "Should not recieve ticket without from address" do + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_empty_from.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal false, issue + end + + test "Should add contact with bad name" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_contact_bad_name.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + assert_equal "New\"", contact.first_name + assert_equal "\"Customer\"", contact.last_name + assert_equal "new_customer@somenet.foo", contact.email + assert last_email.from.include?('test@email.from') + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + end + + test "Should add contact with unicode name" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_contact_unicode_name.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + assert_equal "Кирилл", contact.first_name + assert_equal "Безруков", contact.last_name + assert_equal "new_customer@somenet.foo", contact.email + assert last_email.from.include?('test@email.from') + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + end + + test "Should add new issue and contact with encoded name" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact_encode.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('Проверка') if RUBY_VERSION > "1.9" + contact = issue.customer + assert_equal "Кирилл", contact.first_name + assert_equal "Безруков", contact.last_name + assert_equal "aminov1982@gmail.com", contact.email + assert last_email.from.include?('test@email.from') + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + end + + test "Should add new issue from html only body" do + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_html_only.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + + assert_match "Header one\r\n", issue.description + assert_match " - one\r\n", issue.description + assert_match "one with line breaks,\r\nparagraph number", issue.description + end + + test "Should add new issue to contact without subject" do + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_no_subject.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert_equal '(no subject)', issue.subject + end + + def test_should_add_issue_with_rus_attachment + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_created_contact_tag", Project.find('ecookbook').id] = 'test,main' + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('ticket_with_rus_attachment.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + # assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + assert_equal "РуÑÑÐºÐ°Ñ Ñ‚ÐµÐ¼Ð° Apple Mail", issue.subject + assert_match 'Ð’ Ñтом пиÑьме аттач Ñ Ñ€ÑƒÑÑким названием', issue.description + attachment = issue.attachments.last + assert_not_nil attachment + assert_equal 'Ðттач номер один.rtf', attachment.filename + assert File.size?(attachment.diskfile) > 0 + assert File.size?(issue.attachments.last.diskfile) > 0 + end + + test "Should add issue and contact ru" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_created_contact_tag", Project.find('ecookbook').id] = 'test,main' + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact_ru.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + # assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + assert_equal "результаты турнира", issue.subject + assert_equal "Динара", contact.first_name + assert_equal "Кремчеева", contact.last_name + assert contact.tag_list.include?('test') + assert contact.tag_list.include?('main') + assert_equal 'ecookbook', contact.project.identifier + assert_equal "kr.dinara@mail.ru", contact.email + assert last_email.from.include?('test@email.from') + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + attachment = issue.attachments.last + assert_equal 'воÑходÑщие звезды 2012 6 тур.xml', attachment.filename + assert_not_nil attachment + assert File.size?(attachment.diskfile) > 0 + assert File.size?(issue.attachments.last.diskfile) > 0 + end + + test "Should add issue and contact ru 2" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_created_contact_tag", Project.find('ecookbook').id] = 'test,main' + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact_ru_2.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + # assert_equal issue, Issue.find_by_subject('New support issue from email') + contact = issue.customer + assert_equal "FW: результаты \"Кубка КремлÑ\"", issue.subject + assert_equal "Valeria", contact.first_name + assert_match 'Лера, извини', issue.description + attachment = issue.attachments.last + assert_not_nil attachment + assert_equal '131012.xml', attachment.filename + assert File.size?(attachment.diskfile) > 0 + assert File.size?(issue.attachments.last.diskfile) > 0 + end + + def test_should_add_issue_and_contact_ru_4 + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_new_contact_ru_4.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + assert_equal Issue, issue.class + assert_equal '(no subject)', issue.subject + attachment = issue.attachments.last + assert_equal 'Кубок ОÑени - 2012.xml', attachment.filename + assert File.size?(attachment.diskfile) > 0 + end + + def test_should_add_issue_and_contact_ru_5 + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_new_contact_ru_5.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + assert_equal Issue, issue.class + assert_equal 'Fwd: Турнир 14 октÑÐ±Ñ€Ñ 2012 Калининград', issue.subject + attachment = issue.attachments.last + assert_equal 'Kaliningrad 14102012.xml', attachment.filename + assert File.size?(attachment.diskfile) > 0 + end + + def test_should_add_issue_and_contact_in_japanese + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_japanese.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + assert_equal Issue, issue.class + if RUBY_VERSION > "1.9" + assert_equal 'ãŠå•ã„åˆã‚ã›', issue.subject + end + assert_match "ã„ã¤ã‚‚楽ã—ã使ã‚ã›", issue.description + end + + def test_should_create_from_quoted_body + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_quotes.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "МакÑим", contact.first_name + assert_equal "Скворцов", contact.last_name + assert_match "Короткое название", issue.description + end + + def test_should_create_from_koi8_r_body + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_in_koi8_r.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "Шипиев", contact.first_name + assert_equal "Роман", contact.last_name + assert_match "Речь идет про плагин", issue.description + end + + def test_should_create_from_win1251_body + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_in_win1251.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "RDSU", contact.first_name + if RUBY_VERSION > "1.9" + assert_match /(no subject|Unprocessable subject)/, issue.subject + else + assert_match "??????", issue.subject + end + assert_match "Танцевальный ринг", issue.description + end + + def test_should_create_with_encoded_attachment + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_encoded_attachment.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "Alexander", contact.first_name + assert_match "asdf", issue.subject + assert_match "This ticket is for testing purposes.", issue.description + if RUBY_VERSION > "1.9" + assert_match "Mller.png", issue.attachments.last.filename + else + assert_match "iso-8859-1_M", issue.attachments.last.filename + end + end + + def test_should_create_from_bq_encoded_body + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_bq_encoding.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "Плетнёв", contact.first_name + assert_equal "ÐлекÑей", contact.last_name + assert_match "Ответ на пиÑьмо", issue.description + end + + def test_should_create_with_encoding_iso_8859_15 + return true unless 'str'.respond_to?(:force_encoding) + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_in_iso_8859_15.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + contact = Contact.last + assert_equal Issue, issue.class + assert_equal "Joachim", contact.first_name + assert_equal "Höhl", contact.last_name + assert_match "Mit freundlichen Grüßen", issue.description + end + + def test_do_not_add_same_attachment + + Attachment.create(:container => Issue.find(5), + :file => helpdesk_uploaded_file("attachment.zip", "text/plain"), + :author => User.find(1)) + + + ActionMailer::Base.deliveries.clear + issue = Issue.find(5) + journal = submit_helpdesk_email('reply_with_duplicated_attachment.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, journal.class + + journal.reload + issue = journal.issue + + assert_equal 1, issue.attachments.count + assert_equal 'attachment.zip', issue.attachments.last.filename + assert_match 'два одинаковых вложениÑ', journal.notes + end + + test "Should add issue and contact with French symbols" do + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_french.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + assert_equal Issue, issue.class + assert_equal "email test ok'ok", issue.subject + attachment = issue.attachments.last + assert_equal 'image001.jpg', attachment.filename + assert_match 'the bug isÂ’here', issue.description + assert File.size?(attachment.diskfile) > 0 + end + + def test_ticket_with_multiline_subject + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('ticket_with_multiline_subject.eml', :issue => {:project_id => 'ecookbook'}) + issue.reload + assert_equal Issue, issue.class + assert_equal "Проверка (не открывайте Ñто пиÑьмо)", issue.subject + end + + test "Should not add contact" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_is_not_create_contacts", Project.find('ecookbook').id] = '1' + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal false, issue + ContactsSetting["helpdesk_is_not_create_contacts", Project.find('ecookbook').id] = '0' + end + + test "Should not add contact from blacklist" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_blacklist", Project.find('ecookbook').id] = "new_customer@somenet.foo" + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal false, issue + ContactsSetting["helpdesk_blacklist", Project.find('ecookbook').id] = "" + end + + test "Should not add contact from blacklist by regexp" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_blacklist", Project.find('ecookbook').id] = "new_.*\.foo" + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal false, issue + ContactsSetting["helpdesk_blacklist", Project.find('ecookbook').id] = "" + end + + test "Should add issue to contact" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_to_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue to Ivan') + contact = issue.customer + assert_equal "Ivan", contact.first_name + assert last_email.to.include?(contact.emails.first) + end + + test "Should add project to existing contact" do + ActionMailer::Base.deliveries.clear + onlinestore_project = Project.find_by_identifier('onlinestore') + ContactsSetting["helpdesk_answer_from", onlinestore_project.id] = 'test@email.from' + ContactsSetting[:helpdesk_add_contact_to_project, onlinestore_project.id] = "1" + issue = submit_helpdesk_email('new_issue_to_contact.eml', :issue => {:project_id => 'onlinestore'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + + contact = issue.customer + assert contact.projects.include?(onlinestore_project) + end + + test "Should attach mail to issue" do + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal "message.eml", issue.helpdesk_ticket.message_file.filename + assert issue.helpdesk_ticket.message_file.filesize > 0 + end + + def test_should_add_issue_to_contact_with_params + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_answer_from", Project.find('ecookbook').id] = 'test@email.from' + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_to_contact.eml', + :issue => {:project_id => 'ecookbook', + :priority => 'Urgent', + :status => 'Resolved', + :tracker => 'Support request', + :due_date => Date.today + 20, + :assigned_to => 'jsmith'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue to Ivan') + assert_equal 'Urgent', issue.priority.name + # assert_equal 'Assigned', issue.status.name + assert_equal 'Support request', issue.tracker.name + assert_equal 'jsmith', issue.assigned_to.login + assert_equal Date.today + 20, issue.due_date + contact = issue.customer + assert_equal "Ivan", contact.first_name + assert last_email.to.include?(contact.emails.first) + end + + def test_should_reply_to_issue_to_contact + ActionMailer::Base.deliveries.clear + + issue = Issue.find(5) + contact = Contact.find(1) + + assert_not_equal 'Feedback', issue.status.name + + journal = submit_helpdesk_email('reply_from_contact.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, journal.class + assert !journal.new_record? + + journal.reload + issue.reload + contact = journal.issue.customer + + assert_not_nil journal.journal_message.message_date + assert_equal "support@somenet.foo", journal.journal_message.to_address + assert_equal "ivan@mail.com", journal.journal_message.from_address + assert_equal "bcc@somenet.foo", journal.journal_message.bcc_address + assert_equal "cc@somenet.foo", journal.journal_message.cc_address + assert_equal 1, journal.contact.id + assert_equal issue.customer, journal.contact + assert_equal issue, journal.issue + assert_equal 'subproject1', journal.issue.project.identifier + assert_equal "Ivan", contact.first_name + assert_equal 'Feedback', issue.status.name + end + + test "Should add cc to issue" do + ActionMailer::Base.deliveries.clear + ContactsSetting[:helpdesk_save_cc, Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('ticket_with_cc.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + + assert_equal "cc@somenet.foo,marat.aminov@somenet.foo,ivan@somenet.foo", issue.helpdesk_ticket.cc_address + assert_equal ["Cc -", "Марат Ðминов", "Ivanov Ivan"].sort, issue.contacts.map(&:name).sort + end + + def test_should_reply_to_issue_to_contact_with_attachment + ActionMailer::Base.deliveries.clear + + issue = Issue.find(5) + + assert_not_equal 'Feedback', issue.status.name + + journal = submit_helpdesk_email('reply_with_attachment.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, journal.class + assert !journal.new_record? + + journal.reload + issue.reload + contact = journal.issue.customer + + assert_equal 1, journal.contact.id + assert_equal issue.customer, journal.contact + assert_equal issue, journal.issue + assert_equal 'subproject1', journal.issue.project.identifier + assert_equal "Ivan", contact.first_name + assert_equal 'Feedback', issue.status.name + attachment = issue.attachments.find_by_filename("Paella.jpg") + assert_not_nil attachment + assert File.size?(attachment.diskfile) > 0 + assert File.size?(issue.attachments.last.diskfile) > 0 + end + + test "Should attach email to reply" do + ActionMailer::Base.deliveries.clear + + issue = Issue.find(5) + + assert_not_equal 'Feedback', issue.status.name + + journal = submit_helpdesk_email('reply_from_contact.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, journal.class + assert !journal.new_record? + + journal.reload + issue.reload + contact = journal.issue.customer + + assert_equal 1, journal.contact.id + assert_equal issue.customer, journal.contact + assert_equal issue, journal.issue + assert_equal "reply-#{DateTime.now.strftime('%d.%m.%y-%H.%M.%S')}.eml".truncate(20), journal.journal_message.message_file.filename.truncate(20) + assert File.size?(journal.journal_message.message_file.diskfile) > 0 + end + + test "Should deliver received request confirmation" do + issue = Issue.find(4) + contact = Contact.find(1) + assert HelpdeskMailer.auto_answer(contact, issue).deliver + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + end + + def test_should_ignore_auto_answer + ActionMailer::Base.deliveries.clear + + journal = submit_helpdesk_email('auto_answer.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal false, journal, "Should not accept X-Auto-Response-Suppress: oof" + + journal = submit_helpdesk_email('auto_answer_exchange.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal false, journal, "Should not accept X-Auto-Response-Suppress: all" + end + + # test "Should delete spaces" do + # ActionMailer::Base.deliveries.clear + # issue = submit_helpdesk_email('ticket_with_leading_spaces_to.eml', :issue => {:project_id => 'ecookbook'}) + # assert_equal Issue, issue.class + # assert !issue.new_record? + # issue.reload + # assert_equal "", issue.description + # assert_match /^[^ ]+.*/, issue.description + # end + + def test_should_deliver_response + issue = Issue.find(1) + contact = Contact.find(1) + issue.journals.last.build_journal_message + assert HelpdeskMailer.issue_response(contact, issue.journals.last).deliver + assert last_email.to.include?(contact.emails.first) + assert_equal "Re: #{issue.subject} [#{issue.tracker.name} ##{issue.id}]", last_email.subject + end + + def test_should_replace_macro + issue = Issue.find(1) + issue.assigned_to = User.find(3) + issue.estimated_hours = 12 + issue.done_ratio = 50 + issue.status = IssueStatus.where(:name => "Rejected").first + issue.due_date = Date.today + 20 + issue.save! + + attachment = Attachment.create!(:container => HelpdeskTicket.find(1), + :file => uploaded_file('new_issue_new_contact_ru_2.eml', 'message/rfc822'), + :author => User.find(1)) + + user_cf = UserCustomField.create(:name => 'Test custom field', :is_filter => true, :field_format => 'string') + journal_user = User.find(2) + journal_user.custom_field_values = {user_cf.id => "This is custom значение"} + journal_user.save + + contact = Contact.find(1) + journal = issue.journals.last + journal.notes = "Full name old: %%FULL_NAME%%\r\n" + + "Company old: %%COMPANY%%\r\n" + + "Last name old: %%LAST_NAME%%\r\n" + + "Middle name old: %%MIDDLE_NAME%%\r\n" + + "Date old: %%DATE%%\r\n" + + "Assignee old: %%ASSIGNEE%%\r\n" + + "Issue ID old: %%ISSUE_ID%%\r\n" + + "Issue tracker old: %%ISSUE_TRACKER%%\r\n" + + "Issue description old: %%QUOTED_ISSUE_DESCRIPTION%%\r\n" + + "Project old: %%PROJECT%%\r\n" + + "Subject old: %%SUBJECT%%\r\n" + + "Note author old: %%NOTE_AUTHOR%%\r\n" + + "Note author first name old: %%NOTE_AUTHOR.FIRST_NAME%%\r\n" + + "Note author last name old: %%NOTE_AUTHOR.LAST_NAME%%\r\n" + + "====================" + + "Full name new: {%contact.name%}\r\n" + + "Company new: {%contact.company%}\r\n" + + "Last name new: {%contact.last_name%}\r\n" + + "Middle name new: {%contact.middle_name%}\r\n" + + "Email: {%contact.email%}\r\n" + + "Date new: {%date%}\r\n" + + "Assignee new: {%ticket.assigned_to%}\r\n" + + "Issue ID new: {%ticket.id%}\r\n" + + "Issue tracker new: {%ticket.tracker%}\r\n" + + "Issue description new: {%ticket.quoted_description%}\r\n" + + "Project new: {%ticket.project%}\r\n" + + "Subject new: {%ticket.subject%}\r\n" + + "Note author new: {%response.author%}\r\n" + + "Note author first name new: {%response.author.first_name%}\r\n" + + "Note author last name new: {%response.author.last_name%}\r\n" + + "Issue closed on: {%ticket.closed_on%}\r\n" + + "Issue start date: {%ticket.start_date%}\r\n" + + "Issue due date: {%ticket.due_date%}\r\n" + + "Issue Status: {%ticket.status%}\r\n" + + "Issue Priority: {%ticket.priority%}\r\n" + + "Issue public url: {%ticket.public_url%}\r\n" + + "Issue Estimated hours: {%ticket.estimated_hours%}\r\n" + + "Issue Done ratio: {%ticket.done_ratio%}\r\n" + + "=================\r\n" + + "User custom field: {%response.author.custom_field: Test custom field%}\r\n"+ + "Issue vote url: {%ticket.voting%}\r\n"+ + "Issue good vote url: {%ticket.voting.good%}\r\n"+ + "Issue okay vote url: {%ticket.voting.okay%}\r\n"+ + "Issue bad vote url: {%ticket.voting.bad%}\r\n"+ + "{{send_file(#{attachment.id})}}" + + journal.save! + User.current = User.find(4) + + ContactsSetting["helpdesk_emails_header", Project.find('ecookbook').id] = "Hello old, %%NAME%%\r\nHello new, {%contact.first_name%}" + ContactsSetting["helpdesk_emails_footer", Project.find('ecookbook').id] = "Regards old, %%NOTE_AUTHOR.FIRST_NAME%%\r\nRegards new, {%response.author.first_name%}" + assert HelpdeskMailer.issue_response(contact, journal).deliver + mail_body = last_email.text_part.body.to_s + assert_match /Hello old, Ivan/, mail_body + assert_match /Full name old: Ivan Ivanov/, mail_body + assert_match /Company old: Domoway/, mail_body + assert_match /Last name old: Ivanov/, mail_body + assert_match /Middle name old: Ivanovich/, mail_body + assert_match /Assignee old: Dave Lopper/, mail_body + assert_match /Issue ID old: 1/, mail_body + assert_match /Issue tracker old: Bug/, mail_body + assert_match /Issue description old: > Unable to print recipes/, mail_body + assert_match /Project old: eCookbook/, mail_body + if Redmine::VERSION.to_s >= "3.0" + assert_match /Subject new: Cannot print recipes/, mail_body + assert_match /Subject new: Cannot print recipes/, mail_body + else + assert_match /Subject new: Can't print recipes/, mail_body + assert_match /Subject new: Can't print recipes/, mail_body + end + assert_match /Note author old: John Smith/, mail_body + assert_match /Note author first name old: John/, mail_body + assert_match /Note author last name old: Smith/, mail_body + assert_match /Regards old, John/, mail_body + + assert_match /Hello new, Ivan/, mail_body + assert_match /Full name new: Ivan Ivanov/, mail_body + assert_match /Company new: Domoway/, mail_body + assert_match /Last name new: Ivanov/, mail_body + assert_match /Middle name new: Ivanovich/, mail_body + assert_match /Email: ivan@mail.com/, mail_body + assert_match /Assignee new: Dave Lopper/, mail_body + assert_match /Issue ID new: 1/, mail_body + assert_match /Issue tracker new: Bug/, mail_body + assert_match /Issue description new: > Unable to print recipes/, mail_body + assert_match /Project new: eCookbook/, mail_body + assert_match /Note author new: John Smith/, mail_body + assert_match /Note author first name new: John/, mail_body + assert_match /Note author last name new: Smith/, mail_body + assert_match /Regards new, John/, mail_body + assert_match "Issue closed on: #{ApplicationHelper.format_date(issue.closed_on)}", mail_body if Redmine::VERSION.to_s > '2.3' + assert_match "Issue start date: ", mail_body + assert_match "Issue due date: #{ApplicationHelper.format_date(issue.due_date)}", mail_body + assert_match /Issue Status: Rejected/, mail_body + assert_match /Issue Priority: Low/, mail_body + + assert_match "Issue public url: http:\/\/mydomain.foo\/tickets\/1\/#{issue.helpdesk_ticket.token}", mail_body + assert_match /Issue Estimated hours: 12.0/, mail_body + assert_match /Issue Done ratio: 50/, mail_body + + assert_match /User custom field: This is custom значение/, mail_body + + assert_match "Issue vote url: http:\/\/mydomain.foo\/vote\/1\/#{issue.helpdesk_ticket.token}", mail_body + assert_match "Issue good vote url: http:\/\/mydomain.foo\/vote\/1\/2/#{issue.helpdesk_ticket.token}", mail_body + assert_match "Issue okay vote url: http:\/\/mydomain.foo\/vote\/1\/1/#{issue.helpdesk_ticket.token}", mail_body + assert_match "Issue bad vote url: http:\/\/mydomain.foo\/vote\/1\/0/#{issue.helpdesk_ticket.token}", mail_body + + assert_equal 1, last_email.attachments.count + assert_equal attachment.filename, last_email.attachments.first.filename + end + + def test_should_replace_from_macro + assert_equal '', HelpdeskMailer.apply_from_macro('{%response.author%} ', User.anonymous) + assert_equal '', HelpdeskMailer.apply_from_macro('{%response.author%} something text ', User.anonymous) + assert_equal '', HelpdeskMailer.apply_from_macro('{%response.author.first_name%} ', User.anonymous) + assert_equal 'Robert Hill ', HelpdeskMailer.apply_from_macro('{%response.author%} ', User.find(4)) + assert_equal 'Robert ', HelpdeskMailer.apply_from_macro('{%response.author.first_name%} ', User.find(4)) + assert_equal 'Robert Hill from RedmineUP ', HelpdeskMailer.apply_from_macro('{%response.author%} from RedmineUP ', User.find(4)) + assert_equal 'Robert something text ', HelpdeskMailer.apply_from_macro('{%response.author.first_name%} something text ', User.find(4)) + assert_equal 'test@email.from', HelpdeskMailer.apply_from_macro('test@email.from', User.find(4)) + end + + def test_should_send_from_changed_address + issue = Issue.find(1) + contact = Contact.find(1) + ContactsSetting["helpdesk_answer_from", issue.project.id] = "newfrom@mail.com" + + assert HelpdeskMailer.issue_response(contact, issue.journals.last, params = {}).deliver + assert last_email.to.include?(contact.emails.first) + assert last_email.subject.include?("[#{issue.tracker} ##{issue.id}]") + assert_equal "newfrom@mail.com", last_email.from.first.to_s + end + + def test_should_send_to_changed_address + issue = Issue.find(1) + contact = Contact.find(1) + + assert HelpdeskMailer.issue_response(contact, issue.journals.last, params = {:to_address => "to_address@mail.com"}).deliver + assert_equal "to_address@mail.com", last_email.to_addrs.first.to_s + end + + test "Should find issue by in_reply_to" do + issue = Issue.find(1) + journal = issue.journals.last + contact = Contact.find(1) + journal.journal_message = JournalMessage.new(:contact => contact, + :journal => issue.journals.last, + :from_address => contact.primary_email, + :message_date => Time.now, + :message_id => '123456789@mail.com') + journal.save! + response_journal = submit_helpdesk_email('ticket_with_in_reply_to.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, response_journal.class + assert_equal response_journal.issue, journal.issue + end + + test "Should add journal with empty status settings" do + issue = Issue.find(1) + journal = issue.journals.last + contact = Contact.find(1) + journal.journal_message = JournalMessage.new(:contact => contact, + :journal => issue.journals.last, + :from_address => contact.primary_email, + :message_date => Time.now, + :message_id => '123456789@mail.com') + journal.save! + response_journal = submit_helpdesk_email('ticket_with_in_reply_to.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => "") + assert_equal Journal, response_journal.class + assert_equal response_journal.issue, journal.issue + assert_equal issue.status_id, response_journal.issue.status_id + end + + test 'should correctly decode email subject' do + text_subject = 'incidencia caracteres comentarios' + iso_subject = '=?iso-8859-1?Q?M=C1S_MODIFICACIONES_NO_SOLICITADAS_EN_EL_DISE=D1O_DE_LA_F?==?iso-8859-1?Q?ACTURA?=' + koi8r_subject = '=?koi8-r?B?7sUgz9TQ0sHXzNHA1NPRIMbBy9PZIM7BIM7PzcXSINcg/sXSzs/Hz9LJySAr?==?koi8-r?Q?38220225702//_=F4=F4_125590?=' + koi8r_subject2 = '=?koi8-r?Q?=F2=D5=D3=D3=CB=C1=D1_=D4=C5=CD=C1_Apple_Mail?=' + mailer = HelpdeskMailer.send(:new) + + assert_equal mailer.send(:decode_subject, text_subject), 'incidencia caracteres comentarios' + assert_equal mailer.send(:decode_subject, iso_subject), 'MÃS MODIFICACIONES NO SOLICITADAS EN EL DISEÑO DE LA FACTURA' + assert_equal mailer.send(:decode_subject, koi8r_subject), 'Ðе отправлÑÑŽÑ‚ÑÑ Ñ„Ð°ÐºÑÑ‹ на номер в Черногории +38220225702// ТТ 125590' + assert_equal mailer.send(:decode_subject, koi8r_subject2), 'РуÑÑÐºÐ°Ñ Ñ‚ÐµÐ¼Ð° Apple Mail' + end + + def test_vote_text_in_mail_correct + issue = Issue.find(1) + contact = Contact.find(1) + journal = issue.journals.last + journal.details << JournalDetail.create( :property => 'attr', + :prop_key => 'vote', + :old_value => '1', + :value => '2') + journal.save! + if Redmine::VERSION.to_s >= '2.4' + assert Mailer.issue_edit(journal, [User.find(1)], [User.find(2)]).deliver + else + assert Mailer.issue_edit(journal).deliver + end + assert_match /changed from Just ok to Awesome/, last_email.text_part.body.to_s + end + + def test_should_override_params_from_allow_override + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook' }, + :allow_override => 'project, tracker') + assert_equal Issue, issue.class + issue.reload + assert_equal issue, Issue.order(:id).last + assert_equal issue.tracker, Tracker.where(:name => 'Support request').first + assert_equal issue.project, Project.where(:name => 'OnlineStore').first + assert_not_nil issue.helpdesk_ticket.ticket_date + end + + def test_hould_receive_letter_with_id_or_name_in_project_params + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_contact_unicode_name.eml', :issue => { :project_id => 'ecookbook' }) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.where(:subject => 'New support issue from email').last + + issue2 = submit_helpdesk_email('new_contact_unicode_name.eml', :issue => { :project_id => '1' }) + assert_equal Issue, issue2.class + assert !issue2.new_record? + issue2.reload + assert_equal issue2, Issue.where(:subject => 'New support issue from email').last + end + + def test_should_use_reply_to_field_if_its_present + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('reply_to_mail.eml', :issue => { :project_id => 'ecookbook' }) + assert_equal Issue, issue.class + issue.reload + assert_equal issue, Issue.order(:id).last + assert_equal 'foo@bar.com', issue.helpdesk_ticket.from_address + end + + def test_should_add_issue_and_contact_with_require_fields + ActionMailer::Base.deliveries.clear + IssueCustomField.create!(:name => 'Issue require field', :is_required => true, :is_for_all => true, :field_format => 'string') + issue = submit_helpdesk_email('new_issue_new_contact.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal issue, Issue.find_by_subject('New support issue from email') + assert_not_nil issue.helpdesk_ticket.ticket_date + assert_equal "support@somenet.foo", issue.helpdesk_ticket.to_address + assert_equal "new_customer@somenet.foo", issue.helpdesk_ticket.from_address + assert_equal "New", issue.customer.first_name + assert_equal "Customer-Name", issue.customer.last_name + end + + def test_should_reply_to_issue_to_contact_with_require_fields + ActionMailer::Base.deliveries.clear + IssueCustomField.create!(:name => 'Issue require field', :is_required => true, :is_for_all => true, :field_format => 'string') + issue = Issue.find(5) + contact = Contact.find(1) + journal = submit_helpdesk_email('reply_from_contact.eml', :issue => {:project_id => 'ecookbook'}, :reopen_status => 'Feedback') + assert_equal Journal, journal.class + assert !journal.new_record? + journal.reload + issue.reload + contact = journal.issue.customer + assert_equal "support@somenet.foo", journal.journal_message.to_address + assert_equal "ivan@mail.com", journal.journal_message.from_address + assert_equal 1, journal.contact.id + assert_equal issue.customer, journal.contact + assert_equal issue, journal.issue + end + + def test_receive_ticket_without_text_with_attachment + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_without_text_with_attachment.eml', :issue => { :project_id => 'onlinestore' }) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal 1, issue.attachments.count + assert_equal '', issue.description + end + + def test_should_decode_encoded_subject + ActionMailer::Base.deliveries.clear + issue = submit_helpdesk_email('new_issue_encoded_subject.eml', :issue => { :project_id => 'onlinestore' }) + assert_equal Issue, issue.class + assert !issue.new_record? + issue.reload + assert_equal '170203 Изменение курÑов валют в УС', issue.subject + end + + def test_should_ignore_blacklisted_attachment + ActionMailer::Base.deliveries.clear + with_settings :mail_handler_excluded_filenames => '*.png' do + issue = submit_helpdesk_email('ticket_with_encoded_attachment.eml', :issue => {:project_id => 'ecookbook'}) + assert_equal Issue, issue.class + issue.reload + assert_equal 0, issue.attachments.count + end if Redmine::VERSION.to_s >= '2.4' + end + + def test_should_replace_emoji + ActionMailer::Base.deliveries.clear + ContactsSetting["helpdesk_send_notification", Project.find('ecookbook').id] = 1 + issue = submit_helpdesk_email('emoji_message.eml', :issue => {:project_id => 'ecookbook', :tracker_id => 'all'}) + assert_equal Issue, issue.class + issue.reload + assert_equal issue, Issue.order(:id).last + assert_equal issue.subject, 'Emoji' + end + + private + + def uploaded_file(filename, mime) + fixture_file_upload("../../plugins/redmine_contacts_helpdesk/test/fixtures/helpdesk_mailer/#{filename}", mime, true) + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_ticket_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_ticket_test.rb new file mode 100644 index 0000000..5f0a071 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/helpdesk_ticket_test.rb @@ -0,0 +1,200 @@ +require File.expand_path('../../test_helper', __FILE__) +include RedmineHelpdesk::TestHelper + +class HelpdeskTicketTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + def setup + Setting.default_language = 'en' + RedmineHelpdesk::TestCase.prepare + + ActionMailer::Base.deliveries.clear + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + end + + def test_should_calculate_reaction_date_from_first_journal + helpdesk_ticket = HelpdeskTicket.find(1) + issue = helpdesk_ticket.issue + journal_message = issue.journals.order(:created_on).last.build_journal_message(:contact => helpdesk_ticket.customer, :to_address => helpdesk_ticket.customer.primary_email) + journal_message.save + helpdesk_ticket.reload + + helpdesk_ticket.calculate_metrics + assert_equal 2, issue.journals.count + assert_equal helpdesk_ticket.reaction_time, issue.journals.order(:created_on).first.created_on - helpdesk_ticket.ticket_date.utc + end + + def test_ticket_token + helpdesk_ticket = HelpdeskTicket.find(1) + first_user = User.find(1) + second_user = User.find(2) + second_user.pref['time_zone'] = 'Monterrey' + + User.current = first_user + first_token = helpdesk_ticket.token + User.current = second_user + second_token = helpdesk_ticket.token + + assert_equal first_token, second_token + end + + def test_should_change_default_destination_form_outgoing_email + helpdesk_ticket = HelpdeskTicket.find(1) + issue = helpdesk_ticket.issue + other_contact = Contact.find(2) + journal_message = issue.journals.order(:created_on).last.build_journal_message(:contact => other_contact, + :from_address => other_contact.primary_email, + :is_incoming => true, + :message_date => Time.now) + journal_message.save + helpdesk_ticket.reload + + assert_equal helpdesk_ticket.default_to_address, other_contact.primary_email + + journal_message = issue.journals.order(:created_on).last.build_journal_message(:contact => helpdesk_ticket.customer, + :from_address => helpdesk_ticket.customer.primary_email, + :is_incoming => true, + :message_date => Time.now) + journal_message.save + helpdesk_ticket.reload + + assert_equal helpdesk_ticket.default_to_address, helpdesk_ticket.customer.primary_email + end + + def test_create_assigned_ticket + user = User.find(2) + contact = Contact.find(1) + contact.assigned_to = user + contact.save + + with_helpdesk_settings("helpdesk_assign_contact_user" => 1) do + issue = submit_helpdesk_email('new_issue_to_contact.eml', :issue => { :project_id => 'onlinestore' }) + + assert_not_nil issue + assert_equal issue.is_private?, false + assert_equal issue.assigned_to, user + end + end + + def test_create_private_assigned_ticket + user = User.find(2) + contact = Contact.find(1) + contact.assigned_to = user + contact.save + + with_helpdesk_settings("helpdesk_assign_contact_user" => 1, "helpdesk_create_private_tickets" => 1) do + issue = submit_helpdesk_email('new_issue_to_contact.eml', :issue => { :project_id => 'onlinestore' }) + + assert_not_nil issue + assert_equal issue.reload.is_private?, true + assert_equal issue.reload.assigned_to, user + end + end + + def test_create_not_assigned_ticket_if_project_not_visible + user = User.find(9) + contact = Contact.find(2) + contact.assigned_to = user + contact.save + + with_helpdesk_settings("helpdesk_assign_contact_user" => 1, "helpdesk_create_private_tickets" => 0) do + issue = submit_helpdesk_email('new_issue_to_contact.eml', :issue => { :project_id => 'onlinestore' }) + + assert_not_nil issue + assert_equal issue.is_private?, false + assert_nil issue.assigned_to + end + end + + def test_autoclose + issue = Issue.find(1) + initial_status = issue.status + issue.status = IssueStatus.find(2) + issue.created_on = issue.created_on - 2.hours + issue.save! + issue.reload + status_to = IssueStatus.last + with_helpdesk_settings("helpdesk_autoclose_tickets_after" => 1, + "helpdesk_autoclose_from_status" => issue.status_id, + "helpdesk_autoclose_to_status" => status_to.id, + "helpdesk_autoclose_tickets_time_unit" => 'hour') do + HelpdeskTicket.autoclose(issue.project) + issue.reload + assert_equal status_to, issue.status + end + ensure + issue.update_attributes(:status => initial_status) + end + + def test_dont_autoclose_new_ticket + issue = Issue.find(1).copy + # change status + issue.status = IssueStatus.find(2) + issue.save! + status_to = IssueStatus.last + with_helpdesk_settings("helpdesk_autoclose_tickets_after" => 1, + "helpdesk_autoclose_from_status" => issue.status_id, + "helpdesk_autoclose_to_status" => status_to.id, + "helpdesk_autoclose_tickets_time_unit" => 'hour') do + HelpdeskTicket.autoclose(issue.project) + issue.reload + assert status_to != issue.status + end + end + + def test_autoclose_off + issue = Issue.find(1) + initial_status = issue.status + issue.status = IssueStatus.find(2) + issue.save + issue.reload + status_to = IssueStatus.last + with_helpdesk_settings("helpdesk_autoclose_tickets_after" => nil, + "helpdesk_autoclose_from_status" => issue.status_id, + "helpdesk_autoclose_to_status" => status_to.id, + "helpdesk_autoclose_tickets_time_unit" => 'hour') do + HelpdeskTicket.autoclose(issue.project) + issue.reload + assert status_to != issue.status + end + ensure + issue.update_attributes(:status => initial_status) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/issue_query_patch_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/issue_query_patch_test.rb new file mode 100644 index 0000000..9615364 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/issue_query_patch_test.rb @@ -0,0 +1,38 @@ +require File.expand_path('../../test_helper', __FILE__) + +class IssueQueryPatchTest < ActiveSupport::TestCase + fixtures :projects, :users, :members, :member_roles, :roles, + :groups_users, + :trackers, :projects_trackers, + :enabled_modules, + :issue_statuses, :issue_categories, :issue_relations, :workflows, + :enumerations, + :issues, :journals, :journal_details, + :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + ]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + + def test_issues_with_company_filter + # Equals + @query = IssueQuery.new(:name => '_', :filters => { 'customer_company' => {:operator => '=', :values => ['Domoway']}}) + assert_equal [1,2,5].sort, @query.issues.map(&:id).sort + # Contains + @query = IssueQuery.new(:name => '_', :filters => { 'customer_company' => {:operator => '~', :values => ['omowa']}}) + assert_equal [1,2,5].sort, @query.issues.map(&:id).sort + # Is not null + @query = IssueQuery.new(:name => '_', :filters => { 'customer_company' => {:operator => '*', :values => ['']}}) + assert_equal [1,2,5].sort, @query.issues.map(&:id).sort + + # Is null + Contact.find(3).update_attribute(:company, 'company_name') + @query = IssueQuery.new(:name => '_', :filters => { 'customer_company' => {:operator => '!*', :values => ['']}}) + assert (not @query.issues.any?) + end + +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/journal_message_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/journal_message_test.rb new file mode 100644 index 0000000..3b63a76 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/journal_message_test.rb @@ -0,0 +1,10 @@ +require File.expand_path('../../test_helper', __FILE__) + +class JournalMessageTest < ActiveSupport::TestCase + fixtures :journal_messages + + # Replace this with your real tests. + def test_truth + assert true + end +end diff --git a/plugins/redmine_contacts_helpdesk/test/unit/mail_handler_patch_test.rb b/plugins/redmine_contacts_helpdesk/test/unit/mail_handler_patch_test.rb new file mode 100644 index 0000000..6becd04 --- /dev/null +++ b/plugins/redmine_contacts_helpdesk/test/unit/mail_handler_patch_test.rb @@ -0,0 +1,177 @@ +require File.expand_path('../../test_helper', __FILE__) + +class MailHandlerPatchTest < ActiveSupport::TestCase + fixtures :projects, + :users, + :roles, + :members, + :member_roles, + :issues, + :issue_statuses, + :versions, + :trackers, + :projects_trackers, + :issue_categories, + :enabled_modules, + :enumerations, + :attachments, + :workflows, + :custom_fields, + :custom_values, + :custom_fields_projects, + :custom_fields_trackers, + :time_entries, + :journals, + :journal_details, + :queries + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts).directory + '/test/fixtures/', [:contacts, + :contacts_projects, + :contacts_issues, + :deals, + :notes, + :tags, + :taggings, + :queries]) + + RedmineHelpdesk::TestCase.create_fixtures(Redmine::Plugin.find(:redmine_contacts_helpdesk).directory + '/test/fixtures/', [:journal_messages, + :helpdesk_tickets]) + + fixtures :email_addresses if ActiveRecord::VERSION::MAJOR >= 4 + + include RedmineHelpdesk::TestHelper + + def setup + RedmineHelpdesk::TestCase.prepare + + ActionMailer::Base.deliveries.clear + Setting.host_name = 'mydomain.foo' + Setting.protocol = 'http' + Setting.plain_text_mail = '0' + + Setting.notified_events = Redmine::Notifiable.all.collect(&:name) + end + + def test_send_mail_to_contact + issue = Issue.find(5) + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + RedmineHelpdesk.settings["send_note_by_default"] = false + journal = submit_email('reply_from_mail.eml') + assert_instance_of Journal, journal + assert !journal.new_record? + assert last_email.to.include?(contact.emails.first) + assert !last_email.parts.first.body.to_s.blank? + journal.reload + assert_no_match /^@@sendmail@@\s*/, journal.notes + assert_match /This is a reply from mail/, journal.notes + end + + def test_send_mail_to_contact_by_default + issue = Issue.find(5) + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + RedmineHelpdesk.settings["send_note_by_default"] = true + journal = submit_email('reply_from_mail_by_default.eml') + assert_instance_of Journal, journal + assert !journal.new_record? + assert_equal issue.helpdesk_ticket.from_address, last_email.to.first.to_s + assert !last_email.parts.first.body.to_s.blank? + journal.reload + assert_match /This is a reply from mail by default/, journal.notes + end + + def test_should_assign_user_to_unassigned_issue + issue = Issue.find(5) + issue.assigned_to = nil + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + RedmineHelpdesk.settings["send_note_by_default"] = true + journal = submit_email('reply_from_mail_by_default.eml') + assert_instance_of Journal, journal + assert_equal journal.user, journal.issue.assigned_to + end + + def test_should_assign_new_status + issue = Issue.find(5) + issue.assigned_to = User.find(2) + issue.status_id = IssueStatus.last.id + ContactsSetting[:helpdesk_new_status, issue.project_id] = IssueStatus.first.id + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + RedmineHelpdesk.settings["send_note_by_default"] = true + journal = submit_email('reply_from_mail_by_default.eml') + assert_instance_of Journal, journal + journal.reload + assert_equal IssueStatus.first, journal.issue.status + end + + def test_should_not_send_mail_to_contact_by_default + issue = Issue.find(5) + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + RedmineHelpdesk.settings["send_note_by_default"] = false + journal = submit_email('reply_from_mail_by_default.eml') + assert_instance_of Journal, journal + assert_equal "", last_email.to.first.to_s + end + + def test_should_not_send_mail_to_contact_by_default_with_empty_body + issue = Issue.find(5) + contact = Contact.find(1) + issue.helpdesk_ticket = HelpdeskTicket.new(:customer => contact, + :issue => issue, + :from_address => contact.primary_email, + :ticket_date => Time.now) + issue.save! + assert_not_equal 'Closed', issue.status.name + RedmineHelpdesk.settings["send_note_by_default"] = true + Setting.mail_handler_body_delimiters = "---- This should be cutted ----" + journal = submit_email('reply_from_mail_with_keywords.eml', :allow_override => ['status']) + assert_instance_of Journal, journal + assert_nil ActionMailer::Base.deliveries.last + assert_nil journal.journal_message + end + + def test_should_receive_to_tagged_response_to_issue + ActionMailer::Base.deliveries.clear + issue = Issue.find(1) + journal = submit_email('reply_from_mail_with_tag.eml', :issue => { :project_id => 'ecookbook' }) + assert_equal Journal, journal.class + assert !journal.new_record? + journal.reload + issue.reload + assert_equal issue, journal.issue + end + + def test_should_receive_cc_tagged_response_to_issue + ActionMailer::Base.deliveries.clear + issue = Issue.find(2) + journal = submit_email('reply_from_mail_with_tag_in_cc.eml', :issue => { :project_id => 'ecookbook' }) + assert_equal Journal, journal.class + assert !journal.new_record? + journal.reload + issue.reload + assert_equal issue, journal.issue + end +end diff --git a/plugins/redmine_event_outbox/README.md b/plugins/redmine_event_outbox/README.md new file mode 100644 index 0000000..b6b5453 --- /dev/null +++ b/plugins/redmine_event_outbox/README.md @@ -0,0 +1,66 @@ +# Redmine Event Outbox + +Small Redmine 3.4-compatible plugin that records selected Redmine changes into a +local database outbox table for external workers. + +## Events Captured + +- `issue.created` +- `issue.updated` +- `journal.created` +- `contact.created` when `redmine_contacts` is installed +- `contact.updated` when `redmine_contacts` is installed +- `helpdesk_ticket.created` when `redmine_contacts_helpdesk` is installed +- `helpdesk_ticket.updated` when `redmine_contacts_helpdesk` is installed +- `journal_message.created` when `redmine_contacts_helpdesk` is installed +- `journal_message.updated` when `redmine_contacts_helpdesk` is installed + +The plugin does not publish to Redis, RabbitMQ, webhooks, or external search +services directly from Redmine request callbacks. It only writes local database +rows. + +## Install + +Copy this directory to the Redmine plugins directory: + +```sh +cp -a redmine_event_outbox /path/to/redmine/plugins/ +``` + +Run the plugin migration: + +```sh +cd /path/to/redmine +RAILS_ENV=production bundle exec rake redmine:plugins:migrate NAME=redmine_event_outbox +``` + +Restart Redmine. For Passenger: + +```sh +touch tmp/restart.txt +``` + +## Verify + +List the rake task: + +```sh +RAILS_ENV=production bundle exec rake -T redmine_event_outbox +``` + +Dump pending events: + +```sh +RAILS_ENV=production bundle exec rake redmine_event_outbox:dump LIMIT=20 +``` + +Create or update a low-risk test issue, then run the dump task again. You should +see JSON rows in `event_outbox_events`. + +## Notes + +- Payloads are JSON serialized into a `text` column for MySQL and Redmine 3.4 + compatibility. +- Outbox write failures are rescued and logged so normal Redmine saves are not + intentionally failed by this plugin. +- Consumers should treat events as at-least-once and idempotent. diff --git a/plugins/redmine_event_outbox/app/models/event_outbox_event.rb b/plugins/redmine_event_outbox/app/models/event_outbox_event.rb new file mode 100644 index 0000000..5f521e8 --- /dev/null +++ b/plugins/redmine_event_outbox/app/models/event_outbox_event.rb @@ -0,0 +1,15 @@ +class EventOutboxEvent < ActiveRecord::Base + validates :event_type, :source_type, :source_id, :occurred_at, :payload, :presence => true + + scope :pending, lambda { where(:processed_at => nil).order(:id) } + + def payload_data + ActiveSupport::JSON.decode(payload) + rescue + {} + end + + def payload_data=(data) + self.payload = ActiveSupport::JSON.encode(data) + end +end diff --git a/plugins/redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb b/plugins/redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb new file mode 100644 index 0000000..3645de5 --- /dev/null +++ b/plugins/redmine_event_outbox/db/migrate/001_create_event_outbox_events.rb @@ -0,0 +1,27 @@ +class CreateEventOutboxEvents < ActiveRecord::Migration + def change + create_table :event_outbox_events do |t| + t.string :event_type, :null => false + t.string :source_type, :null => false + t.integer :source_id, :null => false + t.integer :project_id + t.integer :issue_id + t.integer :journal_id + t.integer :user_id + t.datetime :occurred_at, :null => false + t.text :payload, :null => false + t.datetime :processed_at + t.integer :attempts, :null => false, :default => 0 + t.text :last_error + t.datetime :locked_at + t.string :locked_by + t.timestamps + end + + add_index :event_outbox_events, [:processed_at, :id] + add_index :event_outbox_events, :event_type + add_index :event_outbox_events, :issue_id + add_index :event_outbox_events, :project_id + add_index :event_outbox_events, :occurred_at + end +end diff --git a/plugins/redmine_event_outbox/init.rb b/plugins/redmine_event_outbox/init.rb new file mode 100644 index 0000000..93575fa --- /dev/null +++ b/plugins/redmine_event_outbox/init.rb @@ -0,0 +1,11 @@ +require 'redmine' + +Redmine::Plugin.register :redmine_event_outbox do + name 'Redmine Event Outbox' + author 'Internal' + description 'Records Redmine changes into a local outbox table for external workers.' + version '0.0.1' + requires_redmine :version_or_higher => '3.4' +end + +require_dependency 'redmine_event_outbox' diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox.rb new file mode 100644 index 0000000..41af0ad --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox.rb @@ -0,0 +1,36 @@ +require_dependency 'redmine_event_outbox/event' +require_dependency 'redmine_event_outbox/hooks/issues_hook' + +ActionDispatch::Callbacks.to_prepare do + require_dependency 'redmine_event_outbox/patches/journal_patch' + Journal.send(:include, RedmineEventOutbox::Patches::JournalPatch) unless Journal.included_modules.include?(RedmineEventOutbox::Patches::JournalPatch) + + if defined?(Contact) + require_dependency 'redmine_event_outbox/patches/contact_patch' + Contact.send(:include, RedmineEventOutbox::Patches::ContactPatch) unless Contact.included_modules.include?(RedmineEventOutbox::Patches::ContactPatch) + end + + # Optional local integration with the installed RedmineUP helpdesk fork. + # The outbox plugin stays loadable without helpdesk, but captures first-class + # helpdesk identity when the plugin is present. + helpdesk_installed = begin + Redmine::Plugin.installed?(:redmine_contacts_helpdesk) + rescue + false + end + + if helpdesk_installed + require_dependency 'helpdesk_ticket' + require_dependency 'journal_message' + + if defined?(HelpdeskTicket) + require_dependency 'redmine_event_outbox/patches/helpdesk_ticket_patch' + HelpdeskTicket.send(:include, RedmineEventOutbox::Patches::HelpdeskTicketPatch) unless HelpdeskTicket.included_modules.include?(RedmineEventOutbox::Patches::HelpdeskTicketPatch) + end + + if defined?(JournalMessage) + require_dependency 'redmine_event_outbox/patches/journal_message_patch' + JournalMessage.send(:include, RedmineEventOutbox::Patches::JournalMessagePatch) unless JournalMessage.included_modules.include?(RedmineEventOutbox::Patches::JournalMessagePatch) + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/event.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/event.rb new file mode 100644 index 0000000..4c1c7c4 --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/event.rb @@ -0,0 +1,268 @@ +module RedmineEventOutbox + module Event + module_function + + def record_issue_created(issue, actor) + record( + :event_type => 'issue.created', + :source => issue, + :project_id => issue.project_id, + :issue_id => issue.id, + :user_id => issue.author_id, + :payload => issue_payload(issue, actor, 'issue.created') + ) + end + + def record_issue_updated(issue, journal, actor) + record( + :event_type => 'issue.updated', + :source => issue, + :project_id => issue.project_id, + :issue_id => issue.id, + :journal_id => journal.try(:id), + :user_id => actor.try(:id), + :payload => issue_payload(issue, actor, 'issue.updated').merge( + :journal_id => journal.try(:id), + :changed_fields => journal_changed_fields(journal) + ) + ) + end + + def record_journal_created(journal, actor) + return unless journal && journal.journalized_type == 'Issue' + + issue = journal.issue || journal.journalized + return unless issue + + record( + :event_type => 'journal.created', + :source => journal, + :project_id => issue.project_id, + :issue_id => issue.id, + :journal_id => journal.id, + :user_id => journal.user_id, + :payload => journal_payload(journal, issue, actor) + ) + end + + def record_contact_created(contact, actor) + record_contact_event('contact.created', contact, actor) + end + + def record_contact_updated(contact, actor) + record_contact_event('contact.updated', contact, actor) + end + + def record_helpdesk_ticket_created(helpdesk_ticket, actor) + record_helpdesk_ticket_event('helpdesk_ticket.created', helpdesk_ticket, actor) + end + + def record_helpdesk_ticket_updated(helpdesk_ticket, actor) + record_helpdesk_ticket_event('helpdesk_ticket.updated', helpdesk_ticket, actor) + end + + def record_journal_message_created(journal_message, actor) + record_journal_message_event('journal_message.created', journal_message, actor) + end + + def record_journal_message_updated(journal_message, actor) + record_journal_message_event('journal_message.updated', journal_message, actor) + end + + def record_contact_event(event_type, contact, actor) + record( + :event_type => event_type, + :source => contact, + :project_id => primary_project_id(contact), + :user_id => actor.try(:id), + :payload => contact_payload(contact, actor, event_type) + ) + end + + def record_helpdesk_ticket_event(event_type, helpdesk_ticket, actor) + issue = helpdesk_ticket.issue + + record( + :event_type => event_type, + :source => helpdesk_ticket, + :project_id => issue.try(:project_id), + :issue_id => helpdesk_ticket.issue_id, + :user_id => actor.try(:id), + :payload => helpdesk_ticket_payload(helpdesk_ticket, issue, actor, event_type) + ) + end + + def record_journal_message_event(event_type, journal_message, actor) + journal = journal_message.journal + issue = journal.try(:issue) || journal.try(:journalized) + + record( + :event_type => event_type, + :source => journal_message, + :project_id => issue.try(:project_id), + :issue_id => issue.try(:id), + :journal_id => journal_message.journal_id, + :user_id => journal.try(:user_id) || actor.try(:id), + :payload => journal_message_payload(journal_message, journal, issue, actor, event_type) + ) + end + + def record(attributes) + source = attributes.fetch(:source) + payload = attributes.fetch(:payload) + + event = EventOutboxEvent.new( + :event_type => attributes.fetch(:event_type), + :source_type => source.class.name, + :source_id => source.id, + :project_id => attributes[:project_id], + :issue_id => attributes[:issue_id], + :journal_id => attributes[:journal_id], + :user_id => attributes[:user_id], + :occurred_at => Time.now + ) + event.payload_data = payload + event.save! + rescue => e + Rails.logger.error( + "RedmineEventOutbox: failed to record #{attributes[:event_type]} " \ + "for #{source.class.name}##{source.try(:id)}: #{e.class}: #{e.message}" + ) + nil + end + + def issue_payload(issue, actor, event_type) + { + :event_type => event_type, + :issue_id => issue.id, + :project_id => issue.project_id, + :tracker_id => issue.tracker_id, + :status_id => issue.status_id, + :priority_id => issue.priority_id, + :author_id => issue.author_id, + :author_name => principal_name(issue.author), + :assigned_to_id => issue.assigned_to_id, + :assigned_to_name => principal_name(issue.assigned_to), + :actor_id => actor.try(:id), + :actor_name => principal_name(actor), + :subject => issue.subject, + :created_on => iso8601(issue.created_on), + :updated_on => iso8601(issue.updated_on) + } + end + + def journal_payload(journal, issue, actor) + { + :event_type => 'journal.created', + :journal_id => journal.id, + :issue_id => issue.id, + :project_id => issue.project_id, + :user_id => journal.user_id, + :user_name => principal_name(journal.user), + :actor_id => actor.try(:id), + :actor_name => principal_name(actor), + :subject => issue.subject, + :private_notes => journal.private_notes?, + :has_notes => journal.notes.present?, + :changed_fields => journal_changed_fields(journal), + :created_on => iso8601(journal.created_on) + } + end + + def contact_payload(contact, actor, event_type) + { + :event_type => event_type, + :contact_id => contact.id, + :project_ids => contact_project_ids(contact), + :is_company => contact.is_company, + :name => contact_name(contact), + :company => contact.try(:company), + :author_id => contact.try(:author_id), + :author_name => principal_name(contact.try(:author)), + :assigned_to_id => contact.try(:assigned_to_id), + :assigned_to_name => principal_name(contact.try(:assigned_to)), + :actor_id => actor.try(:id), + :actor_name => principal_name(actor), + :created_on => iso8601(contact.try(:created_on)), + :updated_on => iso8601(contact.try(:updated_on)) + } + end + + def helpdesk_ticket_payload(helpdesk_ticket, issue, actor, event_type) + { + :event_type => event_type, + :helpdesk_ticket_id => helpdesk_ticket.id, + :issue_id => helpdesk_ticket.issue_id, + :project_id => issue.try(:project_id), + :contact_id => helpdesk_ticket.contact_id, + :message_id => helpdesk_ticket.message_id, + :is_incoming => helpdesk_ticket.is_incoming?, + :source => helpdesk_ticket.source, + :from_address => helpdesk_ticket.from_address, + :to_address => helpdesk_ticket.to_address, + :cc_address => helpdesk_ticket.cc_address, + :actor_id => actor.try(:id), + :actor_name => principal_name(actor), + :subject => issue.try(:subject), + :ticket_date => iso8601(helpdesk_ticket.try(:ticket_date)), + :updated_on => iso8601(helpdesk_ticket.try(:updated_on)) + } + end + + def journal_message_payload(journal_message, journal, issue, actor, event_type) + # Keep event rows useful for routing/index invalidation without copying + # email bodies, private notes, attachments, or BCC addresses into outbox. + { + :event_type => event_type, + :journal_message_id => journal_message.id, + :journal_id => journal_message.journal_id, + :issue_id => issue.try(:id), + :project_id => issue.try(:project_id), + :contact_id => journal_message.contact_id, + :message_id => journal_message.message_id, + :is_incoming => journal_message.is_incoming?, + :source => journal_message.source, + :from_address => journal_message.from_address, + :to_address => journal_message.to_address, + :cc_address => journal_message.cc_address, + :has_bcc_address => journal_message.bcc_address.present?, + :user_id => journal.try(:user_id), + :user_name => principal_name(journal.try(:user)), + :actor_id => actor.try(:id), + :actor_name => principal_name(actor), + :subject => issue.try(:subject), + :private_notes => journal.try(:private_notes?), + :has_notes => journal.try(:notes).present?, + :message_date => iso8601(journal_message.try(:message_date)) + } + end + + def journal_changed_fields(journal) + return [] unless journal && journal.respond_to?(:details) + journal.details.map(&:prop_key).compact.uniq + end + + def contact_project_ids(contact) + return [] unless contact.respond_to?(:projects) + contact.projects.map(&:id).compact + rescue + [] + end + + def primary_project_id(contact) + contact_project_ids(contact).first + end + + def contact_name(contact) + contact.try(:name).presence || [contact.try(:first_name), contact.try(:last_name)].compact.join(' ').presence + end + + def principal_name(principal) + principal.try(:name).presence if principal + end + + def iso8601(value) + value.try(:utc).try(:iso8601) + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/hooks/issues_hook.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/hooks/issues_hook.rb new file mode 100644 index 0000000..2ddf4cd --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/hooks/issues_hook.rb @@ -0,0 +1,24 @@ +module RedmineEventOutbox + module Hooks + class IssuesHook < Redmine::Hook::ViewListener + def controller_issues_new_after_save(context = {}) + return unless context[:issue] + + RedmineEventOutbox::Event.record_issue_created( + context[:issue], + User.current + ) + end + + def controller_issues_edit_after_save(context = {}) + return unless context[:issue] + + RedmineEventOutbox::Event.record_issue_updated( + context[:issue], + context[:journal], + User.current + ) + end + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/contact_patch.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/contact_patch.rb new file mode 100644 index 0000000..2cd774f --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/contact_patch.rb @@ -0,0 +1,22 @@ +module RedmineEventOutbox + module Patches + module ContactPatch + def self.included(base) + base.class_eval do + after_commit :record_event_outbox_contact_created, :on => :create + after_commit :record_event_outbox_contact_updated, :on => :update + end + end + + private + + def record_event_outbox_contact_created + RedmineEventOutbox::Event.record_contact_created(self, User.current) + end + + def record_event_outbox_contact_updated + RedmineEventOutbox::Event.record_contact_updated(self, User.current) + end + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/helpdesk_ticket_patch.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/helpdesk_ticket_patch.rb new file mode 100644 index 0000000..11528e8 --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/helpdesk_ticket_patch.rb @@ -0,0 +1,22 @@ +module RedmineEventOutbox + module Patches + module HelpdeskTicketPatch + def self.included(base) + base.class_eval do + # Local fork hook: helpdesk ticket rows carry the real customer/email + # identity for many tickets whose Redmine issue author is Anonymous. + after_commit :record_event_outbox_helpdesk_ticket_created, :on => :create + after_commit :record_event_outbox_helpdesk_ticket_updated, :on => :update + end + end + + def record_event_outbox_helpdesk_ticket_created + RedmineEventOutbox::Event.record_helpdesk_ticket_created(self, User.current) + end + + def record_event_outbox_helpdesk_ticket_updated + RedmineEventOutbox::Event.record_helpdesk_ticket_updated(self, User.current) + end + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_message_patch.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_message_patch.rb new file mode 100644 index 0000000..d6cf792 --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_message_patch.rb @@ -0,0 +1,22 @@ +module RedmineEventOutbox + module Patches + module JournalMessagePatch + def self.included(base) + base.class_eval do + # Local fork hook: JournalMessage is the per-email metadata layer for + # helpdesk conversations, so indexers should react to it directly. + after_commit :record_event_outbox_journal_message_created, :on => :create + after_commit :record_event_outbox_journal_message_updated, :on => :update + end + end + + def record_event_outbox_journal_message_created + RedmineEventOutbox::Event.record_journal_message_created(self, User.current) + end + + def record_event_outbox_journal_message_updated + RedmineEventOutbox::Event.record_journal_message_updated(self, User.current) + end + end + end +end diff --git a/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_patch.rb b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_patch.rb new file mode 100644 index 0000000..ca5e30e --- /dev/null +++ b/plugins/redmine_event_outbox/lib/redmine_event_outbox/patches/journal_patch.rb @@ -0,0 +1,17 @@ +module RedmineEventOutbox + module Patches + module JournalPatch + def self.included(base) + base.class_eval do + after_commit :record_event_outbox_journal_created, :on => :create + end + end + + private + + def record_event_outbox_journal_created + RedmineEventOutbox::Event.record_journal_created(self, User.current) + end + end + end +end diff --git a/plugins/redmine_event_outbox/lib/tasks/redmine_event_outbox.rake b/plugins/redmine_event_outbox/lib/tasks/redmine_event_outbox.rake new file mode 100644 index 0000000..60b2f4f --- /dev/null +++ b/plugins/redmine_event_outbox/lib/tasks/redmine_event_outbox.rake @@ -0,0 +1,22 @@ +namespace :redmine_event_outbox do + desc 'Print pending Redmine event outbox rows as JSON.' + task :dump => :environment do + limit = ENV['LIMIT'].to_i + limit = 100 if limit <= 0 + + EventOutboxEvent.pending.limit(limit).each do |event| + puts ActiveSupport::JSON.encode( + :id => event.id, + :event_type => event.event_type, + :source_type => event.source_type, + :source_id => event.source_id, + :project_id => event.project_id, + :issue_id => event.issue_id, + :journal_id => event.journal_id, + :user_id => event.user_id, + :occurred_at => event.occurred_at.try(:utc).try(:iso8601), + :payload => event.payload_data + ) + end + end +end diff --git a/redmine_contacts.py b/redmine_contacts.py new file mode 100755 index 0000000..8cc69b8 --- /dev/null +++ b/redmine_contacts.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python3 +"""Small CLI for searching and lightly updating Redmine CRM contacts.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import time +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from difflib import SequenceMatcher +from pathlib import Path +from typing import Any + + +DEFAULT_BASE_URL = "http://192.168.50.170" +DEFAULT_PROJECT = "customer-service" +DEFAULT_CACHE_DIR = Path(".cache/redmine_contacts") +SEARCH_FIELDS = ( + "first_name", + "middle_name", + "last_name", + "company", + "job_title", + "background", + "website", + "skype_name", + "tag_list", +) +UPDATE_FIELDS = { + "first_name", + "middle_name", + "last_name", + "company", + "website", + "skype_name", + "birthday", + "job_title", + "background", + "phone", + "email", + "tag_list", +} + + +class RedmineError(RuntimeError): + pass + + +@dataclass +class RedmineClient: + base_url: str + api_key: str + project: str + + def request( + self, + method: str, + path: str, + params: dict[str, Any] | None = None, + payload: dict[str, Any] | None = None, + ) -> dict[str, Any]: + url = self._url(path, params) + body = None + headers = { + "Accept": "application/json", + "X-Redmine-API-Key": self.api_key, + } + if payload is not None: + body = json.dumps(payload).encode("utf-8") + headers["Content-Type"] = "application/json" + + request = urllib.request.Request(url, data=body, headers=headers, method=method) + try: + with urllib.request.urlopen(request, timeout=30) as response: + raw = response.read() + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise RedmineError(f"HTTP {exc.code} from Redmine: {detail[:500]}") from exc + except urllib.error.URLError as exc: + raise RedmineError(f"Could not reach Redmine: {exc.reason}") from exc + + if not raw: + return {} + try: + return json.loads(raw.decode("utf-8")) + except json.JSONDecodeError as exc: + raise RedmineError(f"Redmine returned non-JSON response from {url}") from exc + + def fetch_contacts(self, limit: int = 100) -> list[dict[str, Any]]: + contacts: list[dict[str, Any]] = [] + offset = 0 + total = None + while total is None or offset < total: + data = self.request( + "GET", + f"/projects/{self.project}/contacts.json", + {"limit": limit, "offset": offset}, + ) + page = data.get("contacts", []) + contacts.extend(page) + total = int(data.get("total_count", len(contacts))) + received = len(page) + if received == 0: + break + offset += received + return contacts + + def update_contact(self, contact_id: int, fields: dict[str, Any]) -> None: + self.request("PUT", f"/contacts/{contact_id}.json", payload={"contact": fields}) + + def helpdesk_ticket_by_issue(self, issue_id: int) -> dict[str, Any]: + return self.request("GET", f"/helpdesk_search/issues/{issue_id}/ticket") + + def helpdesk_issues_by_contact(self, contact_id: int, limit: int) -> dict[str, Any]: + return self.request("GET", f"/helpdesk_search/contacts/{contact_id}/issues", {"limit": limit}) + + def helpdesk_messages_by_issue(self, issue_id: int, limit: int) -> dict[str, Any]: + return self.request("GET", f"/helpdesk_search/issues/{issue_id}/messages", {"limit": limit}) + + def helpdesk_timeline(self, contact_id: int, limit: int) -> dict[str, Any]: + return self.request("GET", f"/helpdesk_search/contacts/{contact_id}/timeline", {"limit": limit}) + + def _url(self, path: str, params: dict[str, Any] | None = None) -> str: + base = self.base_url.rstrip("/") + url = f"{base}{path}" + if params: + url += "?" + urllib.parse.urlencode(params) + return url + + +def main() -> int: + parser = argparse.ArgumentParser(description="Search and update Redmine CRM contacts.") + parser.add_argument("--base-url", default=os.getenv("REDMINE_BASE_URL", DEFAULT_BASE_URL)) + parser.add_argument("--project", default=os.getenv("REDMINE_PROJECT", DEFAULT_PROJECT)) + parser.add_argument("--cache", type=Path, help="Override the cache file path.") + + subparsers = parser.add_subparsers(dest="command", required=True) + + fetch_parser = subparsers.add_parser("fetch", help="Fetch all visible contacts into the local cache.") + fetch_parser.add_argument("--limit", type=int, default=100) + + search_parser = subparsers.add_parser("search", help="Search cached contacts.") + search_parser.add_argument("query") + search_parser.add_argument("--refresh", action="store_true", help="Refresh cache before searching.") + search_parser.add_argument( + "--offline-cache", + action="store_true", + help="Search only the local cache without requiring REDMINE_API_KEY.", + ) + search_parser.add_argument("--limit", type=int, default=20) + search_parser.add_argument("--min-score", type=float, default=0.68) + + update_parser = subparsers.add_parser("update", help="Update safe fields on one contact.") + update_parser.add_argument("contact_id", type=int) + update_parser.add_argument( + "--set", + action="append", + default=[], + metavar="FIELD=VALUE", + help="Set a contact field. Repeat for multiple fields.", + ) + update_parser.add_argument("--apply", action="store_true", help="Actually send the update.") + + ticket_parser = subparsers.add_parser( + "helpdesk-ticket", + help="Fetch helpdesk ticket metadata for an issue.", + ) + ticket_parser.add_argument("issue_id", type=int) + + contact_issues_parser = subparsers.add_parser( + "helpdesk-issues", + help="List helpdesk issues for a contact.", + ) + contact_issues_parser.add_argument("contact_id", type=int) + contact_issues_parser.add_argument("--limit", type=int, default=100) + + issue_messages_parser = subparsers.add_parser( + "helpdesk-messages", + help="List helpdesk journal message metadata for an issue.", + ) + issue_messages_parser.add_argument("issue_id", type=int) + issue_messages_parser.add_argument("--limit", type=int, default=100) + + timeline_parser = subparsers.add_parser( + "helpdesk-timeline", + help="Show a contact's helpdesk ticket/message timeline.", + ) + timeline_parser.add_argument("contact_id", type=int) + timeline_parser.add_argument("--limit", type=int, default=100) + + args = parser.parse_args() + + try: + if args.command == "fetch": + client = client_from_args(args) + contacts = client.fetch_contacts(limit=args.limit) + cache_path = resolve_cache_path(args) + write_cache(cache_path, args, contacts) + print(f"Cached {len(contacts)} contacts in {cache_path}") + return 0 + + if args.command == "search": + contacts = load_or_refresh(args) + matches = search_contacts(contacts, args.query, args.min_score) + print_matches(matches[: args.limit]) + return 0 + + if args.command == "update": + fields = parse_update_fields(args.set) + if not fields: + raise RedmineError("No fields supplied. Use --set FIELD=VALUE.") + print(f"Contact {args.contact_id} update payload:") + print(json.dumps({"contact": fields}, indent=2, sort_keys=True)) + if not args.apply: + print("Dry run only. Re-run with --apply to write to Redmine.") + return 0 + client = client_from_args(args) + client.update_contact(args.contact_id, fields) + print("Update sent.") + return 0 + + if args.command == "helpdesk-ticket": + client = client_from_args(args) + print_json(client.helpdesk_ticket_by_issue(args.issue_id)) + return 0 + + if args.command == "helpdesk-issues": + client = client_from_args(args) + print_json(client.helpdesk_issues_by_contact(args.contact_id, args.limit)) + return 0 + + if args.command == "helpdesk-messages": + client = client_from_args(args) + print_json(client.helpdesk_messages_by_issue(args.issue_id, args.limit)) + return 0 + + if args.command == "helpdesk-timeline": + client = client_from_args(args) + print_json(client.helpdesk_timeline(args.contact_id, args.limit)) + return 0 + except RedmineError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + return 1 + + +def client_from_args(args: argparse.Namespace) -> RedmineClient: + api_key = os.getenv("REDMINE_API_KEY") + if not api_key: + raise RedmineError("Set REDMINE_API_KEY in the environment.") + return RedmineClient(base_url=args.base_url, api_key=api_key, project=args.project) + + +def load_or_refresh(args: argparse.Namespace) -> list[dict[str, Any]]: + cache_path = resolve_cache_path(args) + if args.refresh or not cache_path.exists(): + client = client_from_args(args) + contacts = client.fetch_contacts() + write_cache(cache_path, args, contacts) + return contacts + if not args.offline_cache: + require_api_key_for_cached_search() + with cache_path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + validate_cache_metadata(data, args, cache_path) + return data["contacts"] + + +def write_cache(cache_path: Path, args: argparse.Namespace, contacts: list[dict[str, Any]]) -> None: + cache_path.parent.mkdir(parents=True, exist_ok=True) + payload = { + "base_url": canonical_base_url(args.base_url), + "project": args.project, + "cached_at": int(time.time()), + "contacts": contacts, + } + with cache_path.open("w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + handle.write("\n") + + +def resolve_cache_path(args: argparse.Namespace) -> Path: + if args.cache: + return args.cache + parsed = urllib.parse.urlparse(args.base_url) + host = parsed.netloc or parsed.path + host_slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", host.strip("/")) + project_slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", args.project) + return DEFAULT_CACHE_DIR / f"{host_slug}_{project_slug}.json" + + +def canonical_base_url(base_url: str) -> str: + return base_url.rstrip("/") + + +def require_api_key_for_cached_search() -> None: + if not os.getenv("REDMINE_API_KEY"): + raise RedmineError( + "Set REDMINE_API_KEY, or pass --offline-cache to search only a local cache explicitly." + ) + + +def validate_cache_metadata(data: dict[str, Any], args: argparse.Namespace, cache_path: Path) -> None: + expected_base_url = canonical_base_url(args.base_url) + cached_base_url = data.get("base_url") + cached_project = data.get("project") + if cached_base_url is None or cached_project is None: + raise RedmineError( + f"{cache_path} is a legacy cache without base-url/project metadata. " + "Refresh it before searching." + ) + if cached_base_url != expected_base_url or cached_project != args.project: + raise RedmineError( + f"{cache_path} was created for {cached_base_url} project {cached_project}, " + f"not {expected_base_url} project {args.project}." + ) + + +def parse_update_fields(assignments: list[str]) -> dict[str, str]: + fields: dict[str, str] = {} + for assignment in assignments: + if "=" not in assignment: + raise RedmineError(f"Invalid --set value: {assignment!r}") + field, value = assignment.split("=", 1) + field = field.strip() + if field not in UPDATE_FIELDS: + allowed = ", ".join(sorted(UPDATE_FIELDS)) + raise RedmineError(f"Unsupported update field {field!r}. Allowed fields: {allowed}") + fields[field] = value + return fields + + +def search_contacts( + contacts: list[dict[str, Any]], + query: str, + min_score: float, +) -> list[tuple[float, dict[str, Any], str]]: + normalized_query = normalize(query) + query_digits = digits_only(query) + scored: list[tuple[float, dict[str, Any], str]] = [] + for contact in contacts: + haystacks = contact_haystacks(contact) + score, reason = score_contact(normalized_query, query_digits, haystacks) + if score >= min_score: + scored.append((score, contact, reason)) + return sorted(scored, key=lambda item: (-item[0], display_name(item[1]).lower(), item[1].get("id", 0))) + + +def score_contact( + query: str, + query_digits: str, + haystacks: list[tuple[str, str]], +) -> tuple[float, str]: + best_score = 0.0 + best_reason = "" + for label, value in haystacks: + normalized_value = normalize(value) + if not normalized_value: + continue + if query and query in normalized_value: + score = 1.0 + else: + score = SequenceMatcher(None, query, normalized_value).ratio() + for token in normalized_value.split(): + score = max(score, SequenceMatcher(None, query, token).ratio() * 0.92) + if score > best_score: + best_score = score + best_reason = label + + if query_digits: + for label, value in haystacks: + value_digits = digits_only(value) + if value_digits and query_digits in value_digits: + return 1.0, label + if value_digits: + score = SequenceMatcher(None, query_digits, value_digits).ratio() * 0.95 + if score > best_score: + best_score = score + best_reason = label + return best_score, best_reason + + +def contact_haystacks(contact: dict[str, Any]) -> list[tuple[str, str]]: + haystacks: list[tuple[str, str]] = [("name", display_name(contact))] + for field in SEARCH_FIELDS: + value = contact.get(field) + if value: + haystacks.append((field, flatten(value))) + for email in contact.get("emails", []) or []: + haystacks.append(("email", flatten(email))) + for phone in contact.get("phones", []) or []: + haystacks.append(("phone", flatten(phone))) + address = contact.get("address") or {} + for key, value in address.items(): + if value: + haystacks.append((f"address.{key}", flatten(value))) + custom_fields = contact.get("custom_fields") or [] + for field in custom_fields: + name = field.get("name", "custom_field") + value = field.get("value") + if value: + haystacks.append((f"custom.{name}", flatten(value))) + return haystacks + + +def display_name(contact: dict[str, Any]) -> str: + if contact.get("is_company"): + return str(contact.get("first_name") or contact.get("company") or "").strip() + parts = [contact.get("first_name"), contact.get("middle_name"), contact.get("last_name")] + name = " ".join(str(part).strip() for part in parts if part) + return name or str(contact.get("company") or "").strip() + + +def flatten(value: Any) -> str: + if isinstance(value, dict): + return " ".join(flatten(item) for item in value.values()) + if isinstance(value, list): + return " ".join(flatten(item) for item in value) + return str(value) + + +def normalize(value: str) -> str: + value = value.lower() + value = re.sub(r"[^a-z0-9@.+]+", " ", value) + return re.sub(r"\s+", " ", value).strip() + + +def digits_only(value: str) -> str: + return re.sub(r"\D+", "", value) + + +def print_matches(matches: list[tuple[float, dict[str, Any], str]]) -> None: + if not matches: + print("No contacts matched.") + return + for score, contact, reason in matches: + emails = ", ".join(flatten(item) for item in contact.get("emails", []) or []) + phones = ", ".join(flatten(item) for item in contact.get("phones", []) or []) + company = contact.get("company") or "" + line = f"{contact.get('id')}: {display_name(contact)}" + if company and company != display_name(contact): + line += f" | {company}" + if emails: + line += f" | {emails}" + if phones: + line += f" | {phones}" + line += f" | score={score:.2f} via {reason}" + print(line) + + +def print_json(data: dict[str, Any]) -> None: + # Helpdesk commands intentionally print raw JSON so later scripts/indexers + # can pipe the metadata without parsing a human-oriented table format. + print(json.dumps(data, indent=2, sort_keys=True)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/redmine_helpdesk_search.py b/redmine_helpdesk_search.py new file mode 100755 index 0000000..9127155 --- /dev/null +++ b/redmine_helpdesk_search.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +"""Local CLI for searching RedmineUP helpdesk tickets and email messages. + +This tool intentionally reads the helpdesk/contact tables directly through SSH +and MySQL. Helpdesk tickets often have Anonymous Redmine issue authors, while +the real customer identity lives in helpdesk_tickets, journal_messages, and +contacts. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import subprocess +import sys +import time +from dataclasses import dataclass +from difflib import SequenceMatcher +from pathlib import Path +from typing import Any, Iterable + + +DEFAULT_SSH_HOST = "reddev@192.168.50.170" +DEFAULT_SSH_KEY = Path("/tmp/reddev") +DEFAULT_REMOTE_REDMINE = "/usr/share/redmine" +DEFAULT_CACHE_DIR = Path(".cache/redmine_helpdesk") +DEFAULT_CACHE_FILE = DEFAULT_CACHE_DIR / "helpdesk_documents.jsonl" + + +class HelpdeskSearchError(RuntimeError): + pass + + +@dataclass(frozen=True) +class RemoteRedmine: + ssh_host: str + ssh_key: Path + remote_redmine: str + + def mysql_json_lines(self, sql: str) -> list[dict[str, Any]]: + """Run a read-only SQL statement remotely and parse one JSON object per row.""" + command = [ + "ssh", + "-i", + str(self.ssh_key), + "-o", + "IdentitiesOnly=yes", + self.ssh_host, + self._mysql_runner_command(), + ] + try: + result = subprocess.run( + command, + input=sql, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + except OSError as exc: + raise HelpdeskSearchError(f"Could not run ssh: {exc}") from exc + + if result.returncode != 0: + raise HelpdeskSearchError(result.stderr.strip() or "Remote MySQL query failed.") + + rows: list[dict[str, Any]] = [] + for line in result.stdout.splitlines(): + if not line.strip(): + continue + try: + rows.append(json.loads(bytes.fromhex(line.strip()).decode("utf-8"))) + except json.JSONDecodeError as exc: + raise HelpdeskSearchError(f"Remote query returned non-JSON row: {line[:200]}") from exc + except ValueError as exc: + raise HelpdeskSearchError(f"Remote query returned non-hex row: {line[:200]}") from exc + return rows + + def _mysql_runner_command(self) -> str: + # Ruby reads database.yml and execs mysql with MYSQL_PWD in the child + # environment. That avoids putting the DB password in ssh command args. + ruby = ( + "require 'yaml'; " + "c = YAML.load_file('config/database.yml')['production']; " + "ENV['MYSQL_PWD'] = c['password'].to_s; " + "args = ['--batch', '--raw', '--quick', '--skip-column-names', " + "'--default-character-set=utf8', '-h', c['host'].to_s, " + "'-P', (c['port'] || 3306).to_s, '-u', c['username'].to_s, c['database'].to_s]; " + "exec('mysql', *args)" + ) + return f"cd {shell_quote(self.remote_redmine)} && ruby -e {shell_quote(ruby)}" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Fetch and search Redmine helpdesk communications.") + parser.add_argument("--ssh-host", default=os.getenv("REDMINE_SSH_HOST", DEFAULT_SSH_HOST)) + parser.add_argument("--ssh-key", type=Path, default=Path(os.getenv("REDMINE_SSH_KEY", str(DEFAULT_SSH_KEY)))) + parser.add_argument("--remote-redmine", default=os.getenv("REDMINE_REMOTE_PATH", DEFAULT_REMOTE_REDMINE)) + parser.add_argument("--cache", type=Path, default=DEFAULT_CACHE_FILE) + + subparsers = parser.add_subparsers(dest="command", required=True) + + fetch_parser = subparsers.add_parser("fetch", help="Fetch helpdesk ticket/message docs into JSONL cache.") + fetch_parser.add_argument("--limit", type=int, help="Limit each document type for a quick test fetch.") + + search_parser = subparsers.add_parser("search", help="Search cached helpdesk tickets/messages.") + search_parser.add_argument("query") + search_parser.add_argument("--type", choices=["all", "ticket", "message"], default="all") + search_parser.add_argument("--limit", type=int, default=20) + search_parser.add_argument("--min-score", type=float, default=0.35) + search_parser.add_argument("--refresh", action="store_true", help="Fetch before searching.") + + timeline_parser = subparsers.add_parser("timeline", help="Show cached helpdesk timeline for a contact id.") + timeline_parser.add_argument("contact_id", type=int) + timeline_parser.add_argument("--limit", type=int, default=50) + timeline_parser.add_argument("--refresh", action="store_true", help="Fetch before showing timeline.") + + issues_parser = subparsers.add_parser("issues-by-contact", help="List cached helpdesk issues for a contact id.") + issues_parser.add_argument("contact_id", type=int) + issues_parser.add_argument("--limit", type=int, default=50) + issues_parser.add_argument("--refresh", action="store_true", help="Fetch before listing issues.") + + try: + if args := parser.parse_args(): + remote = RemoteRedmine(args.ssh_host, args.ssh_key, args.remote_redmine) + + if args.command == "fetch": + documents = fetch_documents(remote, args.limit) + write_jsonl(args.cache, documents) + print(f"Cached {len(documents)} documents in {args.cache}") + return 0 + + if args.command == "search": + if args.refresh or not args.cache.exists(): + documents = fetch_documents(remote, None) + write_jsonl(args.cache, documents) + documents = read_jsonl(args.cache) + matches = search_documents(documents, args.query, args.type, args.min_score) + print_matches(matches[: args.limit]) + return 0 + + if args.command == "timeline": + documents = load_cached_or_refresh(args, remote) + print_timeline(documents, args.contact_id, args.limit) + return 0 + + if args.command == "issues-by-contact": + documents = load_cached_or_refresh(args, remote) + print_issues_by_contact(documents, args.contact_id, args.limit) + return 0 + except HelpdeskSearchError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + return 1 + + +def fetch_documents(remote: RemoteRedmine, limit: int | None) -> list[dict[str, Any]]: + fetched_at = int(time.time()) + documents: list[dict[str, Any]] = [] + documents.extend(add_fetch_metadata(remote.mysql_json_lines(ticket_sql(limit)), fetched_at)) + documents.extend(add_fetch_metadata(remote.mysql_json_lines(message_sql(limit)), fetched_at)) + return documents + + +def add_fetch_metadata(documents: list[dict[str, Any]], fetched_at: int) -> list[dict[str, Any]]: + for document in documents: + document["fetched_at"] = fetched_at + document["text"] = clean_body_text(document.get("text")) + document["search_text"] = normalize( + " ".join( + flatten(document.get(key)) + for key in ( + "issue_id", + "issue_subject", + "contact_name", + "contact_company", + "contact_email", + "from_address", + "to_address", + "cc_address", + "message_id", + "text", + ) + ) + ) + return documents + + +def ticket_sql(limit: int | None) -> str: + limit_clause = sql_limit(limit) + return f""" +SELECT HEX(CAST(JSON_OBJECT( + 'doc_type', 'ticket', + 'doc_id', CONCAT('ticket:', ht.id), + 'helpdesk_ticket_id', ht.id, + 'issue_id', i.id, + 'project_id', i.project_id, + 'project_identifier', p.identifier, + 'contact_id', ht.contact_id, + 'contact_name', TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + 'contact_company', c.company, + 'contact_email', c.email, + 'from_address', ht.from_address, + 'to_address', ht.to_address, + 'cc_address', ht.cc_address, + 'message_id', ht.message_id, + 'source', ht.source, + 'is_incoming', ht.is_incoming, + 'issue_subject', i.subject, + 'status', s.name, + 'tracker', t.name, + 'assigned_to', TRIM(CONCAT_WS(' ', au.firstname, au.lastname)), + 'ticket_date', DATE_FORMAT(ht.ticket_date, '%Y-%m-%dT%H:%i:%sZ'), + 'issue_updated_on', DATE_FORMAT(i.updated_on, '%Y-%m-%dT%H:%i:%sZ'), + 'text', CONCAT_WS('\\n', + i.subject, + LEFT(i.description, 8000), + TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + c.company, + c.email, + ht.from_address, + ht.to_address, + ht.cc_address + ) +) AS CHAR)) AS document +FROM helpdesk_tickets ht +JOIN issues i ON i.id = ht.issue_id +LEFT JOIN contacts c ON c.id = ht.contact_id +LEFT JOIN projects p ON p.id = i.project_id +LEFT JOIN issue_statuses s ON s.id = i.status_id +LEFT JOIN trackers t ON t.id = i.tracker_id +LEFT JOIN users au ON au.id = i.assigned_to_id +ORDER BY ht.ticket_date DESC, ht.id DESC +{limit_clause}; +""" + + +def message_sql(limit: int | None) -> str: + limit_clause = sql_limit(limit) + return f""" +SELECT HEX(CAST(JSON_OBJECT( + 'doc_type', 'message', + 'doc_id', CONCAT('message:', jm.id), + 'journal_message_id', jm.id, + 'journal_id', j.id, + 'issue_id', i.id, + 'project_id', i.project_id, + 'project_identifier', p.identifier, + 'contact_id', jm.contact_id, + 'contact_name', TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + 'contact_company', c.company, + 'contact_email', c.email, + 'from_address', jm.from_address, + 'to_address', jm.to_address, + 'cc_address', jm.cc_address, + 'has_bcc_address', IF(jm.bcc_address IS NULL OR jm.bcc_address = '', false, true), + 'message_id', jm.message_id, + 'source', jm.source, + 'is_incoming', jm.is_incoming, + 'issue_subject', i.subject, + 'status', s.name, + 'tracker', t.name, + 'journal_user', TRIM(CONCAT_WS(' ', ju.firstname, ju.lastname)), + 'message_date', DATE_FORMAT(jm.message_date, '%Y-%m-%dT%H:%i:%sZ'), + 'journal_created_on', DATE_FORMAT(j.created_on, '%Y-%m-%dT%H:%i:%sZ'), + 'text', CONCAT_WS('\\n', + i.subject, + LEFT(j.notes, 8000), + TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + c.company, + c.email, + jm.from_address, + jm.to_address, + jm.cc_address + ) +) AS CHAR)) AS document +FROM journal_messages jm +JOIN journals j ON j.id = jm.journal_id +JOIN issues i ON i.id = j.journalized_id AND j.journalized_type = 'Issue' +LEFT JOIN contacts c ON c.id = jm.contact_id +LEFT JOIN projects p ON p.id = i.project_id +LEFT JOIN issue_statuses s ON s.id = i.status_id +LEFT JOIN trackers t ON t.id = i.tracker_id +LEFT JOIN users ju ON ju.id = j.user_id +ORDER BY jm.message_date DESC, jm.id DESC +{limit_clause}; +""" + + +def sql_limit(limit: int | None) -> str: + if limit is None: + return "" + return f"LIMIT {max(1, int(limit))}" + + +def load_cached_or_refresh(args: argparse.Namespace, remote: RemoteRedmine) -> list[dict[str, Any]]: + if args.refresh or not args.cache.exists(): + documents = fetch_documents(remote, None) + write_jsonl(args.cache, documents) + return documents + return read_jsonl(args.cache) + + +def write_jsonl(path: Path, documents: Iterable[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + for document in documents: + handle.write(json.dumps(document, ensure_ascii=False, sort_keys=True)) + handle.write("\n") + + +def read_jsonl(path: Path) -> list[dict[str, Any]]: + if not path.exists(): + raise HelpdeskSearchError(f"Cache does not exist: {path}. Run fetch first.") + documents: list[dict[str, Any]] = [] + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if line.strip(): + documents.append(json.loads(line)) + return documents + + +def search_documents( + documents: list[dict[str, Any]], + query: str, + doc_type: str, + min_score: float, +) -> list[tuple[float, dict[str, Any], str]]: + normalized_query = normalize(query) + query_tokens = [token for token in normalized_query.split() if token] + scored: list[tuple[float, dict[str, Any], str]] = [] + for document in documents: + if doc_type != "all" and document.get("doc_type") != doc_type: + continue + score, reason = score_document(document, normalized_query, query_tokens) + if score >= min_score: + scored.append((score, document, reason)) + return sorted(scored, key=lambda item: (-item[0], sort_date(item[1]), item[1].get("doc_id", ""))) + + +def score_document(document: dict[str, Any], query: str, query_tokens: list[str]) -> tuple[float, str]: + fields = weighted_fields(document) + best_score = 0.0 + best_reason = "" + for field, value, weight in fields: + normalized_value = value if field == "text" and isinstance(value, str) else normalize(value) + if not normalized_value: + continue + score = 0.0 + if query and query in normalized_value: + score = 1.0 * weight + elif query_tokens: + matched = sum(1 for token in query_tokens if token in normalized_value) + score = max(score, (matched / len(query_tokens)) * 0.85 * weight) + if field != "text" or len(normalized_value) < 500: + score = max(score, SequenceMatcher(None, query, normalized_value[:500]).ratio() * 0.65 * weight) + if score > best_score: + best_score = score + best_reason = field + return best_score, best_reason + + +def weighted_fields(document: dict[str, Any]) -> list[tuple[str, str, float]]: + return [ + ("issue", f"{document.get('issue_id', '')} {document.get('issue_subject', '')}", 1.3), + ("contact", " ".join(flatten(document.get(key)) for key in ("contact_name", "contact_company", "contact_email")), 1.2), + ("addresses", " ".join(flatten(document.get(key)) for key in ("from_address", "to_address", "cc_address")), 1.1), + ("message_id", flatten(document.get("message_id")), 1.0), + ("text", flatten(document.get("search_text") or document.get("text")), 1.0), + ] + + +def print_matches(matches: list[tuple[float, dict[str, Any], str]]) -> None: + if not matches: + print("No helpdesk documents matched.") + return + for score, document, reason in matches: + date = document.get("message_date") or document.get("ticket_date") or document.get("issue_updated_on") or "" + direction = "in" if document.get("is_incoming") else "out" + contact = display_contact(document) + print( + f"{document.get('doc_id')} issue=#{document.get('issue_id')} " + f"contact=#{document.get('contact_id')} {direction} {date} " + f"score={score:.2f} via {reason}" + ) + print(f" {document.get('issue_subject') or ''}") + if contact: + print(f" {contact}") + snippet = make_snippet(document.get("text") or "") + if snippet: + print(f" {snippet}") + + +def print_timeline(documents: list[dict[str, Any]], contact_id: int, limit: int) -> None: + rows = [doc for doc in documents if int(doc.get("contact_id") or 0) == contact_id] + rows.sort(key=sort_date, reverse=True) + for document in rows[:limit]: + date = document.get("message_date") or document.get("ticket_date") or "" + direction = "in" if document.get("is_incoming") else "out" + print(f"{date} {document.get('doc_type')} {direction} issue=#{document.get('issue_id')} {document.get('issue_subject')}") + + +def print_issues_by_contact(documents: list[dict[str, Any]], contact_id: int, limit: int) -> None: + tickets = [doc for doc in documents if doc.get("doc_type") == "ticket" and int(doc.get("contact_id") or 0) == contact_id] + tickets.sort(key=sort_date, reverse=True) + seen: set[int] = set() + count = 0 + for ticket in tickets: + issue_id = int(ticket.get("issue_id") or 0) + if issue_id in seen: + continue + seen.add(issue_id) + print(f"{ticket.get('ticket_date')} issue=#{issue_id} {ticket.get('status') or ''} {ticket.get('issue_subject')}") + count += 1 + if count >= limit: + break + + +def sort_date(document: dict[str, Any]) -> str: + return str(document.get("message_date") or document.get("ticket_date") or document.get("issue_updated_on") or "") + + +def display_contact(document: dict[str, Any]) -> str: + return " | ".join( + item + for item in [ + flatten(document.get("contact_name")), + flatten(document.get("contact_company")), + flatten(document.get("contact_email")), + ] + if item + ) + + +def make_snippet(value: str, length: int = 220) -> str: + value = re.sub(r"\s+", " ", value).strip() + if len(value) <= length: + return value + return value[: length - 3].rstrip() + "..." + + +def clean_body_text(value: Any) -> str: + text = flatten(value) + text = text.replace("\u200c", " ").replace("\u200d", " ").replace("\ufeff", " ") + return re.sub(r"\s+", " ", text).strip() + + +def normalize(value: Any) -> str: + value = flatten(value).lower() + value = re.sub(r"[^a-z0-9@.+#-]+", " ", value) + return re.sub(r"\s+", " ", value).strip() + + +def flatten(value: Any) -> str: + if value is None: + return "" + if isinstance(value, list): + return " ".join(flatten(item) for item in value) + if isinstance(value, dict): + return " ".join(flatten(item) for item in value.values()) + return str(value) + + +def shell_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/redmine_outbox_worker.py b/redmine_outbox_worker.py new file mode 100755 index 0000000..056b482 --- /dev/null +++ b/redmine_outbox_worker.py @@ -0,0 +1,504 @@ +#!/usr/bin/env python3 +"""External worker for Redmine event outbox rows. + +This worker runs outside Redmine. It claims pending `event_outbox_events` rows +from the Redmine database over SSH/MySQL, enriches them with read-only joins, +writes deterministic JSONL records, and marks rows processed only after the +write succeeds. +""" + +from __future__ import annotations + +import argparse +import json +import os +import socket +import subprocess +import sys +import time +import uuid +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + + +DEFAULT_SSH_HOST = "reddev@192.168.50.170" +DEFAULT_SSH_KEY = Path("/tmp/reddev") +DEFAULT_REMOTE_REDMINE = "/usr/share/redmine" +DEFAULT_OUTPUT = Path(".cache/redmine_outbox/derived_documents.jsonl") + + +class OutboxWorkerError(RuntimeError): + pass + + +@dataclass(frozen=True) +class RemoteRedmine: + ssh_host: str + ssh_key: Path + remote_redmine: str + + def mysql_json_lines(self, sql: str) -> list[dict[str, Any]]: + stdout = self.mysql(sql) + rows: list[dict[str, Any]] = [] + for line in stdout.splitlines(): + if not line.strip(): + continue + try: + rows.append(json.loads(bytes.fromhex(line.strip()).decode("utf-8"))) + except json.JSONDecodeError as exc: + raise OutboxWorkerError(f"Remote query returned non-JSON row: {line[:200]}") from exc + except ValueError as exc: + raise OutboxWorkerError(f"Remote query returned non-hex row: {line[:200]}") from exc + return rows + + def mysql(self, sql: str) -> str: + command = [ + "ssh", + "-i", + str(self.ssh_key), + "-o", + "IdentitiesOnly=yes", + self.ssh_host, + self._mysql_runner_command(), + ] + try: + result = subprocess.run( + command, + input=sql, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + except OSError as exc: + raise OutboxWorkerError(f"Could not run ssh: {exc}") from exc + + if result.returncode != 0: + raise OutboxWorkerError(result.stderr.strip() or "Remote MySQL command failed.") + return result.stdout + + def _mysql_runner_command(self) -> str: + ruby = ( + "require 'yaml'; " + "c = YAML.load_file('config/database.yml')['production']; " + "ENV['MYSQL_PWD'] = c['password'].to_s; " + "args = ['--batch', '--raw', '--quick', '--skip-column-names', " + "'--default-character-set=utf8', '-h', c['host'].to_s, " + "'-P', (c['port'] || 3306).to_s, '-u', c['username'].to_s, c['database'].to_s]; " + "exec('mysql', *args)" + ) + return f"cd {shell_quote(self.remote_redmine)} && ruby -e {shell_quote(ruby)}" + + +def main() -> int: + parser = argparse.ArgumentParser(description="Process Redmine event outbox rows into enriched JSONL documents.") + parser.add_argument("--ssh-host", default=os.getenv("REDMINE_SSH_HOST", DEFAULT_SSH_HOST)) + parser.add_argument("--ssh-key", type=Path, default=Path(os.getenv("REDMINE_SSH_KEY", str(DEFAULT_SSH_KEY)))) + parser.add_argument("--remote-redmine", default=os.getenv("REDMINE_REMOTE_PATH", DEFAULT_REMOTE_REDMINE)) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument("--batch-size", type=int, default=20) + parser.add_argument("--max-attempts", type=int, default=5) + parser.add_argument("--stale-lock-minutes", type=int, default=30) + parser.add_argument("--dry-run", action="store_true", help="Fetch and enrich pending rows without locking or marking them.") + parser.add_argument("--claim-only", action="store_true", help="Claim rows, print them, then release the claim without marking processed.") + args = parser.parse_args() + + remote = RemoteRedmine(args.ssh_host, args.ssh_key, args.remote_redmine) + worker_id = make_worker_id() + + try: + if args.dry_run: + events = pending_events(remote, args.batch_size, args.max_attempts, args.stale_lock_minutes) + else: + events = claim_events(remote, worker_id, args.batch_size, args.max_attempts, args.stale_lock_minutes) + + if args.claim_only: + print(json.dumps(events, indent=2, sort_keys=True)) + if not args.dry_run: + release_claims(remote, worker_id) + return 0 + + processed = 0 + for event in events: + try: + documents = enrich_event(remote, event) + if args.dry_run: + for document in documents: + print(json.dumps(document, ensure_ascii=False, sort_keys=True)) + else: + append_jsonl(args.output, documents) + mark_processed(remote, event["id"], worker_id) + processed += 1 + except Exception as exc: + if args.dry_run: + raise + mark_failed(remote, event["id"], worker_id, exc) + print(f"event #{event.get('id')} failed: {exc}", file=sys.stderr) + + action = "previewed" if args.dry_run else "processed" + print(f"{action} {processed} outbox event(s)") + return 0 + except OutboxWorkerError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + +def pending_events(remote: RemoteRedmine, limit: int, max_attempts: int, stale_lock_minutes: int) -> list[dict[str, Any]]: + return remote.mysql_json_lines( + f""" +SELECT HEX(CAST(JSON_OBJECT( + 'id', id, + 'event_type', event_type, + 'source_type', source_type, + 'source_id', source_id, + 'project_id', project_id, + 'issue_id', issue_id, + 'journal_id', journal_id, + 'user_id', user_id, + 'occurred_at', DATE_FORMAT(occurred_at, '%Y-%m-%dT%H:%i:%sZ'), + 'attempts', attempts, + 'payload', payload +) AS CHAR)) AS document +FROM event_outbox_events +WHERE processed_at IS NULL + AND attempts < {sql_int(max_attempts)} + AND (locked_at IS NULL OR locked_at < UTC_TIMESTAMP() - INTERVAL {sql_int(stale_lock_minutes)} MINUTE) +ORDER BY id +LIMIT {sql_int(limit)}; +""" + ) + + +def claim_events( + remote: RemoteRedmine, + worker_id: str, + limit: int, + max_attempts: int, + stale_lock_minutes: int, +) -> list[dict[str, Any]]: + remote.mysql( + f""" +UPDATE event_outbox_events +SET locked_at = UTC_TIMESTAMP(), locked_by = {sql_string(worker_id)} +WHERE processed_at IS NULL + AND attempts < {sql_int(max_attempts)} + AND (locked_at IS NULL OR locked_at < UTC_TIMESTAMP() - INTERVAL {sql_int(stale_lock_minutes)} MINUTE) +ORDER BY id +LIMIT {sql_int(limit)}; +""" + ) + return remote.mysql_json_lines( + f""" +SELECT HEX(CAST(JSON_OBJECT( + 'id', id, + 'event_type', event_type, + 'source_type', source_type, + 'source_id', source_id, + 'project_id', project_id, + 'issue_id', issue_id, + 'journal_id', journal_id, + 'user_id', user_id, + 'occurred_at', DATE_FORMAT(occurred_at, '%Y-%m-%dT%H:%i:%sZ'), + 'attempts', attempts, + 'payload', payload +) AS CHAR)) AS document +FROM event_outbox_events +WHERE processed_at IS NULL + AND locked_by = {sql_string(worker_id)} +ORDER BY id +LIMIT {sql_int(limit)}; +""" + ) + + +def enrich_event(remote: RemoteRedmine, event: dict[str, Any]) -> list[dict[str, Any]]: + payload = parse_payload(event.get("payload")) + documents: list[dict[str, Any]] = [event_document(event, payload)] + event_type = str(event.get("event_type") or "") + + if event_type.startswith("helpdesk_ticket."): + documents.extend(fetch_ticket_documents(remote, [payload.get("helpdesk_ticket_id") or event.get("source_id")])) + elif event_type.startswith("journal_message."): + documents.extend(fetch_message_documents(remote, [payload.get("journal_message_id") or event.get("source_id")])) + elif event_type.startswith("issue.") and event.get("issue_id"): + documents.extend(fetch_tickets_by_issue(remote, [event.get("issue_id")])) + elif event_type.startswith("journal.") and event.get("journal_id"): + documents.extend(fetch_messages_by_journal(remote, [event.get("journal_id")])) + elif event_type.startswith("contact."): + documents.extend(fetch_contact_documents(remote, [payload.get("contact_id") or event.get("source_id")])) + + return [with_event_context(document, event) for document in documents] + + +def event_document(event: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]: + return { + "doc_type": "event", + "doc_id": f"event:{event.get('id')}", + "event_id": event.get("id"), + "event_type": event.get("event_type"), + "source_type": event.get("source_type"), + "source_id": event.get("source_id"), + "project_id": event.get("project_id"), + "issue_id": event.get("issue_id"), + "journal_id": event.get("journal_id"), + "user_id": event.get("user_id"), + "occurred_at": event.get("occurred_at"), + "payload": payload, + } + + +def fetch_ticket_documents(remote: RemoteRedmine, ids: Iterable[Any]) -> list[dict[str, Any]]: + id_list = sql_id_list(ids) + if not id_list: + return [] + return remote.mysql_json_lines(ticket_sql(f"ht.id IN ({id_list})")) + + +def fetch_tickets_by_issue(remote: RemoteRedmine, issue_ids: Iterable[Any]) -> list[dict[str, Any]]: + id_list = sql_id_list(issue_ids) + if not id_list: + return [] + return remote.mysql_json_lines(ticket_sql(f"ht.issue_id IN ({id_list})")) + + +def fetch_message_documents(remote: RemoteRedmine, ids: Iterable[Any]) -> list[dict[str, Any]]: + id_list = sql_id_list(ids) + if not id_list: + return [] + return remote.mysql_json_lines(message_sql(f"jm.id IN ({id_list})")) + + +def fetch_messages_by_journal(remote: RemoteRedmine, journal_ids: Iterable[Any]) -> list[dict[str, Any]]: + id_list = sql_id_list(journal_ids) + if not id_list: + return [] + return remote.mysql_json_lines(message_sql(f"jm.journal_id IN ({id_list})")) + + +def fetch_contact_documents(remote: RemoteRedmine, ids: Iterable[Any]) -> list[dict[str, Any]]: + id_list = sql_id_list(ids) + if not id_list: + return [] + return remote.mysql_json_lines( + f""" +SELECT HEX(CAST(JSON_OBJECT( + 'doc_type', 'contact', + 'doc_id', CONCAT('contact:', c.id), + 'contact_id', c.id, + 'contact_name', TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + 'contact_company', c.company, + 'contact_email', c.email, + 'is_company', c.is_company, + 'created_on', DATE_FORMAT(c.created_on, '%Y-%m-%dT%H:%i:%sZ'), + 'updated_on', DATE_FORMAT(c.updated_on, '%Y-%m-%dT%H:%i:%sZ'), + 'text', CONCAT_WS('\\n', + TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + c.company, + c.email + ) +) AS CHAR)) AS document +FROM contacts c +WHERE c.id IN ({id_list}) +ORDER BY c.id; +""" + ) + + +def ticket_sql(where_clause: str) -> str: + return f""" +SELECT HEX(CAST(JSON_OBJECT( + 'doc_type', 'ticket', + 'doc_id', CONCAT('ticket:', ht.id), + 'helpdesk_ticket_id', ht.id, + 'issue_id', i.id, + 'project_id', i.project_id, + 'project_identifier', p.identifier, + 'contact_id', ht.contact_id, + 'contact_name', TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + 'contact_company', c.company, + 'contact_email', c.email, + 'from_address', ht.from_address, + 'to_address', ht.to_address, + 'cc_address', ht.cc_address, + 'message_id', ht.message_id, + 'source', ht.source, + 'is_incoming', ht.is_incoming, + 'issue_subject', i.subject, + 'status', s.name, + 'tracker', t.name, + 'assigned_to', TRIM(CONCAT_WS(' ', au.firstname, au.lastname)), + 'ticket_date', DATE_FORMAT(ht.ticket_date, '%Y-%m-%dT%H:%i:%sZ'), + 'issue_updated_on', DATE_FORMAT(i.updated_on, '%Y-%m-%dT%H:%i:%sZ'), + 'text', CONCAT_WS('\\n', + i.subject, + LEFT(i.description, 8000), + TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + c.company, + c.email, + ht.from_address, + ht.to_address, + ht.cc_address + ) +) AS CHAR)) AS document +FROM helpdesk_tickets ht +JOIN issues i ON i.id = ht.issue_id +LEFT JOIN contacts c ON c.id = ht.contact_id +LEFT JOIN projects p ON p.id = i.project_id +LEFT JOIN issue_statuses s ON s.id = i.status_id +LEFT JOIN trackers t ON t.id = i.tracker_id +LEFT JOIN users au ON au.id = i.assigned_to_id +WHERE {where_clause} +ORDER BY ht.id; +""" + + +def message_sql(where_clause: str) -> str: + return f""" +SELECT HEX(CAST(JSON_OBJECT( + 'doc_type', 'message', + 'doc_id', CONCAT('message:', jm.id), + 'journal_message_id', jm.id, + 'journal_id', j.id, + 'issue_id', i.id, + 'project_id', i.project_id, + 'project_identifier', p.identifier, + 'contact_id', jm.contact_id, + 'contact_name', TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + 'contact_company', c.company, + 'contact_email', c.email, + 'from_address', jm.from_address, + 'to_address', jm.to_address, + 'cc_address', jm.cc_address, + 'has_bcc_address', IF(jm.bcc_address IS NULL OR jm.bcc_address = '', false, true), + 'message_id', jm.message_id, + 'source', jm.source, + 'is_incoming', jm.is_incoming, + 'issue_subject', i.subject, + 'status', s.name, + 'tracker', t.name, + 'journal_user', TRIM(CONCAT_WS(' ', ju.firstname, ju.lastname)), + 'message_date', DATE_FORMAT(jm.message_date, '%Y-%m-%dT%H:%i:%sZ'), + 'journal_created_on', DATE_FORMAT(j.created_on, '%Y-%m-%dT%H:%i:%sZ'), + 'text', CONCAT_WS('\\n', + i.subject, + LEFT(j.notes, 8000), + TRIM(CONCAT_WS(' ', c.first_name, c.middle_name, c.last_name)), + c.company, + c.email, + jm.from_address, + jm.to_address, + jm.cc_address + ) +) AS CHAR)) AS document +FROM journal_messages jm +JOIN journals j ON j.id = jm.journal_id +JOIN issues i ON i.id = j.journalized_id AND j.journalized_type = 'Issue' +LEFT JOIN contacts c ON c.id = jm.contact_id +LEFT JOIN projects p ON p.id = i.project_id +LEFT JOIN issue_statuses s ON s.id = i.status_id +LEFT JOIN trackers t ON t.id = i.tracker_id +LEFT JOIN users ju ON ju.id = j.user_id +WHERE {where_clause} +ORDER BY jm.id; +""" + + +def with_event_context(document: dict[str, Any], event: dict[str, Any]) -> dict[str, Any]: + document["event_id"] = event.get("id") + document["event_type"] = event.get("event_type") + document["event_occurred_at"] = event.get("occurred_at") + document["derived_at"] = int(time.time()) + return document + + +def append_jsonl(path: Path, documents: Iterable[dict[str, Any]]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + for document in documents: + handle.write(json.dumps(document, ensure_ascii=False, sort_keys=True)) + handle.write("\n") + + +def mark_processed(remote: RemoteRedmine, event_id: Any, worker_id: str) -> None: + remote.mysql( + f""" +UPDATE event_outbox_events +SET processed_at = UTC_TIMESTAMP(), locked_at = NULL, locked_by = NULL, last_error = NULL +WHERE id = {sql_int(event_id)} + AND locked_by = {sql_string(worker_id)}; +""" + ) + + +def mark_failed(remote: RemoteRedmine, event_id: Any, worker_id: str, exc: Exception) -> None: + message = f"{exc.__class__.__name__}: {exc}" + remote.mysql( + f""" +UPDATE event_outbox_events +SET attempts = attempts + 1, + last_error = {sql_string(message[:4000])}, + locked_at = NULL, + locked_by = NULL +WHERE id = {sql_int(event_id)} + AND locked_by = {sql_string(worker_id)}; +""" + ) + + +def release_claims(remote: RemoteRedmine, worker_id: str) -> None: + remote.mysql( + f""" +UPDATE event_outbox_events +SET locked_at = NULL, locked_by = NULL +WHERE processed_at IS NULL + AND locked_by = {sql_string(worker_id)}; +""" + ) + + +def parse_payload(value: Any) -> dict[str, Any]: + if isinstance(value, dict): + return value + if not value: + return {} + try: + parsed = json.loads(str(value)) + except json.JSONDecodeError: + return {} + return parsed if isinstance(parsed, dict) else {} + + +def sql_id_list(values: Iterable[Any]) -> str: + ids = [] + for value in values: + try: + int_value = int(value) + except (TypeError, ValueError): + continue + if int_value > 0: + ids.append(str(int_value)) + return ",".join(sorted(set(ids), key=int)) + + +def sql_int(value: Any) -> int: + try: + return max(0, int(value)) + except (TypeError, ValueError): + return 0 + + +def sql_string(value: str) -> str: + return "'" + str(value).replace("\\", "\\\\").replace("'", "\\'") + "'" + + +def shell_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" + + +def make_worker_id() -> str: + return f"{socket.gethostname()}:{os.getpid()}:{uuid.uuid4().hex[:12]}" + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/reset_helpdesk_mail_settings.py b/reset_helpdesk_mail_settings.py new file mode 100755 index 0000000..ab782f6 --- /dev/null +++ b/reset_helpdesk_mail_settings.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Reset RedmineUP Helpdesk mail settings on the LAN test Redmine instance. + +This is intended to be run after importing a production database into the test +instance. It finds projects with the Helpdesk module enabled and rewrites only +the incoming/outgoing mail settings so test mail flows through Mailpit. +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +DEFAULT_SSH_HOST = "reddev@192.168.50.170" +DEFAULT_SSH_KEY = Path("/tmp/reddev") +DEFAULT_REMOTE_REDMINE = "/usr/share/redmine" +DEFAULT_MAILPIT_HOST = "192.168.1.105" + +MAIL_SETTING_NAMES = [ + "helpdesk_protocol", + "helpdesk_host", + "helpdesk_port", + "helpdesk_username", + "helpdesk_password", + "helpdesk_use_ssl", + "helpdesk_imap_folder", + "helpdesk_move_on_success", + "helpdesk_move_on_failure", + "helpdesk_apop", + "helpdesk_delete_unprocessed", + "helpdesk_smtp_use_default_settings", + "helpdesk_smtp_server", + "helpdesk_smtp_port", + "helpdesk_smtp_domain", + "helpdesk_smtp_authentication", + "helpdesk_smtp_username", + "helpdesk_smtp_password", + "helpdesk_smtp_ssl", + "helpdesk_smtp_tls", + "helpdesk_answer_from", +] + +SECRET_NAMES = {"helpdesk_password", "helpdesk_smtp_password"} + + +class ResetError(RuntimeError): + pass + + +@dataclass(frozen=True) +class RemoteRedmine: + ssh_host: str + ssh_key: Path + remote_redmine: str + + def mysql_json_lines(self, sql: str) -> list[dict[str, Any]]: + stdout = self.mysql(sql) + rows: list[dict[str, Any]] = [] + for line in stdout.splitlines(): + if not line.strip(): + continue + try: + rows.append(json.loads(bytes.fromhex(line.strip()).decode("utf-8"))) + except (ValueError, json.JSONDecodeError) as exc: + raise ResetError(f"Remote query returned an unexpected row: {line[:200]}") from exc + return rows + + def mysql(self, sql: str) -> str: + command = [ + "ssh", + "-i", + str(self.ssh_key), + "-o", + "IdentitiesOnly=yes", + self.ssh_host, + self._mysql_runner_command(), + ] + try: + result = subprocess.run( + command, + input=sql, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + except OSError as exc: + raise ResetError(f"Could not run ssh: {exc}") from exc + + if result.returncode != 0: + raise ResetError(result.stderr.strip() or "Remote MySQL command failed.") + return result.stdout + + def _mysql_runner_command(self) -> str: + ruby = ( + "require 'yaml'; " + "c = YAML.load_file('config/database.yml')['production']; " + "ENV['MYSQL_PWD'] = c['password'].to_s; " + "args = ['--batch', '--raw', '--quick', '--skip-column-names', " + "'--default-character-set=utf8', '-h', c['host'].to_s, " + "'-P', (c['port'] || 3306).to_s, '-u', c['username'].to_s, c['database'].to_s]; " + "exec('mysql', *args)" + ) + return f"cd {shell_quote(self.remote_redmine)} && ruby -e {shell_quote(ruby)}" + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Reset Helpdesk mail settings for projects with the contacts_helpdesk module enabled." + ) + parser.add_argument("--ssh-host", default=os.getenv("REDMINE_SSH_HOST", DEFAULT_SSH_HOST)) + parser.add_argument("--ssh-key", type=Path, default=Path(os.getenv("REDMINE_SSH_KEY", str(DEFAULT_SSH_KEY)))) + parser.add_argument("--remote-redmine", default=os.getenv("REDMINE_REMOTE_PATH", DEFAULT_REMOTE_REDMINE)) + parser.add_argument("--mailpit-host", default=DEFAULT_MAILPIT_HOST, help="Host Redmine should use to reach Mailpit.") + parser.add_argument("--pop3-port", type=int, default=1110) + parser.add_argument("--smtp-port", type=int, default=1025) + parser.add_argument("--username", default="test") + parser.add_argument("--password", default="testpass") + parser.add_argument("--smtp-domain", default="example.test") + parser.add_argument( + "--from-pattern", + default="helpdesk-{identifier}@example.test", + help="Pattern for helpdesk_answer_from. Available fields: {id}, {identifier}, {name}.", + ) + parser.add_argument( + "--project", + action="append", + default=[], + help="Optional project id or identifier to limit changes. Can be passed more than once.", + ) + parser.add_argument("--dry-run", action="store_true", help="Show affected projects and settings without writing.") + args = parser.parse_args() + + remote = RemoteRedmine(args.ssh_host, args.ssh_key, args.remote_redmine) + + try: + projects = find_helpdesk_projects(remote, args.project) + if not projects: + print("No active projects with contacts_helpdesk enabled matched the requested filters.") + return 0 + + print(f"Matched {len(projects)} Helpdesk-enabled project(s):") + for project in projects: + print(f" - #{project['id']} {project['identifier']} ({project['name']})") + + values = build_values(args, projects) + if args.dry_run: + print("\nDry run. Planned settings:") + print_plan(values) + return 0 + + apply_values(remote, values) + print(f"\nUpdated {len(values)} setting row(s) across {len(projects)} project(s).") + print("Password values were written but not displayed.") + return 0 + except ResetError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + + +def find_helpdesk_projects(remote: RemoteRedmine, filters: list[str]) -> list[dict[str, Any]]: + where = ["p.status = 1"] + if filters: + clauses = [] + for value in filters: + if str(value).isdigit(): + clauses.append(f"p.id = {sql_int(value)}") + clauses.append(f"p.identifier = {sql_string(value)}") + where.append("(" + " OR ".join(clauses) + ")") + + return remote.mysql_json_lines( + f""" +SELECT HEX(CAST(JSON_OBJECT( + 'id', p.id, + 'identifier', p.identifier, + 'name', p.name +) AS CHAR)) AS document +FROM projects p +JOIN enabled_modules em + ON em.project_id = p.id + AND em.name = 'contacts_helpdesk' +WHERE {' AND '.join(where)} +ORDER BY p.identifier; +""" + ) + + +def build_values(args: argparse.Namespace, projects: list[dict[str, Any]]) -> list[tuple[int, str, str]]: + rows: list[tuple[int, str, str]] = [] + for project in projects: + project_id = int(project["id"]) + answer_from = args.from_pattern.format( + id=project_id, + identifier=project["identifier"], + name=project["name"], + ) + settings = { + "helpdesk_protocol": "pop3", + "helpdesk_host": args.mailpit_host, + "helpdesk_port": str(args.pop3_port), + "helpdesk_username": args.username, + "helpdesk_password": args.password, + "helpdesk_use_ssl": "0", + "helpdesk_imap_folder": "", + "helpdesk_move_on_success": "", + "helpdesk_move_on_failure": "", + "helpdesk_apop": "0", + "helpdesk_delete_unprocessed": "0", + # RedmineUP's UI label is confusing: 1 means use the custom SMTP block. + "helpdesk_smtp_use_default_settings": "1", + "helpdesk_smtp_server": args.mailpit_host, + "helpdesk_smtp_port": str(args.smtp_port), + "helpdesk_smtp_domain": args.smtp_domain, + "helpdesk_smtp_authentication": "", + "helpdesk_smtp_username": "", + "helpdesk_smtp_password": "", + "helpdesk_smtp_ssl": "0", + "helpdesk_smtp_tls": "0", + "helpdesk_answer_from": answer_from, + } + rows.extend((project_id, name, settings[name]) for name in MAIL_SETTING_NAMES) + return rows + + +def apply_values(remote: RemoteRedmine, rows: list[tuple[int, str, str]]) -> None: + statements = ["START TRANSACTION;"] + for project_id, name, value in rows: + project_id_sql = sql_int(project_id) + name_sql = sql_string(name) + value_sql = sql_string(value) + statements.append( + f""" +UPDATE contacts_settings +SET value = {value_sql}, updated_on = UTC_TIMESTAMP() +WHERE project_id = {project_id_sql} + AND name = {name_sql}; +INSERT INTO contacts_settings (project_id, name, value, updated_on) +SELECT {project_id_sql}, {name_sql}, {value_sql}, UTC_TIMESTAMP() +WHERE NOT EXISTS ( + SELECT 1 + FROM contacts_settings + WHERE project_id = {project_id_sql} + AND name = {name_sql} +); +""" + ) + statements.append("COMMIT;") + + remote.mysql("\n".join(statements)) + + +def print_plan(rows: list[tuple[int, str, str]]) -> None: + current_project_id: int | None = None + for project_id, name, value in rows: + if project_id != current_project_id: + current_project_id = project_id + print(f"\nProject #{project_id}") + display_value = "" if name in SECRET_NAMES else value + print(f" {name} = {display_value}") + + +def sql_int(value: Any) -> int: + try: + return max(0, int(value)) + except (TypeError, ValueError): + return 0 + + +def sql_string(value: Any) -> str: + return "'" + str(value).replace("\\", "\\\\").replace("'", "\\'") + "'" + + +def shell_quote(value: str) -> str: + return "'" + value.replace("'", "'\"'\"'") + "'" + + +if __name__ == "__main__": + raise SystemExit(main())